From 0c44051d2ab8a78d8b979aaf950f5dc1c5c5bfa4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 24 Apr 2024 21:05:09 +0200 Subject: [PATCH 01/96] Bump version to 2024.5.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ba83eca58d8..1abfe08b93c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 7e3038f6ee2..34c7d648795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0.dev0" +version = "2024.5.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c65187cbfbd236186aa3cc78f8554b36da649e09 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 25 Apr 2024 21:06:52 +0200 Subject: [PATCH 02/96] Fix climate entity creation when Shelly WallDisplay uses external relay as actuator (#115216) * Fix climate entity creation when Shelly WallDisplay uses external relay as actuator * More comments * Wrap condition into function --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/climate.py | 6 +++- homeassistant/components/shelly/switch.py | 16 ++++++--- homeassistant/components/shelly/utils.py | 5 +++ tests/components/shelly/test_climate.py | 40 +++++++++++++++++++++- tests/components/shelly/test_switch.py | 1 + 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index b368b38820e..81289bc1a9b 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -132,7 +132,11 @@ def async_setup_rpc_entry( climate_ids = [] for id_ in climate_key_ids: climate_ids.append(id_) - + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator if is_rpc_thermostat_internal_actuator(coordinator.device.status): # Wall Display relay is used as the thermostat actuator, # we need to remove a switch entity diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 14fec43c58b..81b16d48ab8 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -43,6 +43,7 @@ from .utils import ( is_block_channel_type_light, is_rpc_channel_type_light, is_rpc_thermostat_internal_actuator, + is_rpc_thermostat_mode, ) @@ -140,12 +141,19 @@ def async_setup_rpc_entry( continue if coordinator.model == MODEL_WALL_DISPLAY: - if not is_rpc_thermostat_internal_actuator(coordinator.device.status): - # Wall Display relay is not used as the thermostat actuator, - # we need to remove a climate entity + # There are three configuration scenarios for WallDisplay: + # - relay mode (no thermostat) + # - thermostat mode using the internal relay as an actuator + # - thermostat mode using an external (from another device) relay as + # an actuator + if not is_rpc_thermostat_mode(id_, coordinator.device.status): + # The device is not in thermostat mode, we need to remove a climate + # entity unique_id = f"{coordinator.mac}-thermostat:{id_}" async_remove_shelly_entity(hass, "climate", unique_id) - else: + elif is_rpc_thermostat_internal_actuator(coordinator.device.status): + # The internal relay is an actuator, skip this ID so as not to create + # a switch entity continue switch_ids.append(id_) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index ce98e0d5c12..b7cb2f1476a 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -500,3 +500,8 @@ def async_remove_shelly_rpc_entities( if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"): LOGGER.debug("Removing entity: %s", entity_id) entity_reg.async_remove(entity_id) + + +def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool: + """Return True if 'thermostat:' is present in the status.""" + return f"thermostat:{ident}" in status diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 9fee3468f11..9946dd7640d 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,12 @@ from homeassistant.components.climate import ( from homeassistant.components.shelly.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers.device_registry import DeviceRegistry @@ -711,3 +716,36 @@ async def test_wall_display_thermostat_mode( entry = entity_registry.async_get(climate_entity_id) assert entry assert entry.unique_id == "123456789ABC-thermostat:0" + + +async def test_wall_display_thermostat_mode_external_actuator( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Wall Display in thermostat mode with an external actuator.""" + climate_entity_id = "climate.test_name" + switch_entity_id = "switch.test_switch_0" + + new_status = deepcopy(mock_rpc_device.status) + new_status["sys"]["relay_in_thermostat"] = False + monkeypatch.setattr(mock_rpc_device, "status", new_status) + + await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) + + # the switch entity should be created + state = hass.states.get(switch_entity_id) + assert state + assert state.state == STATE_ON + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 + + # the climate entity should be created + state = hass.states.get(climate_entity_id) + assert state + assert state.state == HVACMode.HEAT + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 + + entry = entity_registry.async_get(climate_entity_id) + assert entry + assert entry.unique_id == "123456789ABC-thermostat:0" diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py index fe2c4354afc..dd214c8841d 100644 --- a/tests/components/shelly/test_switch.py +++ b/tests/components/shelly/test_switch.py @@ -330,6 +330,7 @@ async def test_wall_display_relay_mode( new_status = deepcopy(mock_rpc_device.status) new_status["sys"]["relay_in_thermostat"] = False + new_status.pop("thermostat:0") monkeypatch.setattr(mock_rpc_device, "status", new_status) await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) From 0eace572c6f879443c261dfd524ed19680273370 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 27 Apr 2024 09:24:11 +0200 Subject: [PATCH 03/96] Don't create event entries for lighting4 rfxtrx devices (#115716) These have no standardized command need to be reworked in the backing library to support exposing as events. Fixes #115545 --- homeassistant/components/rfxtrx/event.py | 6 +++++- tests/components/rfxtrx/test_event.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/event.py b/homeassistant/components/rfxtrx/event.py index 7e73919aacd..5c3944dc74b 100644 --- a/homeassistant/components/rfxtrx/event.py +++ b/homeassistant/components/rfxtrx/event.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry +from .const import DEVICE_PACKET_TYPE_LIGHTING4 _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,10 @@ async def async_setup_entry( """Set up config entry.""" def _supported(event: RFXtrxEvent) -> bool: - return isinstance(event, (ControlEvent, SensorEvent)) + return ( + isinstance(event, (ControlEvent, SensorEvent)) + and event.device.packettype != DEVICE_PACKET_TYPE_LIGHTING4 + ) def _constructor( event: RFXtrxEvent, diff --git a/tests/components/rfxtrx/test_event.py b/tests/components/rfxtrx/test_event.py index 1a4305d97f6..035949efe3b 100644 --- a/tests/components/rfxtrx/test_event.py +++ b/tests/components/rfxtrx/test_event.py @@ -10,6 +10,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.rfxtrx import get_rfx_object from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import setup_rfx_test_cfg @@ -101,3 +102,25 @@ async def test_invalid_event_type( await hass.async_block_till_done() assert hass.states.get("event.arc_c1") == state + + +async def test_ignoring_lighting4(hass: HomeAssistant, rfxtrx) -> None: + """Test with 1 sensor.""" + entry = await setup_rfx_test_cfg( + hass, + devices={ + "0913000022670e013970": { + "data_bits": 4, + "command_on": 0xE, + "command_off": 0x7, + } + }, + ) + + registry = er.async_get(hass) + entries = [ + entry + for entry in registry.entities.get_entries_for_config_entry_id(entry.entry_id) + if entry.domain == Platform.EVENT + ] + assert entries == [] From d6f1d0666c3fec1f47d9f88e0bcd9a36febedcce Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 25 Apr 2024 19:58:13 +0200 Subject: [PATCH 04/96] Update rfxtrx to 0.31.1 (#116125) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index ec902855f27..bb3701e2e31 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "iot_class": "local_push", "loggers": ["RFXtrx"], - "requirements": ["pyRFXtrx==0.31.0"] + "requirements": ["pyRFXtrx==0.31.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ee8a074bf6b..b58b5948cad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1661,7 +1661,7 @@ pyEmby==1.9 pyHik==0.3.2 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.sony_projector pySDCP==1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2eb9a80281f..75eb06c924d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1311,7 +1311,7 @@ pyDuotecno==2024.3.2 pyElectra==1.2.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.31.0 +pyRFXtrx==0.31.1 # homeassistant.components.tibber pyTibber==0.28.2 From f91266908dd3159cad0accfb99a1e14170b7492b Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 25 Apr 2024 19:57:15 +0200 Subject: [PATCH 05/96] Bump pyfibaro to 0.7.8 (#116126) --- homeassistant/components/fibaro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index bb1558f998b..39850672d06 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["pyfibaro"], - "requirements": ["pyfibaro==0.7.7"] + "requirements": ["pyfibaro==0.7.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index b58b5948cad..1e18b1833c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1818,7 +1818,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.7 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75eb06c924d..74205a57b64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1417,7 +1417,7 @@ pyevilgenius==2.0.0 pyezviz==0.2.1.2 # homeassistant.components.fibaro -pyfibaro==0.7.7 +pyfibaro==0.7.8 # homeassistant.components.fido pyfido==2.1.2 From 74f32cfa90ec66eb8c8dfae596c931e6fc6218db Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 27 Apr 2024 10:26:11 +0300 Subject: [PATCH 06/96] Avoid blocking the event loop when unloading Monoprice (#116141) * Avoid blocking the event loop when unloading Monoprice * Code review suggestions --- .../components/monoprice/__init__.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 57282fb6545..c7683ebedd6 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -57,11 +57,25 @@ 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: - hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() - hass.data[DOMAIN].pop(entry.entry_id) + if not unload_ok: + return False - return unload_ok + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + def _cleanup(monoprice) -> None: + """Destroy the Monoprice object. + + Destroying the Monoprice closes the serial connection, do it in an executor so the garbage + collection does not block. + """ + del monoprice + + monoprice = hass.data[DOMAIN][entry.entry_id][MONOPRICE_OBJECT] + hass.data[DOMAIN].pop(entry.entry_id) + + await hass.async_add_executor_job(_cleanup, monoprice) + + return True async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: From c8d025f52546d117ba6130615d4d8d6a44df5a39 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 13:02:18 +0200 Subject: [PATCH 07/96] Remove deprecation warnings for relative_time (#116144) * Remove deprecation warnings for relative_time * Update homeassistant/helpers/template.py Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> --------- Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> --- .../components/homeassistant/strings.json | 4 --- homeassistant/helpers/template.py | 26 +++---------------- tests/helpers/test_template.py | 6 +---- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 5cdd47d8be4..09b2f17c947 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -56,10 +56,6 @@ "config_entry_reauth": { "title": "[%key:common::config_flow::title::reauth%]", "description": "Reauthentication is needed" - }, - "template_function_relative_time_deprecated": { - "title": "The {relative_time} template function is deprecated", - "description": "The {relative_time} template function is deprecated in Home Assistant. Please use the {time_since} or {time_until} template functions instead." } }, "system_health": { diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 335d6842548..ea45ac4e74a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -59,7 +59,6 @@ from homeassistant.const import ( UnitOfLength, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, Context, HomeAssistant, State, @@ -2480,30 +2479,11 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: Make sure date is not in the future, or else it will return None. If the input are not a datetime object the input will be returned unmodified. + + Note: This template function is deprecated in favor of `time_until`, but is still + supported so as not to break old templates. """ - def warn_relative_time_deprecated() -> None: - ir = issue_registry.async_get(hass) - issue_id = "template_function_relative_time_deprecated" - if ir.async_get_issue(HA_DOMAIN, issue_id): - return - issue_registry.async_create_issue( - hass, - HA_DOMAIN, - issue_id, - breaks_in_ha_version="2024.11.0", - is_fixable=False, - severity=issue_registry.IssueSeverity.WARNING, - translation_key=issue_id, - translation_placeholders={ - "relative_time": "relative_time()", - "time_since": "time_since()", - "time_until": "time_until()", - }, - ) - _LOGGER.warning("Template function 'relative_time' is deprecated") - - warn_relative_time_deprecated() if (render_info := _render_info.get()) is not None: render_info.has_time = True diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d134570d119..a241f6b7234 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfVolume, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import ( area_registry as ar, @@ -2240,7 +2240,6 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: """Test relative_time method.""" hass.config.set_time_zone("UTC") now = datetime.strptime("2000-01-01 10:00:00 +00:00", "%Y-%m-%d %H:%M:%S %z") - issue_registry = ir.async_get(hass) relative_time_template = ( '{{relative_time(strptime("2000-01-01 09:00:00", "%Y-%m-%d %H:%M:%S"))}}' ) @@ -2250,9 +2249,6 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: hass, ).async_render() assert result == "1 hour" - assert issue_registry.async_get_issue( - HA_DOMAIN, "template_function_relative_time_deprecated" - ) result = template.Template( ( "{{" From 18f1c0c9f3e82b2d62b33f39ad48019ed77081e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 13:01:41 +0200 Subject: [PATCH 08/96] Fix lying docstring for relative_time template function (#116146) * Fix lying docstring for relative_time template function * Update homeassistant/helpers/template.py Co-authored-by: Martin Hjelmare --------- Co-authored-by: Martin Hjelmare --- homeassistant/helpers/template.py | 3 ++- tests/helpers/test_template.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ea45ac4e74a..c12494ba71b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2476,7 +2476,8 @@ def relative_time(hass: HomeAssistant, value: Any) -> Any: The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will be returned. - Make sure date is not in the future, or else it will return None. + If the input datetime is in the future, + the input datetime will be returned. If the input are not a datetime object the input will be returned unmodified. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index a241f6b7234..1e2e512cf3d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2303,6 +2303,38 @@ def test_relative_time(mock_is_safe, hass: HomeAssistant) -> None: ).async_render() assert result == "string" + # Test behavior when current time is same as the input time + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 10:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "0 seconds" + + # Test behavior when the input time is in the future + result = template.Template( + ( + "{{" + " relative_time(" + " strptime(" + ' "2000-01-01 11:00:00 +00:00",' + ' "%Y-%m-%d %H:%M:%S %z"' + " )" + " )" + "}}" + ), + hass, + ).async_render() + assert result == "2000-01-01 11:00:00+00:00" + info = template.Template(relative_time_template, hass).async_render_to_info() assert info.has_time is True From 571c86cb91a48bdc5bcf2fe221598690c21f9bae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 27 Apr 2024 09:26:26 +0200 Subject: [PATCH 09/96] Handle invalid device type in onewire (#116153) * Make device type optional in onewire * Add comment --- homeassistant/components/onewire/binary_sensor.py | 2 +- homeassistant/components/onewire/model.py | 2 +- homeassistant/components/onewire/onewirehub.py | 10 +++++++--- homeassistant/components/onewire/sensor.py | 4 ++-- homeassistant/components/onewire/switch.py | 2 +- tests/components/onewire/const.py | 8 +++++++- .../onewire/snapshots/test_binary_sensor.ambr | 12 ++++++++++++ tests/components/onewire/snapshots/test_sensor.ambr | 12 ++++++++++++ tests/components/onewire/snapshots/test_switch.ambr | 12 ++++++++++++ 9 files changed, 55 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index d2e66609103..3c2ca3529cc 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -117,7 +117,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireBinarySensor]: device_type = device.type device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 9deaca2d121..a59953dcd25 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -16,4 +16,4 @@ class OWDeviceDescription: family: str id: str path: str - type: str + type: str | None diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index b01cc6ba3d6..2dc617ba039 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -45,7 +45,7 @@ DEVICE_MANUFACTURER = { _LOGGER = logging.getLogger(__name__) -def _is_known_device(device_family: str, device_type: str) -> bool: +def _is_known_device(device_family: str, device_type: str | None) -> bool: """Check if device family/type is known to the library.""" if device_family in ("7E", "EF"): # EDS or HobbyBoard return device_type in DEVICE_SUPPORT[device_family] @@ -144,11 +144,15 @@ class OneWireHub: return devices - def _get_device_type(self, device_path: str) -> str: + def _get_device_type(self, device_path: str) -> str | None: """Get device model.""" if TYPE_CHECKING: assert self.owproxy - device_type = self.owproxy.read(f"{device_path}type").decode() + try: + device_type = self.owproxy.read(f"{device_path}type").decode() + except protocol.ProtocolError as exc: + _LOGGER.debug("Unable to read `%stype`: %s", device_path, exc) + return None _LOGGER.debug("read `%stype`: %s", device_path, device_type) if device_type == "EDS": device_type = self.owproxy.read(f"{device_path}device_type").decode() diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 46f18842d51..3e43df4dddd 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -377,10 +377,10 @@ def get_entities( device_info = device.device_info device_sub_type = "std" device_path = device.path - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type - elif "7E" in family: + elif device_type and "7E" in family: device_sub_type = "EDS" family = device_type elif "A6" in family: diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 41276218540..94a7d41ab85 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -178,7 +178,7 @@ def get_entities(onewire_hub: OneWireHub) -> list[OneWireSwitch]: device_id = device.id device_info = device.device_info device_sub_type = "std" - if "EF" in family: + if device_type and "EF" in family: device_sub_type = "HobbyBoard" family = device_type elif "A6" in family: diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index e5f8ac575e9..a1bab9807d5 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -1,6 +1,6 @@ """Constants for 1-Wire integration.""" -from pyownet.protocol import Error as ProtocolError +from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import Platform @@ -58,6 +58,12 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 248125"}, ], }, + "16.111111111111": { + # Test case for issue #115984, where the device type cannot be read + ATTR_INJECT_READS: [ + ProtocolError(), # read device type + ], + }, "1F.111111111111": { ATTR_INJECT_READS: [ b"DS2409", # read device type diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 3123dfb6a5e..999794ec20d 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -219,6 +219,18 @@ }), ]) # --- +# name: test_binary_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_binary_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_binary_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_sensor.ambr b/tests/components/onewire/snapshots/test_sensor.ambr index aa8c914ece5..59ed167197d 100644 --- a/tests/components/onewire/snapshots/test_sensor.ambr +++ b/tests/components/onewire/snapshots/test_sensor.ambr @@ -278,6 +278,18 @@ }), ]) # --- +# name: test_sensors[16.111111111111] + list([ + ]) +# --- +# name: test_sensors[16.111111111111].1 + list([ + ]) +# --- +# name: test_sensors[16.111111111111].2 + list([ + ]) +# --- # name: test_sensors[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 2ac542d203c..8fd1e2aeef6 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -351,6 +351,18 @@ }), ]) # --- +# name: test_switches[16.111111111111] + list([ + ]) +# --- +# name: test_switches[16.111111111111].1 + list([ + ]) +# --- +# name: test_switches[16.111111111111].2 + list([ + ]) +# --- # name: test_switches[1D.111111111111] list([ DeviceRegistryEntrySnapshot({ From 0b74f02c4e9700443c5450fd1f1ea28d46dd819c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 10:48:32 +0200 Subject: [PATCH 10/96] Fix language in strict connection guard page (#116154) --- homeassistant/components/http/strict_connection_guard_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/strict_connection_guard_page.html b/homeassistant/components/http/strict_connection_guard_page.html index 86ea8e00e90..8567e500c9d 100644 --- a/homeassistant/components/http/strict_connection_guard_page.html +++ b/homeassistant/components/http/strict_connection_guard_page.html @@ -123,7 +123,7 @@

You need access

- This device is not known on + This device is not known to Home Assistant.

From 29ab68fd24f82955dd0a1bc4f66631854b94ff82 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 25 Apr 2024 11:21:19 +0200 Subject: [PATCH 11/96] Update unlocked icon for locks (#116157) --- homeassistant/components/lock/icons.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lock/icons.json b/homeassistant/components/lock/icons.json index 1bf48f2ab40..0ce2e70d372 100644 --- a/homeassistant/components/lock/icons.json +++ b/homeassistant/components/lock/icons.json @@ -5,7 +5,7 @@ "state": { "jammed": "mdi:lock-alert", "locking": "mdi:lock-clock", - "unlocked": "mdi:lock-open", + "unlocked": "mdi:lock-open-variant", "unlocking": "mdi:lock-clock" } } @@ -13,6 +13,6 @@ "services": { "lock": "mdi:lock", "open": "mdi:door-open", - "unlock": "mdi:lock-open" + "unlock": "mdi:lock-open-variant" } } From 4612f18186f5ccf4f5b3c44636515f31fc133230 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 08:38:24 +0200 Subject: [PATCH 12/96] Remove early return when validating entity registry items (#116160) --- homeassistant/helpers/entity_registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4e77df49ea6..436fc5a18de 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -636,7 +636,6 @@ def _validate_item( unique_id, report_issue, ) - return if ( disabled_by and disabled_by is not UNDEFINED From 12bce5451ee7fe32c4b560a4fe5a3a268ed04629 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 18:15:57 +0200 Subject: [PATCH 13/96] Revert orjson to 3.9.15 due to segmentation faults (#116168) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b88f2aefffa..aa29713a849 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.3 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.1 +orjson==3.9.15 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.3.0 diff --git a/pyproject.toml b/pyproject.toml index 34c7d648795..0427019a29e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "cryptography==42.0.5", "Pillow==10.3.0", "pyOpenSSL==24.1.0", - "orjson==3.10.1", + "orjson==3.9.15", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index 34ee8237921..44c60aec07a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ PyJWT==2.8.0 cryptography==42.0.5 Pillow==10.3.0 pyOpenSSL==24.1.0 -orjson==3.10.1 +orjson==3.9.15 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 5ac8488d2a0473a667f1191d87f8edea2f4e1541 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 25 Apr 2024 13:35:29 -0500 Subject: [PATCH 14/96] Update Ollama model names list (#116172) --- homeassistant/components/ollama/const.py | 145 ++++++++++++----------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index 853370066dc..e25ae1f0877 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -81,75 +81,86 @@ DEFAULT_MAX_HISTORY = 20 MAX_HISTORY_SECONDS = 60 * 60 # 1 hour MODEL_NAMES = [ # https://ollama.com/library - "gemma", - "llama2", - "mistral", - "mixtral", - "llava", - "neural-chat", - "codellama", - "dolphin-mixtral", - "qwen", - "llama2-uncensored", - "mistral-openorca", - "deepseek-coder", - "nous-hermes2", - "phi", - "orca-mini", - "dolphin-mistral", - "wizard-vicuna-uncensored", - "vicuna", - "tinydolphin", - "llama2-chinese", - "nomic-embed-text", - "openhermes", - "zephyr", - "tinyllama", - "openchat", - "wizardcoder", - "starcoder", - "phind-codellama", - "starcoder2", - "yi", - "orca2", - "falcon", - "wizard-math", - "dolphin-phi", - "starling-lm", - "nous-hermes", - "stable-code", - "medllama2", - "bakllava", - "codeup", - "wizardlm-uncensored", - "solar", - "everythinglm", - "sqlcoder", - "dolphincoder", - "nous-hermes2-mixtral", - "stable-beluga", - "yarn-mistral", - "stablelm2", - "samantha-mistral", - "meditron", - "stablelm-zephyr", - "magicoder", - "yarn-llama2", - "llama-pro", - "deepseek-llm", - "wizard-vicuna", - "codebooga", - "mistrallite", - "all-minilm", - "nexusraven", - "open-orca-platypus2", - "goliath", - "notux", - "megadolphin", "alfred", - "xwinlm", - "wizardlm", + "all-minilm", + "bakllava", + "codebooga", + "codegemma", + "codellama", + "codeqwen", + "codeup", + "command-r", + "command-r-plus", + "dbrx", + "deepseek-coder", + "deepseek-llm", + "dolphin-llama3", + "dolphin-mistral", + "dolphin-mixtral", + "dolphin-phi", + "dolphincoder", "duckdb-nsql", + "everythinglm", + "falcon", + "gemma", + "goliath", + "llama-pro", + "llama2", + "llama2-chinese", + "llama2-uncensored", + "llama3", + "llava", + "magicoder", + "meditron", + "medllama2", + "megadolphin", + "mistral", + "mistral-openorca", + "mistrallite", + "mixtral", + "mxbai-embed-large", + "neural-chat", + "nexusraven", + "nomic-embed-text", "notus", + "notux", + "nous-hermes", + "nous-hermes2", + "nous-hermes2-mixtral", + "open-orca-platypus2", + "openchat", + "openhermes", + "orca-mini", + "orca2", + "phi", + "phi3", + "phind-codellama", + "qwen", + "samantha-mistral", + "snowflake-arctic-embed", + "solar", + "sqlcoder", + "stable-beluga", + "stable-code", + "stablelm-zephyr", + "stablelm2", + "starcoder", + "starcoder2", + "starling-lm", + "tinydolphin", + "tinyllama", + "vicuna", + "wizard-math", + "wizard-vicuna", + "wizard-vicuna-uncensored", + "wizardcoder", + "wizardlm", + "wizardlm-uncensored", + "wizardlm2", + "xwinlm", + "yarn-llama2", + "yarn-mistral", + "yi", + "zephyr", ] DEFAULT_MODEL = "llama2:latest" From e0cc9198aa105ae68d5f1a2efc50c113991b00bb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 25 Apr 2024 17:32:42 +0200 Subject: [PATCH 15/96] Revert "Return specific group state if there is one" (#116176) Revert "Return specific group state if there is one (#115866)" This reverts commit 350ca48d4c10b2105e1e3513da7137498dd6ad83. --- homeassistant/components/group/entity.py | 95 ++++------------------ homeassistant/components/group/registry.py | 14 +--- tests/components/group/test_init.py | 24 +----- 3 files changed, 24 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/group/entity.py b/homeassistant/components/group/entity.py index 5ac913dde8d..a8fd9027984 100644 --- a/homeassistant/components/group/entity.py +++ b/homeassistant/components/group/entity.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Collection, Mapping import logging from typing import Any -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_ON from homeassistant.core import ( CALLBACK_TYPE, Event, @@ -131,9 +131,6 @@ class Group(Entity): _unrecorded_attributes = frozenset({ATTR_ENTITY_ID, ATTR_ORDER, ATTR_AUTO}) _attr_should_poll = False - # In case there is only one active domain we use specific ON or OFF - # values, if all ON or OFF states are equal - single_active_domain: str | None tracking: tuple[str, ...] trackable: tuple[str, ...] @@ -290,7 +287,6 @@ class Group(Entity): if not entity_ids: self.tracking = () self.trackable = () - self.single_active_domain = None return registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] @@ -298,22 +294,12 @@ class Group(Entity): tracking: list[str] = [] trackable: list[str] = [] - self.single_active_domain = None - multiple_domains: bool = False for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] tracking.append(ent_id_lower) - if domain in excluded_domains: - continue - - trackable.append(ent_id_lower) - - if not multiple_domains and self.single_active_domain is None: - self.single_active_domain = domain - if self.single_active_domain != domain: - multiple_domains = True - self.single_active_domain = None + if domain not in excluded_domains: + trackable.append(ent_id_lower) self.trackable = tuple(trackable) self.tracking = tuple(tracking) @@ -409,36 +395,10 @@ class Group(Entity): self._on_off[entity_id] = state in registry.on_off_mapping else: entity_on_state = registry.on_states_by_domain[domain] - self._on_states.update(entity_on_state) + if domain in registry.on_states_by_domain: + self._on_states.update(entity_on_state) self._on_off[entity_id] = state in entity_on_state - def _detect_specific_on_off_state(self, group_is_on: bool) -> set[str]: - """Check if a specific ON or OFF state is possible.""" - # In case the group contains entities of the same domain with the same ON - # or an OFF state (one or more domains), we want to use that specific state. - # If we have more then one ON or OFF state we default to STATE_ON or STATE_OFF. - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - active_on_states: set[str] = set() - active_off_states: set[str] = set() - for entity_id in self.trackable: - if (state := self.hass.states.get(entity_id)) is None: - continue - current_state = state.state - if ( - group_is_on - and (domain_on_states := registry.on_states_by_domain.get(state.domain)) - and current_state in domain_on_states - ): - active_on_states.add(current_state) - # If we have more than one on state, the group state - # will result in STATE_ON and we can stop checking - if len(active_on_states) > 1: - break - elif current_state in registry.off_on_mapping: - active_off_states.add(current_state) - - return active_on_states if group_is_on else active_off_states - @callback def _async_update_group_state(self, tr_state: State | None = None) -> None: """Update group state. @@ -465,48 +425,27 @@ class Group(Entity): elif tr_state.attributes.get(ATTR_ASSUMED_STATE): self._assumed_state = True - # If we do not have an on state for any domains - # we use None (which will be STATE_UNKNOWN) - if (num_on_states := len(self._on_states)) == 0: - self._state = None - return - - group_is_on = self.mode(self._on_off.values()) - + num_on_states = len(self._on_states) # If all the entity domains we are tracking # have the same on state we use this state # and its hass.data[REG_KEY].on_off_mapping to off if num_on_states == 1: - on_state = next(iter(self._on_states)) + on_state = list(self._on_states)[0] + # If we do not have an on state for any domains + # we use None (which will be STATE_UNKNOWN) + elif num_on_states == 0: + self._state = None + return # If the entity domains have more than one - # on state, we use STATE_ON/STATE_OFF, unless there is - # only one specific `on` state in use for one specific domain - elif self.single_active_domain and num_on_states: - active_on_states = self._detect_specific_on_off_state(True) - on_state = ( - list(active_on_states)[0] if len(active_on_states) == 1 else STATE_ON - ) - elif group_is_on: + # on state, we use STATE_ON/STATE_OFF + else: on_state = STATE_ON + group_is_on = self.mode(self._on_off.values()) if group_is_on: self._state = on_state - return - - registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] - if ( - active_domain := self.single_active_domain - ) and active_domain in registry.off_state_by_domain: - # If there is only one domain used, - # then we return the off state for that domain.s - self._state = registry.off_state_by_domain[active_domain] else: - active_off_states = self._detect_specific_on_off_state(False) - # If there is one off state in use then we return that specific state, - # also if there a multiple domains involved, e.g. - # person and device_tracker, with a shared state. - self._state = ( - list(active_off_states)[0] if len(active_off_states) == 1 else STATE_OFF - ) + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._state = registry.on_off_mapping[on_state] def async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: diff --git a/homeassistant/components/group/registry.py b/homeassistant/components/group/registry.py index 474448db68a..6cdb929d60c 100644 --- a/homeassistant/components/group/registry.py +++ b/homeassistant/components/group/registry.py @@ -49,12 +49,9 @@ class GroupIntegrationRegistry: def __init__(self) -> None: """Imitialize registry.""" - self.on_off_mapping: dict[str, dict[str | None, str]] = { - STATE_ON: {None: STATE_OFF} - } + self.on_off_mapping: dict[str, str] = {STATE_ON: STATE_OFF} self.off_on_mapping: dict[str, str] = {STATE_OFF: STATE_ON} self.on_states_by_domain: dict[str, set[str]] = {} - self.off_state_by_domain: dict[str, str] = {} self.exclude_domains: set[str] = set() def exclude_domain(self) -> None: @@ -63,14 +60,11 @@ class GroupIntegrationRegistry: def on_off_states(self, on_states: set, off_state: str) -> None: """Register on and off states for the current domain.""" - domain = current_domain.get() for on_state in on_states: if on_state not in self.on_off_mapping: - self.on_off_mapping[on_state] = {domain: off_state} - else: - self.on_off_mapping[on_state][domain] = off_state + self.on_off_mapping[on_state] = off_state + if len(on_states) == 1 and off_state not in self.off_on_mapping: self.off_on_mapping[off_state] = list(on_states)[0] - self.on_states_by_domain[domain] = set(on_states) - self.off_state_by_domain[domain] = off_state + self.on_states_by_domain[current_domain.get()] = set(on_states) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index b9cdfcb1590..d3f2747933e 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -9,7 +9,7 @@ from unittest.mock import patch import pytest -from homeassistant.components import group, vacuum +from homeassistant.components import group from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, @@ -659,24 +659,6 @@ async def test_is_on(hass: HomeAssistant) -> None: (STATE_ON, True), (STATE_OFF, False), ), - ( - ("vacuum", "vacuum"), - # Cleaning is the only on state - (vacuum.STATE_DOCKED, vacuum.STATE_CLEANING), - # Returning is the only on state - (vacuum.STATE_RETURNING, vacuum.STATE_PAUSED), - (vacuum.STATE_CLEANING, True), - (vacuum.STATE_RETURNING, True), - ), - ( - ("vacuum", "vacuum"), - # Multiple on states, so group state will be STATE_ON - (vacuum.STATE_RETURNING, vacuum.STATE_CLEANING), - # Only off states, so group state will be off - (vacuum.STATE_PAUSED, vacuum.STATE_IDLE), - (STATE_ON, True), - (STATE_OFF, False), - ), ], ) async def test_is_on_and_state_mixed_domains( @@ -1238,7 +1220,7 @@ async def test_group_climate_all_cool(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "cool" + assert hass.states.get("group.group_zero").state == STATE_ON async def test_group_climate_all_off(hass: HomeAssistant) -> None: @@ -1352,7 +1334,7 @@ async def test_group_vacuum_on(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert hass.states.get("group.group_zero").state == "cleaning" + assert hass.states.get("group.group_zero").state == STATE_ON async def test_device_tracker_not_home(hass: HomeAssistant) -> None: From 1defd18cf56cec25f52e96719f0eccde1929f54a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 12:56:33 -0500 Subject: [PATCH 16/96] Bump govee-ble to 0.31.2 (#116177) changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.31.0...v0.31.2 Fixes some unrelated BLE devices being detected as a GVH5106 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 64feedc44c1..98b802f8233 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -90,5 +90,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/govee_ble", "iot_class": "local_push", - "requirements": ["govee-ble==0.31.0"] + "requirements": ["govee-ble==0.31.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1e18b1833c0..46d842fb7d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ goslide-api==0.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74205a57b64..c0ddfe00a17 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -803,7 +803,7 @@ googlemaps==2.5.1 gotailwind==0.2.2 # homeassistant.components.govee_ble -govee-ble==0.31.0 +govee-ble==0.31.2 # homeassistant.components.govee_light_local govee-local-api==1.4.4 From 7cabb04bc9163f34a4db75f93e058d7f8fe00775 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 25 Apr 2024 20:43:31 +0300 Subject: [PATCH 17/96] Bump pyrisco to 0.6.1 (#116182) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 4c590b95e52..22e73a10d6d 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyrisco"], "quality_scale": "platinum", - "requirements": ["pyrisco==0.6.0"] + "requirements": ["pyrisco==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 46d842fb7d1..bb5fbd528bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0ddfe00a17..4c6f5d590e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1635,7 +1635,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.6.0 +pyrisco==0.6.1 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 8ac6593b5392d9740552d00b5542e55f72731e2f Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Thu, 25 Apr 2024 14:26:11 -0400 Subject: [PATCH 18/96] Make Roborock listener update thread safe (#116184) Co-authored-by: J. Nick Koston --- homeassistant/components/roborock/device.py | 2 +- tests/components/roborock/test_sensor.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roborock/device.py b/homeassistant/components/roborock/device.py index 69384d6e23a..6450d849859 100644 --- a/homeassistant/components/roborock/device.py +++ b/homeassistant/components/roborock/device.py @@ -137,4 +137,4 @@ class RoborockCoordinatedEntity( else: self.coordinator.roborock_device_info.props.consumable = value self.coordinator.data = self.coordinator.roborock_device_info.props - self.async_write_ha_state() + self.schedule_update_ha_state() diff --git a/tests/components/roborock/test_sensor.py b/tests/components/roborock/test_sensor.py index 23d16f643b2..88ed6e1098c 100644 --- a/tests/components/roborock/test_sensor.py +++ b/tests/components/roborock/test_sensor.py @@ -89,6 +89,7 @@ async def test_listener_update( ) ] ) + await hass.async_block_till_done() assert hass.states.get("sensor.roborock_s7_maxv_filter_time_left").state == str( FILTER_REPLACE_TIME - 743 ) From 63ef52a312ec77917647444362ea5e39b5b8c250 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 14:07:07 -0500 Subject: [PATCH 19/96] Fix smartthings doing I/O in the event loop to import platforms (#116190) --- homeassistant/components/smartthings/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 8136806cd0b..9bfa11d3293 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_loaded_integration +from homeassistant.setup import SetupPhases, async_pause_setup from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( @@ -170,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Setup device broker - broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PLATFORMS): + # DeviceBroker has a side effect of importing platform + # modules when its created. In the future this should be + # refactored to not do this. + broker = await hass.async_add_import_executor_job( + DeviceBroker, hass, entry, token, smart_app, devices, scenes + ) broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker From a9b9d7f566807f8cb170234b8b007a36bc226182 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2024 21:20:24 +0200 Subject: [PATCH 20/96] Fix flaky traccar_server tests (#116191) --- .../components/traccar_server/diagnostics.py | 4 +- .../snapshots/test_diagnostics.ambr | 190 +++++++++--------- .../traccar_server/test_diagnostics.py | 14 +- 3 files changed, 110 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index 80dc7a9c7cd..68f1e4fca8a 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -57,7 +57,7 @@ async def async_get_config_entry_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, "unit_of_measurement": entity.unit_of_measurement, "state": _entity_state(hass, entity, coordinator), @@ -92,7 +92,7 @@ async def async_get_device_diagnostics( "coordinator_data": coordinator.data, "entities": [ { - "enity_id": entity.entity_id, + "entity_id": entity.entity_id, "disabled": entity.disabled, "unit_of_measurement": entity.unit_of_measurement, "state": _entity_state(hass, entity, coordinator), diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 89a6416c303..39e67db8df7 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -73,7 +73,30 @@ 'entities': list([ dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'binary_sensor.x_wing_motion', + 'state': dict({ + 'attributes': dict({ + 'device_class': 'motion', + 'friendly_name': 'X-Wing Motion', + }), + 'state': 'off', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'binary_sensor.x_wing_status', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Status', + }), + 'state': 'on', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ 'category': 'starfighter', @@ -92,30 +115,31 @@ }), dict({ 'disabled': False, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'sensor.x_wing_address', 'state': dict({ 'attributes': dict({ - 'device_class': 'motion', - 'friendly_name': 'X-Wing Motion', + 'friendly_name': 'X-Wing Address', }), - 'state': 'off', + 'state': '**REDACTED**', }), 'unit_of_measurement': None, }), dict({ 'disabled': False, - 'enity_id': 'binary_sensor.x_wing_status', + 'entity_id': 'sensor.x_wing_altitude', 'state': dict({ 'attributes': dict({ - 'friendly_name': 'X-Wing Status', + 'friendly_name': 'X-Wing Altitude', + 'state_class': 'measurement', + 'unit_of_measurement': 'm', }), - 'state': 'on', + 'state': '546841384638', }), - 'unit_of_measurement': None, + 'unit_of_measurement': 'm', }), dict({ 'disabled': False, - 'enity_id': 'sensor.x_wing_battery', + 'entity_id': 'sensor.x_wing_battery', 'state': dict({ 'attributes': dict({ 'device_class': 'battery', @@ -129,7 +153,18 @@ }), dict({ 'disabled': False, - 'enity_id': 'sensor.x_wing_speed', + 'entity_id': 'sensor.x_wing_geofence', + 'state': dict({ + 'attributes': dict({ + 'friendly_name': 'X-Wing Geofence', + }), + 'state': 'Tatooine', + }), + 'unit_of_measurement': None, + }), + dict({ + 'disabled': False, + 'entity_id': 'sensor.x_wing_speed', 'state': dict({ 'attributes': dict({ 'device_class': 'speed', @@ -141,41 +176,6 @@ }), 'unit_of_measurement': 'kn', }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_altitude', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Altitude', - 'state_class': 'measurement', - 'unit_of_measurement': 'm', - }), - 'state': '546841384638', - }), - 'unit_of_measurement': 'm', - }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_address', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Address', - }), - 'state': '**REDACTED**', - }), - 'unit_of_measurement': None, - }), - dict({ - 'disabled': False, - 'enity_id': 'sensor.x_wing_geofence', - 'state': dict({ - 'attributes': dict({ - 'friendly_name': 'X-Wing Geofence', - }), - 'state': 'Tatooine', - }), - 'unit_of_measurement': None, - }), ]), 'subscription_status': 'disconnected', }) @@ -254,51 +254,51 @@ 'entities': list([ dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'binary_sensor.x_wing_motion', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_status', + 'entity_id': 'binary_sensor.x_wing_status', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_battery', + 'entity_id': 'device_tracker.x_wing', 'state': None, - 'unit_of_measurement': '%', + 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_speed', + 'entity_id': 'sensor.x_wing_address', 'state': None, - 'unit_of_measurement': 'kn', + 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_altitude', + 'entity_id': 'sensor.x_wing_altitude', 'state': None, 'unit_of_measurement': 'm', }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_address', + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'sensor.x_wing_geofence', + 'entity_id': 'sensor.x_wing_speed', 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'device_tracker.x_wing', - 'state': None, - 'unit_of_measurement': None, + 'unit_of_measurement': 'kn', }), ]), 'subscription_status': 'disconnected', @@ -378,49 +378,19 @@ 'entities': list([ dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_motion', + 'entity_id': 'binary_sensor.x_wing_motion', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': True, - 'enity_id': 'binary_sensor.x_wing_status', - 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_battery', - 'state': None, - 'unit_of_measurement': '%', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_speed', - 'state': None, - 'unit_of_measurement': 'kn', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_altitude', - 'state': None, - 'unit_of_measurement': 'm', - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_address', - 'state': None, - 'unit_of_measurement': None, - }), - dict({ - 'disabled': True, - 'enity_id': 'sensor.x_wing_geofence', + 'entity_id': 'binary_sensor.x_wing_status', 'state': None, 'unit_of_measurement': None, }), dict({ 'disabled': False, - 'enity_id': 'device_tracker.x_wing', + 'entity_id': 'device_tracker.x_wing', 'state': dict({ 'attributes': dict({ 'category': 'starfighter', @@ -437,6 +407,36 @@ }), 'unit_of_measurement': None, }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_address', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_altitude', + 'state': None, + 'unit_of_measurement': 'm', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_battery', + 'state': None, + 'unit_of_measurement': '%', + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_geofence', + 'state': None, + 'unit_of_measurement': None, + }), + dict({ + 'disabled': True, + 'entity_id': 'sensor.x_wing_speed', + 'state': None, + 'unit_of_measurement': 'kn', + }), ]), 'subscription_status': 'disconnected', }) diff --git a/tests/components/traccar_server/test_diagnostics.py b/tests/components/traccar_server/test_diagnostics.py index 493f0ae92d1..9019cd0ebf1 100644 --- a/tests/components/traccar_server/test_diagnostics.py +++ b/tests/components/traccar_server/test_diagnostics.py @@ -33,6 +33,10 @@ async def test_entry_diagnostics( hass_client, mock_config_entry, ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name="entry") @@ -64,13 +68,17 @@ async def test_device_diagnostics( device_id=device.id, include_disabled_entities=True, ) - # Enable all entitits to show everything in snapshots + # Enable all entities to show everything in snapshots for entity in entities: entity_registry.async_update_entity(entity.entity_id, disabled_by=None) result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name=device.name) @@ -110,5 +118,9 @@ async def test_device_diagnostics_with_disabled_entity( result = await get_diagnostics_for_device( hass, hass_client, mock_config_entry, device=device ) + # Sort the list of entities + result["entities"] = sorted( + result["entities"], key=lambda entity: entity["entity_id"] + ) assert result == snapshot(name=device.name) From 9f84c38f081ba22670aab5ba1d839ba106575ce5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 25 Apr 2024 15:37:38 -0500 Subject: [PATCH 21/96] Bump bluetooth-auto-recovery to 1.4.2 (#116192) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f6adcbed7d8..ed1e11d8ddd 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -17,7 +17,7 @@ "bleak==0.21.1", "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.19.0", - "bluetooth-auto-recovery==1.4.1", + "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", "habluetooth==2.8.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index aa29713a849..442db45e714 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 bluetooth-adapters==0.19.0 -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index bb5fbd528bf..6ab10019d78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -582,7 +582,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c6f5d590e9..bc69a55c955 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ bluemaestro-ble==0.2.3 bluetooth-adapters==0.19.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==1.4.1 +bluetooth-auto-recovery==1.4.2 # homeassistant.components.bluetooth # homeassistant.components.ld2410_ble From 1be5249269b0699a7b9009db3f73b148876b1659 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 05:00:07 +0200 Subject: [PATCH 22/96] Reduce scope of bootstrap test fixture to module (#116195) --- tests/test_bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 2e35e4ffddb..96caf5d10c8 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -44,7 +44,7 @@ async def apply_stop_hass(stop_hass: None) -> None: """Make sure all hass are stopped.""" -@pytest.fixture(scope="session", autouse=True) +@pytest.fixture(scope="module", autouse=True) def mock_http_start_stop() -> Generator[None, None, None]: """Mock HTTP start and stop.""" with ( From 8f02ed4bf3a9699b710a44ae3eee5c6dd7c150e5 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 26 Apr 2024 23:44:13 +1000 Subject: [PATCH 23/96] Breakfix to handle null value in Teslemetry (#116206) * Fixes * Remove unused test --- homeassistant/components/teslemetry/coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/teslemetry/coordinator.py b/homeassistant/components/teslemetry/coordinator.py index 75794c7cdec..be34386a508 100644 --- a/homeassistant/components/teslemetry/coordinator.py +++ b/homeassistant/components/teslemetry/coordinator.py @@ -119,7 +119,7 @@ class TeslemetryEnergyDataCoordinator(TeslemetryDataCoordinator): # Convert Wall Connectors from array to dict data["response"]["wall_connectors"] = { - wc["din"]: wc for wc in data["response"].get("wall_connectors", []) + wc["din"]: wc for wc in (data["response"].get("wall_connectors") or []) } return data["response"] From 5fb08e8b256b3ba486340bf34b5e7028df14a6e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 26 Apr 2024 12:28:40 +0200 Subject: [PATCH 24/96] Restore default timezone after electric_kiwi sensor tests (#116217) --- tests/components/electric_kiwi/conftest.py | 3 --- tests/components/electric_kiwi/test_sensor.py | 19 ++++++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/components/electric_kiwi/conftest.py b/tests/components/electric_kiwi/conftest.py index 8819b1e134d..8052ae5e129 100644 --- a/tests/components/electric_kiwi/conftest.py +++ b/tests/components/electric_kiwi/conftest.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator from time import time from unittest.mock import AsyncMock, patch -import zoneinfo from electrickiwi_api.model import AccountBalance, Hop, HopIntervals import pytest @@ -24,8 +23,6 @@ CLIENT_ID = "1234" CLIENT_SECRET = "5678" REDIRECT_URI = "https://example.com/auth/external/callback" -TZ_NAME = "Pacific/Auckland" -TIMEZONE = zoneinfo.ZoneInfo(TZ_NAME) YieldFixture = Generator[AsyncMock, None, None] ComponentSetup = Callable[[], Awaitable[bool]] diff --git a/tests/components/electric_kiwi/test_sensor.py b/tests/components/electric_kiwi/test_sensor.py index f91e4d9c58c..a247497b263 100644 --- a/tests/components/electric_kiwi/test_sensor.py +++ b/tests/components/electric_kiwi/test_sensor.py @@ -2,6 +2,7 @@ from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock +import zoneinfo from freezegun import freeze_time import pytest @@ -19,10 +20,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry import homeassistant.util.dt as dt_util -from .conftest import TIMEZONE, ComponentSetup, YieldFixture +from .conftest import ComponentSetup, YieldFixture from tests.common import MockConfigEntry +DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE +TEST_TZ_NAME = "Pacific/Auckland" +TEST_TIMEZONE = zoneinfo.ZoneInfo(TEST_TZ_NAME) + + +@pytest.fixture(autouse=True) +def restore_timezone(): + """Restore default timezone.""" + yield + + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) + @pytest.mark.parametrize( ("sensor", "sensor_state"), @@ -124,8 +137,8 @@ async def test_check_and_move_time(ek_api: AsyncMock) -> None: """Test correct time is returned depending on time of day.""" hop = await ek_api(Mock()).get_hop() - test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TIMEZONE) - dt_util.set_default_time_zone(TIMEZONE) + test_time = datetime(2023, 6, 21, 18, 0, 0, tzinfo=TEST_TIMEZONE) + dt_util.set_default_time_zone(TEST_TIMEZONE) with freeze_time(test_time): value = _check_and_move_time(hop, "4:00 PM") From 2861ac4ac9a0f21c02856c297ec46e624f20ad59 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 26 Apr 2024 11:22:04 +0200 Subject: [PATCH 25/96] Use None as default value for strict connection cloud store (#116219) --- homeassistant/components/cloud/prefs.py | 15 +++++++++------ tests/components/cloud/test_prefs.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 9fce615128b..72207513ca9 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -365,13 +365,16 @@ class CloudPreferences: @property def strict_connection(self) -> http.const.StrictConnectionMode: """Return the strict connection mode.""" - mode = self._prefs.get( - PREF_STRICT_CONNECTION, http.const.StrictConnectionMode.DISABLED - ) + mode = self._prefs.get(PREF_STRICT_CONNECTION) - if not isinstance(mode, http.const.StrictConnectionMode): + if mode is None: + # Set to default value + # We store None in the store as the default value to detect if the user has changed the + # value or not. + mode = http.const.StrictConnectionMode.DISABLED + elif not isinstance(mode, http.const.StrictConnectionMode): mode = http.const.StrictConnectionMode(mode) - return mode # type: ignore[no-any-return] + return mode async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" @@ -430,5 +433,5 @@ class CloudPreferences: PREF_REMOTE_DOMAIN: None, PREF_REMOTE_ALLOW_REMOTE_ENABLE: True, PREF_USERNAME: username, - PREF_STRICT_CONNECTION: http.const.StrictConnectionMode.DISABLED, + PREF_STRICT_CONNECTION: None, } diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 1ed2e1d524f..57715fe2bdf 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -197,3 +197,21 @@ async def test_strict_connection_convertion( await hass.async_block_till_done() assert cloud.client.prefs.strict_connection is mode + + +@pytest.mark.parametrize("storage_data", [{}, {PREF_STRICT_CONNECTION: None}]) +async def test_strict_connection_default( + hass: HomeAssistant, + cloud: MagicMock, + hass_storage: dict[str, Any], + storage_data: dict[str, Any], +) -> None: + """Test strict connection default values.""" + hass_storage[STORAGE_KEY] = { + "version": 1, + "data": storage_data, + } + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + assert cloud.client.prefs.strict_connection is StrictConnectionMode.DISABLED From e9c4185cf64bd258ce85d668b78f643a0d30918c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 26 Apr 2024 13:03:16 +0100 Subject: [PATCH 26/96] Fix state classes for ovo energy sensors (#116225) * Fix state classes for ovo energy sensors * Restore monetary values Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --------- Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/ovo_energy/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py index 5b16e8cdef5..3012a130a1a 100644 --- a/homeassistant/components/ovo_energy/sensor.py +++ b/homeassistant/components/ovo_energy/sensor.py @@ -54,7 +54,7 @@ SENSOR_TYPES_ELECTRICITY: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_ELECTRICITY_COST, translation_key=KEY_LAST_ELECTRICITY_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.electricity[-1].cost.amount if usage.electricity[-1].cost is not None else None, @@ -88,7 +88,7 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = ( key=KEY_LAST_GAS_COST, translation_key=KEY_LAST_GAS_COST, device_class=SensorDeviceClass.MONETARY, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, value=lambda usage: usage.gas[-1].cost.amount if usage.gas[-1].cost is not None else None, From 603f46184cc4b9d722b2bcf8d38e092c61174886 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 26 Apr 2024 15:40:32 +0200 Subject: [PATCH 27/96] Update frontend to 20240426.0 (#116230) --- 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 ad63bdbed84..a5446f688ba 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==20240424.1"] + "requirements": ["home-assistant-frontend==20240426.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 442db45e714..1b4223d7b33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6ab10019d78..ebef89bd0e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc69a55c955..18b9c0c31e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240424.1 +home-assistant-frontend==20240426.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 8d11a9f21aa2c8b187ae73d49437663275a3a760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:25:19 -0500 Subject: [PATCH 28/96] Move thread safety check in entity_registry sooner (#116263) * Move thread safety check in entity_registry sooner It turns out we have a lot of custom components that are writing to the entity registry using the async APIs from threads. We now catch it at the point async_fire is called. Instread we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. * coverage * Apply suggestions from code review --- homeassistant/helpers/entity_registry.py | 10 ++++-- tests/helpers/test_entity_registry.py | 44 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 436fc5a18de..589b379cf08 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -819,6 +819,7 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=unit_of_measurement, ) + self.hass.verify_event_loop_thread("async_get_or_create") _validate_item( self.hass, domain, @@ -879,7 +880,7 @@ class EntityRegistry(BaseRegistry): _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_ENTITY_REGISTRY_UPDATED, _EventEntityRegistryUpdatedData_CreateRemove( action="create", entity_id=entity_id @@ -891,6 +892,7 @@ class EntityRegistry(BaseRegistry): @callback def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" + self.hass.verify_event_loop_thread("async_remove") entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) @@ -904,7 +906,7 @@ class EntityRegistry(BaseRegistry): platform=entity.platform, unique_id=entity.unique_id, ) - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_ENTITY_REGISTRY_UPDATED, _EventEntityRegistryUpdatedData_CreateRemove( action="remove", entity_id=entity_id @@ -1085,6 +1087,8 @@ class EntityRegistry(BaseRegistry): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update_entity") + new = self.entities[entity_id] = attr.evolve(old, **new_values) self.async_schedule_save() @@ -1098,7 +1102,7 @@ class EntityRegistry(BaseRegistry): if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id - self.hass.bus.async_fire(EVENT_ENTITY_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_ENTITY_REGISTRY_UPDATED, data) return new diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 60971d98df2..bc3b2d6f705 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,7 @@ """Tests for the Entity Registry.""" from datetime import timedelta +from functools import partial from typing import Any from unittest.mock import patch @@ -1988,3 +1989,46 @@ async def test_entries_for_category(entity_registry: er.EntityRegistry) -> None: assert not er.async_entries_for_category(entity_registry, "", "id") assert not er.async_entries_for_category(entity_registry, "scope1", "unknown") assert not er.async_entries_for_category(entity_registry, "scope1", "") + + +async def test_get_or_create_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create_from a thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_get_or_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + entity_registry.async_get_or_create, "light", "hue", "1234" + ) + + +async def test_async_update_entity_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_get_or_create from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update_entity from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + entity_registry.async_update_entity, + entry.entity_id, + new_unique_id="5678", + ) + ) + + +async def test_async_remove_thread_safety( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test call async_remove from a thread.""" + entry = entity_registry.async_get_or_create("light", "hue", "1234") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id) From 85baa2508d4f82a110cc9a7d171dd3de779ebbef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:24:55 -0500 Subject: [PATCH 29/96] Move thread safety check in device_registry sooner (#116264) It turns out we have custom components that are writing to the device registry using the async APIs from threads. We now catch it at the point async_fire is called. Instead we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. --- homeassistant/helpers/device_registry.py | 6 ++- tests/helpers/test_device_registry.py | 47 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 00d0a0ba62f..0e64540f11a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -904,6 +904,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if not new_values: return old + self.hass.verify_event_loop_thread("async_update_device") new = attr.evolve(old, **new_values) self.devices[device_id] = new @@ -923,13 +924,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): else: data = {"action": "update", "device_id": new.id, "changes": old_values} - self.hass.bus.async_fire(EVENT_DEVICE_REGISTRY_UPDATED, data) + self.hass.bus.async_fire_internal(EVENT_DEVICE_REGISTRY_UPDATED, data) return new @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" + self.hass.verify_event_loop_thread("async_remove_device") device = self.devices.pop(device_id) self.deleted_devices[device_id] = DeletedDeviceEntry( config_entries=device.config_entries, @@ -941,7 +943,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): for other_device in list(self.devices.values()): if other_device.via_device_id == device_id: self.async_update_device(other_device.id, via_device_id=None) - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_DEVICE_REGISTRY_UPDATED, _EventDeviceRegistryUpdatedData_CreateRemove( action="remove", device_id=device_id diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index ee895e3fd3e..6b167f8ee49 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2,6 +2,7 @@ from collections.abc import Iterable from contextlib import AbstractContextManager, nullcontext +from functools import partial import time from typing import Any from unittest.mock import patch @@ -2473,3 +2474,49 @@ async def test_device_name_translation_placeholders_errors( ) assert expected_error in caplog.text + + +async def test_async_get_or_create_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_get_or_create raises when called from wrong thread.""" + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_update_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + device_registry.async_get_or_create, + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + ) + + +async def test_async_remove_device_thread_safety( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, +) -> None: + """Test async_remove_device raises when called from wrong thread.""" + device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers=set(), + manufacturer="manufacturer", + model="model", + ) + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_remove_device from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + device_registry.async_remove_device, device.id + ) From 46dff86d1adbbd253c21ca21f03a0a8f41a91188 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 02:26:35 -0500 Subject: [PATCH 30/96] Move thread safety check in area_registry sooner (#116265) It turns out we have custom components that are writing to the area registry using the async APIs from threads. We now catch it at the point async_fire is called. Instead we should check sooner and use async_fire_internal so we catch the unsafe operation before it can corrupt the registry. --- homeassistant/helpers/area_registry.py | 11 ++++++-- tests/helpers/test_area_registry.py | 38 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index b39fee9c185..4dba510396f 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -202,6 +202,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): picture: str | None = None, ) -> AreaEntry: """Create a new area.""" + self.hass.verify_event_loop_thread("async_create") normalized_name = normalize_name(name) if self.async_get_area_by_name(name): @@ -221,7 +222,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): assert area.id is not None self.areas[area.id] = area self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="create", area_id=area.id), ) @@ -230,6 +231,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): @callback def async_delete(self, area_id: str) -> None: """Delete area.""" + self.hass.verify_event_loop_thread("async_delete") device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) @@ -237,7 +239,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): del self.areas[area_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="remove", area_id=area_id), ) @@ -266,6 +268,10 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): name=name, picture=picture, ) + # Since updated may be the old or the new and we always fire + # an event even if nothing has changed we cannot use async_fire_internal + # here because we do not know if the thread safety check already + # happened or not in _async_update. self.hass.bus.async_fire( EVENT_AREA_REGISTRY_UPDATED, EventAreaRegistryUpdatedData(action="update", area_id=area_id), @@ -306,6 +312,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): if not new_values: return old + self.hass.verify_event_loop_thread("_async_update") new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type] self.async_schedule_save() diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 1ee8a42b6b9..22f1dc8e534 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,5 +1,6 @@ """Tests for the Area Registry.""" +from functools import partial from typing import Any import pytest @@ -491,3 +492,40 @@ async def test_entries_for_label( assert not ar.async_entries_for_label(area_registry, "unknown") assert not ar.async_entries_for_label(area_registry, "") + + +async def test_async_get_or_create_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to create in the wrong thread.""" + with pytest.raises( + RuntimeError, + match="Detected code that calls async_create from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_create, "Mock1") + + +async def test_async_update_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to update in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls _async_update from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial(area_registry.async_update, area.id, name="Mock2") + ) + + +async def test_async_delete_thread_checks( + hass: HomeAssistant, area_registry: ar.AreaRegistry +) -> None: + """We raise when trying to delete in the wrong thread.""" + area = area_registry.async_create("Mock1") + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job(area_registry.async_delete, area.id) From 3c48c4173494894fd8e7561865e94346e9fe232b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 27 Apr 2024 03:24:23 -0400 Subject: [PATCH 31/96] Bump zwave-js-server-python to 0.55.4 (#116278) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index a06de5cb8ee..83a139331bb 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -9,7 +9,7 @@ "iot_class": "local_push", "loggers": ["zwave_js_server"], "quality_scale": "platinum", - "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.3"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.55.4"], "usb": [ { "vid": "0658", diff --git a/requirements_all.txt b/requirements_all.txt index ebef89bd0e0..d78a00ca68e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2965,7 +2965,7 @@ zigpy==0.64.0 zm-py==0.5.4 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18b9c0c31e3..ed5ec38af1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2303,7 +2303,7 @@ zigpy-znp==0.12.1 zigpy==0.64.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.55.3 +zwave-js-server-python==0.55.4 # homeassistant.components.zwave_me zwave-me-ws==0.4.3 From 9819cdfec22fc99d78e7570676b977f66f023011 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Apr 2024 07:27:57 +0000 Subject: [PATCH 32/96] Bump version to 2024.5.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1abfe08b93c..a56405d810a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 0427019a29e..fc2f658a9c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b0" +version = "2024.5.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ee4f55a5a94fafda384cad595bbd88728d6f5218 Mon Sep 17 00:00:00 2001 From: Marco van 't Wout Date: Mon, 29 Apr 2024 12:02:49 +0200 Subject: [PATCH 33/96] Improve error handling for HTTP errors on Growatt Server (#110633) * Update dependency growattServer for improved error details Updating to latest version. Since version 1.3.1 it will raise requests.exceptions.HTTPError for unexpected API responses such as HTTP 405 (rate limiting/firewall) * Improve error details by raising ConfigEntryAuthFailed Previous code was returning None which the caller couldn't handle * Use a more appropiate exception type * Update homeassistant/components/growatt_server/sensor.py * Update homeassistant/components/growatt_server/sensor.py * Fix --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/growatt_server/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index d872474f1da..98ceb35ee17 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/growatt_server", "iot_class": "cloud_polling", "loggers": ["growattServer"], - "requirements": ["growattServer==1.3.0"] + "requirements": ["growattServer==1.5.0"] } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3cf1fa30c99..c41d3ac486f 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle, dt as dt_util @@ -46,8 +47,7 @@ def get_device_list(api, config): not login_response["success"] and login_response["msg"] == LOGIN_INVALID_AUTH_CODE ): - _LOGGER.error("Username, Password or URL may be incorrect!") - return + raise ConfigEntryError("Username, Password or URL may be incorrect!") user_id = login_response["user"]["id"] if plant_id == DEFAULT_PLANT_ID: plant_info = api.plant_list(user_id) diff --git a/requirements_all.txt b/requirements_all.txt index d78a00ca68e..205907c1288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ greenwavereality==0.5.1 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed5ec38af1d..5e5aecc63e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -821,7 +821,7 @@ greeneye_monitor==3.0.3 gridnet==5.0.0 # homeassistant.components.growatt_server -growattServer==1.3.0 +growattServer==1.5.0 # homeassistant.components.google_sheets gspread==5.5.0 From 6d8066afa2d767350753b3a64dbe335455bcce8f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 29 Apr 2024 10:59:36 +0200 Subject: [PATCH 34/96] Add matter during onboarding (#116163) * Add matter during onboarding * test_zeroconf_not_onboarded_running * test_zeroconf_not_onboarded_installed * test_zeroconf_not_onboarded_not_installed * test_zeroconf_discovery_not_onboarded_not_supervisor * Clean up * Add udp address * Test zeroconf udp info too * test_addon_installed_failures_zeroconf * test_addon_running_failures_zeroconf * test_addon_not_installed_failures_zeroconf * Clean up stale changes * Set unique id for discovery step * Fix tests for background flow * Fix flow running in background * Test already discovered zeroconf * Mock unload entry --- .../components/matter/config_flow.py | 27 +- homeassistant/components/matter/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 5 + tests/components/matter/test_config_flow.py | 414 +++++++++++++++++- 4 files changed, 436 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/matter/config_flow.py b/homeassistant/components/matter/config_flow.py index 7dc06807a98..b079dcd9b54 100644 --- a/homeassistant/components/matter/config_flow.py +++ b/homeassistant/components/matter/config_flow.py @@ -17,6 +17,8 @@ from homeassistant.components.hassio import ( HassioServiceInfo, is_hassio, ) +from homeassistant.components.onboarding import async_is_onboarded +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant @@ -64,6 +66,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Set up flow instance.""" + self._running_in_background = False self.ws_address: str | None = None # If we install the add-on we should uninstall it on entry remove. self.integration_created_addon = False @@ -78,7 +81,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) - if not self.install_task.done(): + if not self._running_in_background and not self.install_task.done(): return self.async_show_progress( step_id="install_addon", progress_action="install_addon", @@ -89,12 +92,16 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.install_task except AddonError as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_install_failed() return self.async_show_progress_done(next_step_id="install_failed") finally: self.install_task = None self.integration_created_addon = True + if self._running_in_background: + return await self.async_step_start_addon() return self.async_show_progress_done(next_step_id="start_addon") async def async_step_install_failed( @@ -125,7 +132,7 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): """Start Matter Server add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) - if not self.start_task.done(): + if not self._running_in_background and not self.start_task.done(): return self.async_show_progress( step_id="start_addon", progress_action="start_addon", @@ -136,10 +143,14 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): await self.start_task except (FailedConnect, AddonError, AbortFlow) as err: LOGGER.error(err) + if self._running_in_background: + return await self.async_step_start_failed() return self.async_show_progress_done(next_step_id="start_failed") finally: self.start_task = None + if self._running_in_background: + return await self.async_step_finish_addon_setup() return self.async_show_progress_done(next_step_id="finish_addon_setup") async def async_step_start_failed( @@ -223,6 +234,18 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=get_manual_schema(user_input), errors=errors ) + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + if not async_is_onboarded(self.hass) and is_hassio(self.hass): + await self._async_handle_discovery_without_unique_id() + self._running_in_background = True + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) + return await self._async_step_discovery_without_unique_id() + async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> ConfigFlowResult: diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 0e27eb36f85..b3acc0d547c 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", "requirements": ["python-matter-server==5.7.0"], - "zeroconf": ["_matter._tcp.local."] + "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 3441026994b..7b1bbff9de0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -608,6 +608,11 @@ ZEROCONF = { "domain": "matter", }, ], + "_matterc._udp.local.": [ + { + "domain": "matter", + }, + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv", diff --git a/tests/components/matter/test_config_flow.py b/tests/components/matter/test_config_flow.py index 33e94a743f7..39ae40172c1 100644 --- a/tests/components/matter/test_config_flow.py +++ b/tests/components/matter/test_config_flow.py @@ -24,6 +24,37 @@ ADDON_DISCOVERY_INFO = { "host": "host1", "port": 5581, } +ZEROCONF_INFO_TCP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matter._tcp.local.", + name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", + properties={"SII": "3300", "SAI": "1100", "T": "0"}, +) + +ZEROCONF_INFO_UDP = ZeroconfServiceInfo( + ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), + ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], + port=5540, + hostname="CDEFGHIJ12345678.local.", + type="_matterc._udp.local.", + name="ABCDEFGH123456789._matterc._udp.local.", + properties={ + "VP": "4874+77", + "DT": "21", + "DN": "Eve Door", + "SII": "3300", + "SAI": "1100", + "T": "0", + "D": "183", + "CM": "2", + "RI": "0400530980B950D59BF473CFE42BD7DDBF2D", + "PH": "36", + "PI": None, + }, +) @pytest.fixture(name="setup_entry", autouse=True) @@ -35,6 +66,15 @@ def setup_entry_fixture() -> Generator[AsyncMock, None, None]: yield mock_setup_entry +@pytest.fixture(name="unload_entry", autouse=True) +def unload_entry_fixture() -> Generator[AsyncMock, None, None]: + """Mock entry unload.""" + with patch( + "homeassistant.components.matter.async_unload_entry", return_value=True + ) as mock_unload_entry: + yield mock_unload_entry + + @pytest.fixture(name="client_connect", autouse=True) def client_connect_fixture() -> Generator[AsyncMock, None, None]: """Mock server version.""" @@ -80,6 +120,16 @@ def addon_setup_time_fixture() -> Generator[int, None, None]: yield addon_setup_time +@pytest.fixture(name="not_onboarded") +def mock_onboarded_fixture() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is not yet onboarded.""" + with patch( + "homeassistant.components.matter.config_flow.async_is_onboarded", + return_value=False, + ) as mock_onboarded: + yield mock_onboarded + + async def test_manual_create_entry( hass: HomeAssistant, client_connect: AsyncMock, @@ -179,24 +229,18 @@ async def test_manual_already_configured( assert setup_entry.call_count == 1 +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) async def test_zeroconf_discovery( hass: HomeAssistant, client_connect: AsyncMock, setup_entry: AsyncMock, + zeroconf_info: ZeroconfServiceInfo, ) -> None: """Test flow started from Zeroconf discovery.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=ZeroconfServiceInfo( - ip_address=ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6"), - ip_addresses=[ip_address("fd11:be53:8d46:0:729e:5a4f:539d:1ee6")], - port=5540, - hostname="CDEFGHIJ12345678.local.", - type="_matter._tcp.local.", - name="ABCDEFGH123456789-0000000012345678._matter._tcp.local.", - properties={"SII": "3300", "SAI": "1100", "T": "0"}, - ), + data=zeroconf_info, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "manual" @@ -221,6 +265,185 @@ async def test_zeroconf_discovery( assert setup_entry.call_count == 1 +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_zeroconf_discovery_not_onboarded_not_supervisor( + hass: HomeAssistant, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow started from Zeroconf discovery when not onboarded.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "manual" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:5580/ws", + }, + ) + await hass.async_block_till_done() + + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://localhost:5580/ws", + "integration_created_addon": False, + "use_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_already_discovered( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and already discovered.""" + result_flow_1 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + result_flow_2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + assert result_flow_2["type"] is FlowResultType.ABORT + assert result_flow_2["reason"] == "already_configured" + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result_flow_1["type"] is FlowResultType.CREATE_ENTRY + assert result_flow_1["title"] == "Matter" + assert result_flow_1["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_running( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_running: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on running.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_installed: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": False, + } + assert setup_entry.call_count == 1 + + +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_zeroconf_not_onboarded_not_installed( + hass: HomeAssistant, + supervisor: MagicMock, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + addon_not_installed: AsyncMock, + install_addon: AsyncMock, + start_addon: AsyncMock, + client_connect: AsyncMock, + setup_entry: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test flow Zeroconf discovery when not onboarded and add-on not installed.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 0 + assert addon_store_info.call_count == 2 + assert install_addon.call_args == call(hass, "core_matter_server") + assert start_addon.call_args == call(hass, "core_matter_server") + assert client_connect.call_count == 1 + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Matter" + assert result["data"] == { + "url": "ws://host1:5581/ws", + "use_addon": True, + "integration_created_addon": True, + } + assert setup_entry.call_count == 1 + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_supervisor_discovery( hass: HomeAssistant, @@ -702,6 +925,90 @@ async def test_addon_running_failures( assert result["reason"] == abort_reason +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "discovery_info_error", + "client_connect_error", + "addon_info_error", + "abort_reason", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + None, + "cannot_connect", + True, + True, + ), + ( + None, + None, + None, + None, + "addon_get_discovery_info_failed", + True, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + None, + HassioAPIError(), + "addon_info_failed", + False, + False, + ), + ], +) +async def test_addon_running_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_running: AsyncMock, + addon_info: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + discovery_info_error: Exception | None, + client_connect_error: Exception | None, + addon_info_error: Exception | None, + abort_reason: str, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test all failures when add-on is running and not onboarded.""" + get_addon_discovery_info.side_effect = discovery_info_error + client_connect.side_effect = client_connect_error + addon_info.side_effect = addon_info_error + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf_info, + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == abort_reason + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_running_already_configured( hass: HomeAssistant, @@ -854,6 +1161,71 @@ async def test_addon_installed_failures( assert result["reason"] == "addon_start_failed" +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +@pytest.mark.parametrize( + ( + "discovery_info", + "start_addon_error", + "client_connect_error", + "discovery_info_called", + "client_connect_called", + ), + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + False, + False, + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + CannotConnect(Exception("Boom")), + True, + True, + ), + ( + None, + None, + None, + True, + False, + ), + ], +) +async def test_addon_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_installed: AsyncMock, + addon_info: AsyncMock, + start_addon: AsyncMock, + get_addon_discovery_info: AsyncMock, + client_connect: AsyncMock, + start_addon_error: Exception | None, + client_connect_error: Exception | None, + discovery_info_called: bool, + client_connect_called: bool, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on start failure when add-on is installed and not onboarded.""" + start_addon.side_effect = start_addon_error + client_connect.side_effect = client_connect_error + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert addon_info.call_count == 1 + assert start_addon.call_args == call(hass, "core_matter_server") + assert get_addon_discovery_info.called is discovery_info_called + assert client_connect.called is client_connect_called + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_start_failed" + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_installed_already_configured( hass: HomeAssistant, @@ -985,6 +1357,30 @@ async def test_addon_not_installed_failures( assert result["reason"] == "addon_install_failed" +@pytest.mark.parametrize("zeroconf_info", [ZEROCONF_INFO_TCP, ZEROCONF_INFO_UDP]) +async def test_addon_not_installed_failures_zeroconf( + hass: HomeAssistant, + supervisor: MagicMock, + addon_not_installed: AsyncMock, + addon_info: AsyncMock, + install_addon: AsyncMock, + not_onboarded: MagicMock, + zeroconf_info: ZeroconfServiceInfo, +) -> None: + """Test add-on install failure.""" + install_addon.side_effect = HassioAPIError() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=zeroconf_info + ) + await hass.async_block_till_done() + + assert install_addon.call_args == call(hass, "core_matter_server") + assert addon_info.call_count == 0 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "addon_install_failed" + + @pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) async def test_addon_not_installed_already_configured( hass: HomeAssistant, From 2c46db16d4cee8863b531aa7e4feffb8b56509fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Apr 2024 07:08:29 -0500 Subject: [PATCH 35/96] Fix script in restart mode that is fired from the same trigger (#116247) --- homeassistant/helpers/script.py | 20 +++--- tests/components/automation/test_init.py | 82 +++++++++++++++++++++++- 2 files changed, 91 insertions(+), 11 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d925bf215ab..d739fbfef98 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1692,7 +1692,7 @@ class Script: script_stack = script_stack_cv.get() if ( self.script_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED) - and (script_stack := script_stack_cv.get()) is not None + and script_stack is not None and id(self) in script_stack ): script_execution_set("disallowed_recursion_detected") @@ -1706,15 +1706,19 @@ class Script: run = cls( self._hass, self, cast(dict, variables), context, self._log_exceptions ) + has_existing_runs = bool(self._runs) self._runs.append(run) - if self.script_mode == SCRIPT_MODE_RESTART: + if self.script_mode == SCRIPT_MODE_RESTART and has_existing_runs: # When script mode is SCRIPT_MODE_RESTART, first add the new run and then # stop any other runs. If we stop other runs first, self.is_running will # return false after the other script runs were stopped until our task - # resumes running. + # resumes running. Its important that we check if there are existing + # runs before sleeping as otherwise if two runs are started at the exact + # same time they will cancel each other out. self._log("Restarting") # Important: yield to the event loop to allow the script to start in case - # the script is restarting itself. + # the script is restarting itself so it ends up in the script stack and + # the recursion check above will prevent the script from running. await asyncio.sleep(0) await self.async_stop(update_state=False, spare=run) @@ -1730,9 +1734,7 @@ class Script: self._changed() raise - async def _async_stop( - self, aws: list[asyncio.Task], update_state: bool, spare: _ScriptRun | None - ) -> None: + async def _async_stop(self, aws: list[asyncio.Task], update_state: bool) -> None: await asyncio.wait(aws) if update_state: self._changed() @@ -1749,9 +1751,7 @@ class Script: ] if not aws: return - await asyncio.shield( - create_eager_task(self._async_stop(aws, update_state, spare)) - ) + await asyncio.shield(create_eager_task(self._async_stop(aws, update_state))) async def _async_get_condition(self, config): if isinstance(config, template.Template): diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 61e6d0e4660..edf0eff878b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components import automation +from homeassistant.components import automation, input_boolean, script from homeassistant.components.automation import ( ATTR_SOURCE, DOMAIN, @@ -41,6 +41,7 @@ from homeassistant.core import ( ) from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.script import ( SCRIPT_MODE_CHOICES, SCRIPT_MODE_PARALLEL, @@ -2980,3 +2981,82 @@ async def test_automation_turns_off_other_automation( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() assert len(calls) == 0 + + +async def test_two_automations_call_restart_script_same_time( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test two automations that call a restart mode script at the same.""" + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + events = [] + + @callback + def _save_event(event): + events.append(event) + + assert await async_setup_component( + hass, + input_boolean.DOMAIN, + { + input_boolean.DOMAIN: { + "test_1": None, + } + }, + ) + cancel = async_track_state_change_event(hass, "input_boolean.test_1", _save_event) + + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + "fire_toggle": { + "sequence": [ + { + "service": "input_boolean.toggle", + "target": {"entity_id": "input_boolean.test_1"}, + } + ] + }, + } + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_0", + "mode": "single", + }, + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "to": "on", + }, + "action": { + "service": "script.fire_toggle", + }, + "id": "automation_1", + "mode": "single", + }, + ] + }, + ) + + hass.states.async_set("binary_sensor.presence", "on") + await hass.async_block_till_done() + assert len(events) == 2 + cancel() From abf45a0e0c9e32aa5b5e8b34d6da970b0855338c Mon Sep 17 00:00:00 2001 From: hopkins-tk Date: Sat, 27 Apr 2024 10:02:52 +0200 Subject: [PATCH 36/96] Fix Aseko binary sensors names (#116251) * Fix Aseko binary sensors names * Fix add missing key to strings.json * Fix remove setting shorthand translation key attribute * Update homeassistant/components/aseko_pool_live/strings.json --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/aseko_pool_live/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aseko_pool_live/binary_sensor.py b/homeassistant/components/aseko_pool_live/binary_sensor.py index dbbdff38200..79953565769 100644 --- a/homeassistant/components/aseko_pool_live/binary_sensor.py +++ b/homeassistant/components/aseko_pool_live/binary_sensor.py @@ -42,6 +42,7 @@ UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = ( ), AsekoBinarySensorEntityDescription( key="has_error", + translation_key="error", value_fn=lambda unit: unit.has_error, device_class=BinarySensorDeviceClass.PROBLEM, ), @@ -78,7 +79,6 @@ class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity): """Initialize the unit binary sensor.""" super().__init__(unit, coordinator) self.entity_description = entity_description - self._attr_name = f"{self._device_name} {entity_description.name}" self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}" @property From bfcffb5cb16fa62e744f3fb3f7a9b4f2367db7bb Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 28 Apr 2024 01:42:38 +0200 Subject: [PATCH 37/96] Fix no will published when mqtt is down (#116319) --- homeassistant/components/mqtt/client.py | 3 ++- tests/components/mqtt/test_init.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 133991ade16..7f58a21a1f1 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -711,7 +711,8 @@ class MQTT: async with self._connection_lock: self._should_reconnect = False self._async_cancel_reconnect() - self._mqttc.disconnect() + # We do not gracefully disconnect to ensure + # the broker publishes the will message @callback def async_restore_tracked_subscriptions( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 9d135b89f36..cfb8ce7ac04 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -141,17 +141,17 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop( +async def test_mqtt_does_not_disconnect_on_home_assistant_stop( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, mqtt_client_mock: MqttMockPahoClient, ) -> None: - """Test if client stops on HA stop.""" + """Test if client is not disconnected on HA stop.""" await mqtt_mock_entry() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_client_mock.disconnect.call_count == 1 + assert mqtt_client_mock.disconnect.call_count == 0 async def test_mqtt_await_ack_at_disconnect( From f2a101128f862117ac7e56f84f0a32eca6e6f6c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:13:37 -0500 Subject: [PATCH 38/96] Make discovery flow tasks background tasks (#116327) --- homeassistant/config_entries.py | 1 + homeassistant/helpers/discovery_flow.py | 2 +- tests/components/gardena_bluetooth/test_config_flow.py | 2 +- tests/components/hassio/test_init.py | 2 +- tests/components/homeassistant_yellow/test_init.py | 8 ++++---- tests/components/plex/test_config_flow.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 056814bbc4d..88230a78428 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1157,6 +1157,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): cooldown=DISCOVERY_COOLDOWN, immediate=True, function=self._async_discovery, + background=True, ) async def async_wait_import_flow_initialized(self, handler: str) -> None: diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index 314777733c3..e479a47ecfd 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -30,7 +30,7 @@ def async_create_flow( if not dispatcher or dispatcher.started: if init_coro := _async_init_flow(hass, domain, context, data): - hass.async_create_task( + hass.async_create_background_task( init_coro, f"discovery flow {domain} {context}", eager_start=True ) return diff --git a/tests/components/gardena_bluetooth/test_config_flow.py b/tests/components/gardena_bluetooth/test_config_flow.py index 7707a13180f..3b4e9c242b3 100644 --- a/tests/components/gardena_bluetooth/test_config_flow.py +++ b/tests/components/gardena_bluetooth/test_config_flow.py @@ -103,7 +103,7 @@ async def test_bluetooth( # Inject the service info will trigger the flow to start inject_bluetooth_service_info(hass, WATER_TIMER_SERVICE_INFO) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) result = next(iter(hass.config_entries.flow.async_progress_by_handler(DOMAIN))) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index da49b8d9f16..572593d642b 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1098,7 +1098,7 @@ async def test_setup_hardware_integration( ) as mock_setup_entry, ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert result assert aioclient_mock.call_count == 19 diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 0631c2cb983..ec3ba4e7005 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -44,7 +44,7 @@ async def test_setup_entry( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 @@ -90,7 +90,7 @@ async def test_setup_zha(hass: HomeAssistant, addon_store_info) -> None: ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -144,7 +144,7 @@ async def test_setup_zha_multipan( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA @@ -198,7 +198,7 @@ async def test_setup_zha_multipan_other_device( ), ): assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_get_os_info.mock_calls) == 1 # Finish setting up ZHA diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 33e1b3637d8..5f2531992d4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -722,7 +722,7 @@ async def test_integration_discovery(hass: HomeAssistant) -> None: with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): await config_flow.async_discover(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() From d1e74710940eeded63d82b79995914da459ab543 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:13:51 -0500 Subject: [PATCH 39/96] Prevent setup retry from delaying shutdown (#116328) --- homeassistant/config_entries.py | 2 +- .../components/gardena_bluetooth/test_init.py | 2 +- .../specific_devices/test_ecobee3.py | 1 + .../homekit_controller/test_init.py | 6 +++-- tests/components/teslemetry/test_init.py | 2 +- tests/components/wiz/test_init.py | 4 ++-- tests/components/yeelight/test_init.py | 22 +++++++++---------- tests/components/zha/test_init.py | 3 ++- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 88230a78428..73e1d8debd6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -698,7 +698,7 @@ class ConfigEntry: # Check again when we fire in case shutdown # has started so we do not block shutdown if not hass.is_stopping: - hass.async_create_task( + hass.async_create_background_task( self._async_setup_retry(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, diff --git a/tests/components/gardena_bluetooth/test_init.py b/tests/components/gardena_bluetooth/test_init.py index 1f294c6169d..53688846c07 100644 --- a/tests/components/gardena_bluetooth/test_init.py +++ b/tests/components/gardena_bluetooth/test_init.py @@ -57,6 +57,6 @@ async def test_setup_retry( mock_client.read_char.side_effect = original_read_char async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert mock_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 3f93ca1a896..059993e3bef 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -203,6 +203,7 @@ async def test_ecobee3_setup_connection_failure( # We just advance time by 5 minutes so that the retry happens, rather # than manually invoking async_setup_entry. await time_changed(hass, 5 * 60) + await hass.async_block_till_done(wait_background_tasks=True) climate = entity_registry.async_get("climate.homew") assert climate.unique_id == "00:00:00:00:00:00_1_16" diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 59fdf555a50..9d2022f6b1c 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -160,7 +160,7 @@ async def test_offline_device_raises(hass: HomeAssistant, controller) -> None: is_connected = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF @@ -217,16 +217,18 @@ async def test_ble_device_only_checks_is_available( is_available = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert hass.states.get("light.testdevice").state == STATE_OFF is_available = False async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE is_available = True async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("light.testdevice").state == STATE_OFF diff --git a/tests/components/teslemetry/test_init.py b/tests/components/teslemetry/test_init.py index fb405e2ee03..f21a421ed6e 100644 --- a/tests/components/teslemetry/test_init.py +++ b/tests/components/teslemetry/test_init.py @@ -74,7 +74,7 @@ async def test_vehicle_first_refresh( # Wait for the retry freezer.tick(timedelta(seconds=60)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Verify we have loaded assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index 3fa369c4d9d..c3438aed1b2 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -32,9 +32,9 @@ async def test_setup_retry(hass: HomeAssistant) -> None: bulb.getMac = AsyncMock(return_value=FAKE_MAC) with _patch_discovery(), _patch_wizlight(device=bulb): - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index af442d1c8d0..0bff635fb6e 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -69,7 +69,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # The discovery should update the ip address assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -78,7 +78,7 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant) -> None: with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( @@ -362,7 +362,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant) -> None: _patch_discovery_interval(), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -380,7 +380,7 @@ async def test_async_listen_error_late_discovery( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -388,7 +388,7 @@ async def test_async_listen_error_late_discovery( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED assert config_entry.data[CONF_DETECTED_MODEL] == MODEL @@ -411,7 +411,7 @@ async def test_fail_to_fetch_initial_state( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() @@ -419,7 +419,7 @@ async def test_fail_to_fetch_initial_state( with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -502,7 +502,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.data[CONF_ID] == ID async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with ( _patch_discovery(), @@ -511,7 +511,7 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant) -> None: patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED @@ -535,7 +535,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.unique_id == ID async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with ( _patch_discovery(), @@ -544,7 +544,7 @@ async def test_async_setup_with_missing_unique_id(hass: HomeAssistant) -> None: patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 99d6a78924b..70ba88ee6e7 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -255,10 +255,11 @@ async def test_zha_retry_unique_ids( lambda hass, delay, action: async_call_later(hass, 0, action), ): await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Wait for the config entry setup to retry await asyncio.sleep(0.1) + await hass.async_block_till_done(wait_background_tasks=True) assert len(mock_connect.mock_calls) == 2 From 624eed4b83b2d5612b86ee2f9182cc17e6dfc1a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:30 -0500 Subject: [PATCH 40/96] Fix august delaying shutdown (#116329) --- homeassistant/components/august/subscriber.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index 7294f8bc90f..bec8e2f0b97 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -47,7 +47,9 @@ class AugustSubscriberMixin: @callback def _async_scheduled_refresh(self, now: datetime) -> None: """Call the refresh method.""" - self._hass.async_create_task(self._async_refresh(now), eager_start=True) + self._hass.async_create_background_task( + self._async_refresh(now), name=f"{self} schedule refresh", eager_start=True + ) @callback def _async_cancel_update_interval(self, _: Event | None = None) -> None: From 1309fc5eda6f063b1c65213b2f0ff341c4634c71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:04 -0500 Subject: [PATCH 41/96] Fix unifiprotect delaying shutdown if websocket if offline (#116331) --- homeassistant/components/unifiprotect/data.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index c0a6d65ff7a..55ddf91d3cb 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -269,7 +269,12 @@ class ProtectData: this will be a no-op. If the websocket is disconnected, this will trigger a reconnect and refresh. """ - self._hass.async_create_task(self.async_refresh(), eager_start=True) + self._entry.async_create_background_task( + self._hass, + self.async_refresh(), + name=f"{DOMAIN} {self._entry.title} refresh", + eager_start=True, + ) @callback def async_subscribe_device_id( From c3cb79e0e9b4140ecee9cf30a28e30bd40e55827 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:14:45 -0500 Subject: [PATCH 42/96] Fix wemo push updates delaying shutdown (#116333) --- homeassistant/components/wemo/wemo_device.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 148646736bc..7326e0b42f5 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -5,6 +5,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, fields from datetime import timedelta +from functools import partial import logging from typing import TYPE_CHECKING, Literal @@ -130,7 +131,14 @@ class DeviceCoordinator(DataUpdateCoordinator[None]): # pylint: disable=hass-en ) else: updated = self.wemo.subscription_update(event_type, params) - self.hass.create_task(self._async_subscription_callback(updated)) + self.hass.loop.call_soon_threadsafe( + partial( + self.hass.async_create_background_task, + self._async_subscription_callback(updated), + f"{self.name} subscription_callback", + eager_start=True, + ) + ) async def async_shutdown(self) -> None: """Unregister push subscriptions and remove from coordinators dict.""" From c4c21bc8ea19d99ba391f741c8a86a37790dbd00 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:57:31 -0500 Subject: [PATCH 43/96] Fix bluetooth adapter discovery delaying startup and shutdown (#116335) --- homeassistant/components/bluetooth/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 4768d58379a..acc38cad58b 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -152,6 +152,7 @@ async def _async_start_adapter_discovery( cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, immediate=False, function=_async_rediscover_adapters, + background=True, ) @hass_callback From 6786479a816af20c31df215cf2d510299a971e12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 08:54:34 -0500 Subject: [PATCH 44/96] Fix sonos events delaying shutdown (#116337) --- homeassistant/components/sonos/speaker.py | 22 ++++++++++++++-------- tests/components/sonos/test_switch.py | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 667e2bb405f..e2529ddfe94 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -407,8 +407,8 @@ class SonosSpeaker: @callback def async_renew_failed(self, exception: Exception) -> None: """Handle a failed subscription renewal.""" - self.hass.async_create_task( - self._async_renew_failed(exception), eager_start=True + self.hass.async_create_background_task( + self._async_renew_failed(exception), "sonos renew failed", eager_start=True ) async def _async_renew_failed(self, exception: Exception) -> None: @@ -451,16 +451,20 @@ class SonosSpeaker: """Add the soco instance associated with the event to the callback.""" if "alarm_list_version" not in event.variables: return - self.hass.async_create_task( - self.alarms.async_process_event(event, self), eager_start=True + self.hass.async_create_background_task( + self.alarms.async_process_event(event, self), + "sonos process event", + eager_start=True, ) @callback def async_dispatch_device_properties(self, event: SonosEvent) -> None: """Update device properties from an event.""" self.event_stats.process(event) - self.hass.async_create_task( - self.async_update_device_properties(event), eager_start=True + self.hass.async_create_background_task( + self.async_update_device_properties(event), + "sonos device properties", + eager_start=True, ) async def async_update_device_properties(self, event: SonosEvent) -> None: @@ -483,8 +487,10 @@ class SonosSpeaker: return if "container_update_i_ds" not in event.variables: return - self.hass.async_create_task( - self.favorites.async_process_event(event, self), eager_start=True + self.hass.async_create_background_task( + self.favorites.async_process_event(event, self), + "sonos dispatch favorites", + eager_start=True, ) @callback diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index eb31d991a3a..d6814886d55 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -157,7 +157,7 @@ async def test_alarm_create_delete( alarm_event.variables["alarm_list_version"] = two_alarms["CurrentAlarmListVersion"] sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" in entity_registry.entities @@ -169,7 +169,7 @@ async def test_alarm_create_delete( alarm_clock.ListAlarms.return_value = one_alarm sub_callback(event=alarm_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.sonos_alarm_15" not in entity_registry.entities From 66538ba34eeffd00675afaa3959a77c3188a6af4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 17:29:00 -0500 Subject: [PATCH 45/96] Add thread safety checks to async_create_task (#116339) * Add thread safety checks to async_create_task Calling async_create_task from a thread almost always results in an fast crash. Since most internals are using async_create_background_task or other task APIs, and this is the one integrations seem to get wrong the most, add a thread safety check here * Add thread safety checks to async_create_task Calling async_create_task from a thread almost always results in an fast crash. Since most internals are using async_create_background_task or other task APIs, and this is the one integrations seem to get wrong the most, add a thread safety check here * missed one * Update homeassistant/core.py * fix mocks * one more internal * more places where internal can be used * more places where internal can be used * more places where internal can be used * internal one more place since this is high volume and was already eager_start --- homeassistant/bootstrap.py | 2 +- homeassistant/config_entries.py | 4 +- homeassistant/core.py | 37 ++++++++++++++++++- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_component.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/integration_platform.py | 4 +- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/restore_state.py | 4 +- homeassistant/helpers/script.py | 4 +- homeassistant/helpers/storage.py | 2 +- homeassistant/setup.py | 2 +- tests/common.py | 8 ++-- tests/test_core.py | 18 +++++++-- 14 files changed, 70 insertions(+), 23 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index cbc808eb0fa..fc5eedffc39 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -731,7 +731,7 @@ async def async_setup_multi_components( # to wait to be imported, and the sooner we can get the base platforms # loaded the sooner we can start loading the rest of the integrations. futures = { - domain: hass.async_create_task( + domain: hass.async_create_task_internal( async_setup_component(hass, domain, config), f"setup component {domain}", eager_start=True, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 73e1d8debd6..619b2a4b48a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1087,7 +1087,7 @@ class ConfigEntry: target: target to call. """ - task = hass.async_create_task( + task = hass.async_create_task_internal( target, f"{name} {self.title} {self.domain} {self.entry_id}", eager_start ) if eager_start and task.done(): @@ -1643,7 +1643,7 @@ class ConfigEntries: # starting a new flow with the 'unignore' step. If the integration doesn't # implement async_step_unignore then this will be a no-op. if entry.source == SOURCE_IGNORE: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.hass.config_entries.flow.async_init( entry.domain, context={"source": SOURCE_UNIGNORE}, diff --git a/homeassistant/core.py b/homeassistant/core.py index a3150adc221..2b1b9756a50 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -785,7 +785,9 @@ class HomeAssistant: target: target to call. """ self.loop.call_soon_threadsafe( - functools.partial(self.async_create_task, target, name, eager_start=True) + functools.partial( + self.async_create_task_internal, target, name, eager_start=True + ) ) @callback @@ -800,6 +802,37 @@ class HomeAssistant: This method must be run in the event loop. If you are using this in your integration, use the create task methods on the config entry instead. + target: target to call. + """ + # We turned on asyncio debug in April 2024 in the dev containers + # in the hope of catching some of the issues that have been + # reported. It will take a while to get all the issues fixed in + # custom components. + # + # In 2025.5 we should guard the `verify_event_loop_thread` + # check with a check for the `hass.config.debug` flag being set as + # long term we don't want to be checking this in production + # environments since it is a performance hit. + self.verify_event_loop_thread("async_create_task") + return self.async_create_task_internal(target, name, eager_start) + + @callback + def async_create_task_internal( + self, + target: Coroutine[Any, Any, _R], + name: str | None = None, + eager_start: bool = True, + ) -> asyncio.Task[_R]: + """Create a task from within the event loop, internal use only. + + This method is intended to only be used by core internally + and should not be considered a stable API. We will make + breaking change to this function in the future and it + should not be used in integrations. + + This method must be run in the event loop. If you are using this in your + integration, use the create task methods on the config entry instead. + target: target to call. """ if eager_start: @@ -2695,7 +2728,7 @@ class ServiceRegistry: coro = self._execute_service(handler, service_call) if not blocking: - self._hass.async_create_task( + self._hass.async_create_task_internal( self._run_service_call_catch_exceptions(coro, service_call), f"service call background {service_call.domain}.{service_call.service}", eager_start=True, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a91b4c32d21..6352a56dc90 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1490,7 +1490,7 @@ class Entity( is_remove = action == "remove" self._removed_from_registry = is_remove if action == "update" or is_remove: - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_process_registry_update_or_remove(event), eager_start=True ) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f467b5683a9..eb54d83e1dd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -146,7 +146,7 @@ class EntityComponent(Generic[_EntityT]): # Look in config for Domain, Domain 2, Domain 3 etc and load them for p_type, p_config in conf_util.config_per_platform(config, self.domain): if p_type is not None: - self.hass.async_create_task( + self.hass.async_create_task_internal( self.async_setup_platform(p_type, p_config), f"EntityComponent setup platform {p_type} {self.domain}", eager_start=True, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2b9a5d436ed..f95c0a0b66a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -477,7 +477,7 @@ class EntityPlatform: self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: """Schedule adding entities for a single platform async.""" - task = self.hass.async_create_task( + task = self.hass.async_create_task_internal( self.async_add_entities(new_entities, update_before_add=update_before_add), f"EntityPlatform async_add_entities {self.domain}.{self.platform_name}", eager_start=True, diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index be525b384e0..fbd26019b64 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -85,7 +85,7 @@ def _async_integration_platform_component_loaded( # At least one of the platforms is not loaded, we need to load them # so we have to fall back to creating a task. - hass.async_create_task( + hass.async_create_task_internal( _async_process_integration_platforms_for_component( hass, integration, platforms_that_exist, integration_platforms_by_name ), @@ -206,7 +206,7 @@ async def async_process_integration_platforms( # We use hass.async_create_task instead of asyncio.create_task because # we want to make sure that startup waits for the task to complete. # - future = hass.async_create_task( + future = hass.async_create_task_internal( _async_process_integration_platforms( hass, platform_name, top_level_components.copy(), process_job ), diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 0ddf4a1e329..119142ec14a 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -659,7 +659,7 @@ class DynamicServiceIntentHandler(IntentHandler): ) await self._run_then_background( - hass.async_create_task( + hass.async_create_task_internal( hass.services.async_call( domain, service, diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 40c898fe1d2..2b3afc2f57b 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -236,7 +236,9 @@ class RestoreStateData: # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwriting the last states once Home Assistant # has started and the old states have been read. - self.hass.async_create_task(_async_dump_states(), "RestoreStateData dump") + self.hass.async_create_task_internal( + _async_dump_states(), "RestoreStateData dump" + ) # Dump states periodically cancel_interval = async_track_time_interval( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d739fbfef98..1bbe7749ff7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -734,7 +734,7 @@ class _ScriptRun: ) trace_set_result(params=params, running_script=running_script) response_data = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( self._hass.services.async_call( **params, blocking=True, @@ -1208,7 +1208,7 @@ class _ScriptRun: async def _async_run_script(self, script: Script) -> None: """Execute a script.""" result = await self._async_run_long_action( - self._hass.async_create_task( + self._hass.async_create_task_internal( script.async_run(self._variables, self._context), eager_start=True ) ) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 20054274275..315d28e06e6 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -468,7 +468,7 @@ class Store(Generic[_T]): # wrote. Reschedule the timer to the next write time. self._async_reschedule_delayed_write(self._next_write_time) return - self.hass.async_create_task( + self.hass.async_create_task_internal( self._async_callback_delayed_write(), eager_start=True ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index fab70e31d9d..7ba51b644e5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -600,7 +600,7 @@ def _async_when_setup( _LOGGER.exception("Error handling when_setup callback for %s", component) if component in hass.config.components: - hass.async_create_task( + hass.async_create_task_internal( when_setup(), f"when setup {component}", eager_start=True ) return diff --git a/tests/common.py b/tests/common.py index b5fe0f7bae1..a3af2a3103b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -234,7 +234,7 @@ async def async_test_home_assistant( orig_async_add_job = hass.async_add_job orig_async_add_executor_job = hass.async_add_executor_job - orig_async_create_task = hass.async_create_task + orig_async_create_task_internal = hass.async_create_task_internal orig_tz = dt_util.DEFAULT_TIME_ZONE def async_add_job(target, *args, eager_start: bool = False): @@ -263,18 +263,18 @@ async def async_test_home_assistant( return orig_async_add_executor_job(target, *args) - def async_create_task(coroutine, name=None, eager_start=True): + def async_create_task_internal(coroutine, name=None, eager_start=True): """Create task.""" if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): fut = asyncio.Future() fut.set_result(None) return fut - return orig_async_create_task(coroutine, name, eager_start) + return orig_async_create_task_internal(coroutine, name, eager_start) hass.async_add_job = async_add_job hass.async_add_executor_job = async_add_executor_job - hass.async_create_task = async_create_task + hass.async_create_task_internal = async_create_task_internal hass.data[loader.DATA_CUSTOM_COMPONENTS] = {} diff --git a/tests/test_core.py b/tests/test_core.py index a553d5bbbed..66b5be718b1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -329,7 +329,7 @@ async def test_async_create_task_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job(), eager_start=False) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=False) assert len(hass.loop.call_soon.mock_calls) == 0 assert len(hass.loop.create_task.mock_calls) == 1 assert len(hass.add_job.mock_calls) == 0 @@ -342,7 +342,7 @@ async def test_async_create_task_eager_start_schedule_coroutine() -> None: async def job(): pass - ha.HomeAssistant.async_create_task(hass, job(), eager_start=True) + ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True) # Should create the task directly since 3.12 supports eager_start assert len(hass.loop.create_task.mock_calls) == 0 assert len(hass.add_job.mock_calls) == 0 @@ -355,7 +355,7 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None: async def job(): pass - task = ha.HomeAssistant.async_create_task( + task = ha.HomeAssistant.async_create_task_internal( hass, job(), "named task", eager_start=False ) assert len(hass.loop.call_soon.mock_calls) == 0 @@ -3480,3 +3480,15 @@ async def test_async_remove_thread_safety(hass: HomeAssistant) -> None: await hass.async_add_executor_job( hass.services.async_remove, "test_domain", "test_service" ) + + +async def test_async_create_task_thread_safety(hass: HomeAssistant) -> None: + """Test async_create_task thread safety.""" + + async def _any_coro(): + pass + + with pytest.raises( + RuntimeError, match="Detected code that calls async_create_task from a thread." + ): + await hass.async_add_executor_job(hass.async_create_task, _any_coro) From c533ca50b1d7cf5ceac79e4d5c0e013b10a9b77b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 10:36:03 -0500 Subject: [PATCH 46/96] Fix homeassistant_alerts delaying shutdown (#116340) --- homeassistant/components/homeassistant_alerts/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index ef5e330699a..5b5e758fba4 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -87,7 +87,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not coordinator.last_update_success: return - hass.async_create_task(async_update_alerts(), eager_start=True) + hass.async_create_background_task( + async_update_alerts(), "homeassistant_alerts update", eager_start=True + ) coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) From 5ca91190f2c8e1aa3421c9e046659455c8157098 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 28 Apr 2024 17:34:27 +0200 Subject: [PATCH 47/96] Fix Netatmo indoor sensor (#116342) * Debug netatmo indoor sensor * Debug netatmo indoor sensor * Fix --- homeassistant/components/netatmo/sensor.py | 5 ++++- .../components/netatmo/snapshots/test_sensor.ambr | 14 +++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index fd40bbf88b6..7d99ef9d32c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -529,7 +529,10 @@ class NetatmoWeatherSensor(NetatmoWeatherModuleEntity, SensorEntity): @property def available(self) -> bool: """Return True if entity is available.""" - return self.device.reachable or False + return ( + self.device.reachable + or getattr(self.device, self.entity_description.netatmo_name) is not None + ) @callback def async_update_callback(self) -> None: diff --git a/tests/components/netatmo/snapshots/test_sensor.ambr b/tests/components/netatmo/snapshots/test_sensor.ambr index 0684956adb8..6ab1e4b1e1a 100644 --- a/tests/components/netatmo/snapshots/test_sensor.ambr +++ b/tests/components/netatmo/snapshots/test_sensor.ambr @@ -901,13 +901,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Reachability', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.bedroom_reachability', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'False', }) # --- # name: test_entity[sensor.bedroom_temperature-entry] @@ -1050,13 +1052,15 @@ 'attributes': ReadOnlyDict({ 'attribution': 'Data provided by Netatmo', 'friendly_name': 'Bedroom Wi-Fi', + 'latitude': 13.377726, + 'longitude': 52.516263, }), 'context': , 'entity_id': 'sensor.bedroom_wi_fi', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'High', }) # --- # name: test_entity[sensor.bureau_modulate_battery-entry] @@ -6692,7 +6696,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': '27', }) # --- # name: test_entity[sensor.villa_outdoor_humidity-entry] @@ -6791,7 +6795,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'High', }) # --- # name: test_entity[sensor.villa_outdoor_reachability-entry] @@ -6838,7 +6842,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unavailable', + 'state': 'False', }) # --- # name: test_entity[sensor.villa_outdoor_temperature-entry] From 0cec3781267eb510dce27568e592be58a6fd3ef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 09:21:32 -0500 Subject: [PATCH 48/96] Fix some flapping sonos tests (#116343) --- tests/components/sonos/test_repairs.py | 1 + tests/components/sonos/test_switch.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index 49b87b272d6..2fa951c6a79 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -47,3 +47,4 @@ async def test_subscription_repair_issues( sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d6814886d55..11ce1aa5ddb 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -122,6 +122,7 @@ async def test_switch_attributes( # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 88015986addd8ba5f0c8d5d92aa4d74b270249e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 10:57:08 -0500 Subject: [PATCH 49/96] Fix bond update delaying shutdown when push updated are not available (#116344) If push updates are not available, bond could delay shutdown. The update task should have been marked as a background task --- homeassistant/components/bond/entity.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f547707d5f1..4495e76859d 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -128,7 +128,9 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return - self.hass.async_create_task(self._async_update(), eager_start=True) + self.hass.async_create_background_task( + self._async_update(), f"{DOMAIN} {self.name} update", eager_start=True + ) async def _async_update(self) -> None: """Fetch via the API.""" From 9445b84ab5da7c63f8c204c9352d5d1b01fb99ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 11:19:38 -0500 Subject: [PATCH 50/96] Fix shelly delaying shutdown (#116346) --- .../components/shelly/coordinator.py | 36 +++++++++++++++---- tests/components/shelly/test_binary_sensor.py | 10 +++--- tests/components/shelly/test_climate.py | 18 +++++----- tests/components/shelly/test_coordinator.py | 16 ++++----- tests/components/shelly/test_number.py | 10 +++--- tests/components/shelly/test_sensor.py | 18 +++++----- tests/components/shelly/test_update.py | 6 ++-- 7 files changed, 69 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index bd6686198ed..d3d7b86de11 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -361,7 +361,12 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): ) -> None: """Handle device update.""" if update_type is BlockUpdateType.ONLINE: - self.hass.async_create_task(self._async_device_connect(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_device_connect(), + "block device online", + eager_start=True, + ) elif update_type is BlockUpdateType.COAP_PERIODIC: self._push_update_failures = 0 ir.async_delete_issue( @@ -654,12 +659,24 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): ) -> None: """Handle device update.""" if update_type is RpcUpdateType.ONLINE: - self.hass.async_create_task(self._async_device_connect(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_device_connect(), + "rpc device online", + eager_start=True, + ) elif update_type is RpcUpdateType.INITIALIZED: - self.hass.async_create_task(self._async_connected(), eager_start=True) + self.entry.async_create_background_task( + self.hass, self._async_connected(), "rpc device init", eager_start=True + ) self.async_set_updated_data(None) elif update_type is RpcUpdateType.DISCONNECTED: - self.hass.async_create_task(self._async_disconnected(), eager_start=True) + self.entry.async_create_background_task( + self.hass, + self._async_disconnected(), + "rpc device disconnected", + eager_start=True, + ) elif update_type is RpcUpdateType.STATUS: self.async_set_updated_data(None) if self.sleep_period: @@ -673,7 +690,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected - self.hass.async_create_task(self._async_connected(), eager_start=True) + self.entry.async_create_task( + self.hass, self._async_connected(), eager_start=True + ) async def shutdown(self) -> None: """Shutdown the coordinator.""" @@ -756,4 +775,9 @@ async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) ): - hass.async_create_task(coordinator.async_request_refresh(), eager_start=True) + entry.async_create_background_task( + hass, + coordinator.async_request_refresh(), + "reconnect soon", + eager_start=True, + ) diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index 624eb82f060..524bc1e8ffc 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -145,7 +145,7 @@ async def test_block_sleeping_binary_sensor( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -181,7 +181,7 @@ async def test_block_restored_sleeping_binary_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -207,7 +207,7 @@ async def test_block_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -275,7 +275,7 @@ async def test_rpc_sleeping_binary_sensor( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF @@ -346,7 +346,7 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 9946dd7640d..a70cdef3fb1 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -70,7 +70,7 @@ async def test_climate_hvac_mode( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Test initial hvac mode - off state = hass.states.get(ENTITY_ID) @@ -131,7 +131,7 @@ async def test_climate_set_temperature( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -198,7 +198,7 @@ async def test_climate_set_preset_mode( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE @@ -284,7 +284,7 @@ async def test_block_restored_climate( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 @@ -355,7 +355,7 @@ async def test_block_restored_climate_us_customery( monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == HVACMode.OFF assert hass.states.get(entity_id).attributes.get("temperature") == 39 @@ -457,7 +457,7 @@ async def test_block_set_mode_connection_error( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -482,7 +482,7 @@ async def test_block_set_mode_auth_error( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED @@ -540,7 +540,7 @@ async def test_block_restored_climate_auth_error( return_value={}, side_effect=InvalidAuthError ) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED @@ -567,7 +567,7 @@ async def test_device_not_calibrated( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_status = MOCK_STATUS_COAP.copy() mock_status["calibrated"] = False diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 9f251d1e008..1e581e156c5 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -224,7 +224,7 @@ async def test_block_sleeping_device_firmware_unsupported( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED assert ( @@ -299,7 +299,7 @@ async def test_block_sleeping_device_no_periodic_updates( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.1" @@ -542,7 +542,7 @@ async def test_rpc_update_entry_sleep_period( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 600 @@ -550,7 +550,7 @@ async def test_rpc_update_entry_sleep_period( monkeypatch.setitem(mock_rpc_device.status["sys"], "wakeup_period", 3600) freezer.tick(timedelta(seconds=600 * SLEEP_PERIOD_MULTIPLIER)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.data["sleep_period"] == 3600 @@ -575,14 +575,14 @@ async def test_rpc_sleeping_device_no_periodic_updates( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) == "22.9" # Move time to generate polling freezer.tick(timedelta(seconds=SLEEP_PERIOD_MULTIPLIER * 1000)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert get_entity_state(hass, entity_id) is STATE_UNAVAILABLE @@ -599,7 +599,7 @@ async def test_rpc_sleeping_device_firmware_unsupported( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED assert ( @@ -765,7 +765,7 @@ async def test_rpc_update_entry_fw_ver( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.unique_id device = dev_reg.async_get_device( diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 99ad5709d29..0b9fee9e47f 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -44,7 +44,7 @@ async def test_block_number_update( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -99,7 +99,7 @@ async def test_block_restored_number( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -136,7 +136,7 @@ async def test_block_restored_number_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "50" @@ -156,7 +156,7 @@ async def test_block_number_set_value( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_block_device.reset_mock() await hass.services.async_call( @@ -217,7 +217,7 @@ async def test_block_set_value_auth_error( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 6151cac10ab..ceaa9b66b8d 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -165,7 +165,7 @@ async def test_block_sleeping_sensor( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -207,7 +207,7 @@ async def test_block_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -233,7 +233,7 @@ async def test_block_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" @@ -306,7 +306,7 @@ async def test_block_not_matched_restored_sleeping_sensor( ) monkeypatch.setattr(mock_block_device, "initialized", True) mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "20.4" @@ -464,7 +464,7 @@ async def test_rpc_sleeping_sensor( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.9" @@ -503,7 +503,7 @@ async def test_rpc_restored_sleeping_sensor( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -539,7 +539,7 @@ async def test_rpc_restored_sleeping_sensor_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -607,7 +607,7 @@ async def test_rpc_sleeping_update_entity_service( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == "22.9" @@ -657,7 +657,7 @@ async def test_block_sleeping_update_entity_service( # Make device online mock_block_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == "22.1" diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 93b0f55c415..0f26fd14d12 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -352,7 +352,7 @@ async def test_rpc_sleeping_update( # Make device online mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -413,7 +413,7 @@ async def test_rpc_restored_sleeping_update( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() @@ -462,7 +462,7 @@ async def test_rpc_restored_sleeping_update_no_last_state( # Make device online monkeypatch.setattr(mock_rpc_device, "initialized", True) mock_rpc_device.mock_online() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Mock update mock_rpc_device.mock_update() From 087b6533cddd9ebd9ac8af141d3acb2d4234b2d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 28 Apr 2024 12:54:53 -0500 Subject: [PATCH 51/96] Fix another case of homeassistant_alerts delaying shutdown (#116352) --- homeassistant/components/homeassistant_alerts/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 5b5e758fba4..b33bfe5ed1e 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -101,6 +101,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: cooldown=COMPONENT_LOADED_COOLDOWN, immediate=False, function=coordinator.async_refresh, + background=True, ) @callback From a61650e38f94758f6d90a8e5cc4692e3f6827e5c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 29 Apr 2024 16:03:57 +0300 Subject: [PATCH 52/96] Prevent Shelly raising in a task (#116355) Co-authored-by: J. Nick Koston --- .../components/shelly/coordinator.py | 24 +++-- tests/components/shelly/test_coordinator.py | 96 ++++++++++++++++++- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index d3d7b86de11..e321f393ba3 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -154,24 +154,27 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): ) self.device_id = device_entry.id - async def _async_device_connect(self) -> None: - """Connect to a Shelly Block device.""" + async def _async_device_connect_task(self) -> bool: + """Connect to a Shelly device task.""" LOGGER.debug("Connecting to Shelly Device - %s", self.name) try: await self.device.initialize() update_device_fw_info(self.hass, self.device, self.entry) except DeviceConnectionError as err: - raise UpdateFailed(f"Device disconnected: {repr(err)}") from err + LOGGER.debug( + "Error connecting to Shelly device %s, error: %r", self.name, err + ) + return False except InvalidAuthError: self.entry.async_start_reauth(self.hass) - return + return False if not self.device.firmware_supported: async_create_issue_unsupported_firmware(self.hass, self.entry) - return + return False if not self._pending_platforms: - return + return True LOGGER.debug("Device %s is online, resuming setup", self.entry.title) platforms = self._pending_platforms @@ -193,6 +196,8 @@ class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): # Resume platform setup await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) + return True + async def _async_reload_entry(self) -> None: """Reload entry.""" self._debounced_reload.async_cancel() @@ -363,7 +368,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): if update_type is BlockUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "block device online", eager_start=True, ) @@ -591,7 +596,8 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if self.device.connected: return - await self._async_device_connect() + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self) -> None: """Handle device disconnected.""" @@ -661,7 +667,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): if update_type is RpcUpdateType.ONLINE: self.entry.async_create_background_task( self.hass, - self._async_device_connect(), + self._async_device_connect_task(), "rpc device online", eager_start=True, ) diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 1e581e156c5..1dc45a98c44 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -24,10 +24,11 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, + DeviceRegistry, async_entries_for_config_entry, async_get as async_get_dev_reg, format_mac, @@ -40,10 +41,11 @@ from . import ( inject_rpc_device_event, mock_polling_rpc_update, mock_rest_update, + register_device, register_entity, ) -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache RELAY_BLOCK_ID = 0 LIGHT_BLOCK_ID = 2 @@ -806,3 +808,93 @@ async def test_rpc_runs_connected_events_when_initialized( # BLE script list is called during connected events assert call.script_list() in mock_rpc_device.mock_calls + + +async def test_block_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_block_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test block sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 1, sleep_period=sleep_period, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_motion", "sensor_0-motion", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_block_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_block_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_block_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE + + +async def test_rpc_sleeping_device_connection_error( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_rpc_device: Mock, + device_reg: DeviceRegistry, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test RPC sleeping device connection error during initialize.""" + sleep_period = 1000 + entry = await init_integration(hass, 2, sleep_period=1000, skip_setup=True) + register_device(device_reg, entry) + entity_id = register_entity( + hass, BINARY_SENSOR_DOMAIN, "test_name_cloud", "cloud-cloud", entry + ) + mock_restore_cache(hass, [State(entity_id, STATE_ON)]) + monkeypatch.setattr(mock_rpc_device, "initialized", False) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert get_entity_state(hass, entity_id) == STATE_ON + + # Make device online event with connection error + monkeypatch.setattr( + mock_rpc_device, + "initialize", + AsyncMock( + side_effect=DeviceConnectionError, + ), + ) + mock_rpc_device.mock_online() + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Error connecting to Shelly device" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_ON + + # Move time to generate sleep period update + freezer.tick(timedelta(seconds=sleep_period * SLEEP_PERIOD_MULTIPLIER)) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert "Sleeping device did not update" in caplog.text + assert get_entity_state(hass, entity_id) == STATE_UNAVAILABLE From 6fe20be095db62fb49c7b0f0cad0cb81c7820381 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Apr 2024 09:07:48 -0500 Subject: [PATCH 53/96] Fix usb scan delaying shutdown (#116390) If the integration page is accessed right before shutdown it can trigger the usb scan debouncer which was not marked as background so shutdown would wait for the scan to finish --- homeassistant/components/usb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 959a8f5894c..46950ba5b91 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -394,6 +394,7 @@ class USBDiscovery: cooldown=REQUEST_SCAN_COOLDOWN, immediate=True, function=self._async_scan, + background=True, ) await self._request_debouncer.async_call() From 0a9ac6b7a90c7a102c313e5cbf59d2d07d6b9ede Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Apr 2024 14:09:46 +0000 Subject: [PATCH 54/96] Bump version to 2024.5.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a56405d810a..07f4058ea19 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index fc2f658a9c0..68b38bb516b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b1" +version = "2024.5.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ac45d20e1f00b7b49123e20ccb3b98cb5cb5f6d7 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:08:36 +0200 Subject: [PATCH 55/96] Bump fyta_cli to 0.4.1 (#115918) * bump fyta_cli to 0.4.0 * Update PLANT_STATUS and add PLANT_MEASUREMENT_STATUS * bump fyta_cli to v0.4.0 * minor adjustments of states to API documentation --- homeassistant/components/fyta/manifest.json | 2 +- homeassistant/components/fyta/sensor.py | 28 +++++++---- homeassistant/components/fyta/strings.json | 53 +++++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 49 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index 55255777994..020ab330152 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fyta", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["fyta_cli==0.3.5"] + "requirements": ["fyta_cli==0.4.1"] } diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 2b9e8e3de07..c3e90cef28e 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Final -from fyta_cli.fyta_connector import PLANT_STATUS +from fyta_cli.fyta_connector import PLANT_MEASUREMENT_STATUS, PLANT_STATUS from homeassistant.components.sensor import ( SensorDeviceClass, @@ -34,7 +34,15 @@ class FytaSensorEntityDescription(SensorEntityDescription): ) -PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"] +PLANT_STATUS_LIST: list[str] = ["deleted", "doing_great", "need_attention", "no_sensor"] +PLANT_MEASUREMENT_STATUS_LIST: list[str] = [ + "no_data", + "too_low", + "low", + "perfect", + "high", + "too_high", +] SENSORS: Final[list[FytaSensorEntityDescription]] = [ FytaSensorEntityDescription( @@ -52,29 +60,29 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, - options=PLANT_STATUS_LIST, - value_fn=PLANT_STATUS.get, + options=PLANT_MEASUREMENT_STATUS_LIST, + value_fn=PLANT_MEASUREMENT_STATUS.get, ), FytaSensorEntityDescription( key="temperature", diff --git a/homeassistant/components/fyta/strings.json b/homeassistant/components/fyta/strings.json index 3df851489bc..bacd24555b0 100644 --- a/homeassistant/components/fyta/strings.json +++ b/homeassistant/components/fyta/strings.json @@ -36,6 +36,16 @@ "plant_status": { "name": "Plant state", "state": { + "deleted": "Deleted", + "doing_great": "Doing great", + "need_attention": "Needs attention", + "no_sensor": "No sensor" + } + }, + "temperature_status": { + "name": "Temperature state", + "state": { + "no_data": "No data", "too_low": "Too low", "low": "Low", "perfect": "Perfect", @@ -43,44 +53,37 @@ "too_high": "Too high" } }, - "temperature_status": { - "name": "Temperature state", - "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" - } - }, "light_status": { "name": "Light state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "moisture_status": { "name": "Moisture state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "salinity_status": { "name": "Salinity state", "state": { - "too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]", - "low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]", - "perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]", - "high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]", - "too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]" + "no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]", + "too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]", + "low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]", + "perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]", + "high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]", + "too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]" } }, "light": { diff --git a/requirements_all.txt b/requirements_all.txt index 205907c1288..6e41f3b743e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -900,7 +900,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e5aecc63e0..a2625b50e87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -735,7 +735,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.5 +fyta_cli==0.4.1 # homeassistant.components.google_translate gTTS==2.2.4 From 7ee79002b392c28abd07d6234163fa139614ed5f Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:09:07 +0200 Subject: [PATCH 56/96] Store access token in entry for Fyta (#116260) * save access_token and expiration date in ConfigEntry * add MINOR_VERSION and async_migrate_entry * shorten reading of expiration from config entry * add additional consts and test for config entry migration * Update homeassistant/components/fyta/coordinator.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/__init__.py Co-authored-by: Joost Lekkerkerker * omit check for datetime data type * Update homeassistant/components/fyta/__init__.py Co-authored-by: Joost Lekkerkerker * Update homeassistant/components/fyta/coordinator.py Co-authored-by: Joost Lekkerkerker --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/fyta/__init__.py | 52 ++++++++++++++++++-- homeassistant/components/fyta/config_flow.py | 17 +++++-- homeassistant/components/fyta/const.py | 1 + homeassistant/components/fyta/coordinator.py | 25 ++++++++-- tests/components/fyta/conftest.py | 26 +++++++++- tests/components/fyta/test_config_flow.py | 36 ++++++++++---- tests/components/fyta/test_init.py | 42 ++++++++++++++++ 7 files changed, 179 insertions(+), 20 deletions(-) create mode 100644 tests/components/fyta/test_init.py diff --git a/homeassistant/components/fyta/__init__.py b/homeassistant/components/fyta/__init__.py index febd5b94469..205dd97a42f 100644 --- a/homeassistant/components/fyta/__init__.py +++ b/homeassistant/components/fyta/__init__.py @@ -2,15 +2,23 @@ from __future__ import annotations +from datetime import datetime import logging +from typing import Any +from zoneinfo import ZoneInfo from fyta_cli.fyta_connector import FytaConnector from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN from .coordinator import FytaCoordinator _LOGGER = logging.getLogger(__name__) @@ -22,11 +30,16 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Fyta integration.""" + tz: str = hass.config.time_zone username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] + access_token: str = entry.data[CONF_ACCESS_TOKEN] + expiration: datetime = datetime.fromisoformat( + entry.data[CONF_EXPIRATION] + ).astimezone(ZoneInfo(tz)) - fyta = FytaConnector(username, password) + fyta = FytaConnector(username, password, access_token, expiration, tz) coordinator = FytaCoordinator(hass, fyta) @@ -47,3 +60,36 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + + if config_entry.version == 1: + new = {**config_entry.data} + if config_entry.minor_version < 2: + fyta = FytaConnector( + config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + ) + credentials: dict[str, Any] = await fyta.login() + await fyta.client.close() + + new[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + hass.config_entries.async_update_entry( + config_entry, data=new, minor_version=2, version=1 + ) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True diff --git a/homeassistant/components/fyta/config_flow.py b/homeassistant/components/fyta/config_flow.py index e11c024ec1f..3d83c099ac3 100644 --- a/homeassistant/components/fyta/config_flow.py +++ b/homeassistant/components/fyta/config_flow.py @@ -17,7 +17,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import DOMAIN +from .const import CONF_EXPIRATION, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,14 +31,19 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Fyta.""" VERSION = 1 - _entry: ConfigEntry | None = None + MINOR_VERSION = 2 + + def __init__(self) -> None: + """Initialize FytaConfigFlow.""" + self.credentials: dict[str, Any] = {} + self._entry: ConfigEntry | None = None async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: """Reusable Auth Helper.""" fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) try: - await fyta.login() + self.credentials = await fyta.login() except FytaConnectionError: return {"base": "cannot_connect"} except FytaAuthentificationError: @@ -51,6 +56,10 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): finally: await fyta.client.close() + self.credentials[CONF_EXPIRATION] = self.credentials[ + CONF_EXPIRATION + ].isoformat() + return {} async def async_step_user( @@ -62,6 +71,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) if not (errors := await self.async_auth(user_input)): + user_input |= self.credentials return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) @@ -85,6 +95,7 @@ class FytaConfigFlow(ConfigFlow, domain=DOMAIN): assert self._entry is not None if user_input and not (errors := await self.async_auth(user_input)): + user_input |= self.credentials return self.async_update_reload_and_abort( self._entry, data={**self._entry.data, **user_input} ) diff --git a/homeassistant/components/fyta/const.py b/homeassistant/components/fyta/const.py index f99735dc6fa..bf4636a713a 100644 --- a/homeassistant/components/fyta/const.py +++ b/homeassistant/components/fyta/const.py @@ -1,3 +1,4 @@ """Const for fyta integration.""" DOMAIN = "fyta" +CONF_EXPIRATION = "expiration" diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index 65bd0cb532c..021bddf2cf8 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -12,10 +12,13 @@ from fyta_cli.fyta_exceptions import ( ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from .const import CONF_EXPIRATION + _LOGGER = logging.getLogger(__name__) @@ -39,17 +42,33 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]): ) -> dict[int, dict[str, Any]]: """Fetch data from API endpoint.""" - if self.fyta.expiration is None or self.fyta.expiration < datetime.now(): + if ( + self.fyta.expiration is None + or self.fyta.expiration.timestamp() < datetime.now().timestamp() + ): await self.renew_authentication() return await self.fyta.update_all_plants() - async def renew_authentication(self) -> None: + async def renew_authentication(self) -> bool: """Renew access token for FYTA API.""" + credentials: dict[str, Any] = {} try: - await self.fyta.login() + credentials = await self.fyta.login() except FytaConnectionError as ex: raise ConfigEntryNotReady from ex except (FytaAuthentificationError, FytaPasswordError) as ex: raise ConfigEntryAuthFailed from ex + + new_config_entry = {**self.config_entry.data} + new_config_entry[CONF_ACCESS_TOKEN] = credentials[CONF_ACCESS_TOKEN] + new_config_entry[CONF_EXPIRATION] = credentials[CONF_EXPIRATION].isoformat() + + self.hass.config_entries.async_update_entry( + self.config_entry, data=new_config_entry + ) + + _LOGGER.debug("Credentials successfully updated") + + return True diff --git a/tests/components/fyta/conftest.py b/tests/components/fyta/conftest.py index efebf9827b9..9250c26926a 100644 --- a/tests/components/fyta/conftest.py +++ b/tests/components/fyta/conftest.py @@ -5,6 +5,11 @@ from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.fyta.const import CONF_EXPIRATION +from homeassistant.const import CONF_ACCESS_TOKEN + +from .test_config_flow import ACCESS_TOKEN, EXPIRATION + @pytest.fixture def mock_fyta(): @@ -15,7 +20,26 @@ def mock_fyta(): "homeassistant.components.fyta.config_flow.FytaConnector", return_value=mock_fyta_api, ) as mock_fyta_api: - mock_fyta_api.return_value.login.return_value = {} + mock_fyta_api.return_value.login.return_value = { + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, + } + yield mock_fyta_api + + +@pytest.fixture +def mock_fyta_init(): + """Build a fixture for the Fyta API that connects successfully and returns one device.""" + + mock_fyta_api = AsyncMock() + with patch( + "homeassistant.components.fyta.FytaConnector", + return_value=mock_fyta_api, + ) as mock_fyta_api: + mock_fyta_api.return_value.login.return_value = { + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: EXPIRATION, + } yield mock_fyta_api diff --git a/tests/components/fyta/test_config_flow.py b/tests/components/fyta/test_config_flow.py index 6aad6295819..69478d04ca0 100644 --- a/tests/components/fyta/test_config_flow.py +++ b/tests/components/fyta/test_config_flow.py @@ -1,5 +1,6 @@ """Test the fyta config flow.""" +from datetime import UTC, datetime from unittest.mock import AsyncMock from fyta_cli.fyta_exceptions import ( @@ -10,8 +11,8 @@ from fyta_cli.fyta_exceptions import ( import pytest from homeassistant import config_entries -from homeassistant.components.fyta.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,10 +20,12 @@ from tests.common import MockConfigEntry USERNAME = "fyta_user" PASSWORD = "fyta_pass" +ACCESS_TOKEN = "123xyz" +EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC) async def test_user_flow( - hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry + hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry: AsyncMock ) -> None: """Test we get the form.""" @@ -39,7 +42,12 @@ async def test_user_flow( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME - assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + assert result2["data"] == { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-12-31T10:00:00+00:00", + } assert len(mock_setup_entry.mock_calls) == 1 @@ -57,7 +65,7 @@ async def test_form_exceptions( exception: Exception, error: dict[str, str], mock_fyta: AsyncMock, - mock_setup_entry, + mock_setup_entry: AsyncMock, ) -> None: """Test we can handle Form exceptions.""" @@ -89,6 +97,8 @@ async def test_form_exceptions( assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert result["data"][CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" assert len(mock_setup_entry.mock_calls) == 1 @@ -134,14 +144,19 @@ async def test_reauth( exception: Exception, error: dict[str, str], mock_fyta: AsyncMock, - mock_setup_entry, + mock_setup_entry: AsyncMock, ) -> None: """Test reauth-flow works.""" entry = MockConfigEntry( domain=DOMAIN, title=USERNAME, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_EXPIRATION: "2024-06-30T10:00:00+00:00", + }, ) entry.add_to_hass(hass) @@ -157,7 +172,8 @@ async def test_reauth( # tests with connection error result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result["flow_id"], + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) await hass.async_block_till_done() @@ -178,5 +194,5 @@ async def test_reauth( assert result["reason"] == "reauth_successful" assert entry.data[CONF_USERNAME] == "other_username" assert entry.data[CONF_PASSWORD] == "other_password" - - assert len(mock_setup_entry.mock_calls) == 1 + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" diff --git a/tests/components/fyta/test_init.py b/tests/components/fyta/test_init.py new file mode 100644 index 00000000000..844a818df85 --- /dev/null +++ b/tests/components/fyta/test_init.py @@ -0,0 +1,42 @@ +"""Test the initialization.""" + +from unittest.mock import AsyncMock + +from homeassistant.components.fyta.const import CONF_EXPIRATION, DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .test_config_flow import ACCESS_TOKEN, PASSWORD, USERNAME + +from tests.common import MockConfigEntry + + +async def test_migrate_config_entry( + hass: HomeAssistant, + mock_fyta_init: AsyncMock, +) -> None: + """Test successful migration of entry data.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=USERNAME, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + version=1, + minor_version=1, + ) + entry.add_to_hass(hass) + + assert entry.version == 1 + assert entry.minor_version == 1 + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data[CONF_USERNAME] == USERNAME + assert entry.data[CONF_PASSWORD] == PASSWORD + assert entry.data[CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert entry.data[CONF_EXPIRATION] == "2024-12-31T10:00:00+00:00" From 99e3236fb7b6fe2a12a09d17ceff0590694e4bf3 Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Mon, 29 Apr 2024 17:00:13 +0200 Subject: [PATCH 57/96] Deprecate YAML configuration of Habitica (#116374) Add deprecation issue for yaml import --- .../components/habitica/config_flow.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 9ee2aef40ba..9a8852b731d 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_API_USER, DEFAULT_URL, DOMAIN @@ -79,6 +80,20 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import habitica config from configuration.yaml.""" + + async_create_issue( + self.hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + is_fixable=False, + breaks_in_ha_version="2024.11.0", + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Habitica", + }, + ) return await self.async_step_user(import_data) From 39d923dc0273e1728d01e36bf15d2be0d60a86c6 Mon Sep 17 00:00:00 2001 From: Steve Easley Date: Mon, 29 Apr 2024 12:25:16 -0400 Subject: [PATCH 58/96] Fix jvcprojector command timeout with some projectors (#116392) * Fix projector timeout in pyprojector lib v1.0.10 * Fix projector timeout by increasing time between power command and refresh. * Bump jvcprojector lib to ensure unknown power states are handled --- homeassistant/components/jvc_projector/manifest.json | 2 +- homeassistant/components/jvc_projector/remote.py | 3 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jvc_projector/manifest.json b/homeassistant/components/jvc_projector/manifest.json index de7e77197f2..d3e1bf3d940 100644 --- a/homeassistant/components/jvc_projector/manifest.json +++ b/homeassistant/components/jvc_projector/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["jvcprojector"], - "requirements": ["pyjvcprojector==1.0.9"] + "requirements": ["pyjvcprojector==1.0.11"] } diff --git a/homeassistant/components/jvc_projector/remote.py b/homeassistant/components/jvc_projector/remote.py index dcc9e5cff51..b69d3b0118b 100644 --- a/homeassistant/components/jvc_projector/remote.py +++ b/homeassistant/components/jvc_projector/remote.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from collections.abc import Iterable import logging from typing import Any @@ -74,11 +75,13 @@ class JvcProjectorRemote(JvcProjectorEntity, RemoteEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.device.power_on() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.device.power_off() + await asyncio.sleep(1) await self.coordinator.async_refresh() async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 6e41f3b743e..1f83f00bb53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1902,7 +1902,7 @@ pyisy==3.1.14 pyitachip2ir==0.0.7 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2625b50e87..060287d637a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1483,7 +1483,7 @@ pyiss==1.0.1 pyisy==3.1.14 # homeassistant.components.jvc_projector -pyjvcprojector==1.0.9 +pyjvcprojector==1.0.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 8f2d10c49a762dd54edae9d0809cab6406be258a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Mon, 29 Apr 2024 19:33:31 +0200 Subject: [PATCH 59/96] Remove strict connection (#116396) --- homeassistant/components/cloud/__init__.py | 8 -------- homeassistant/components/cloud/prefs.py | 11 +---------- homeassistant/components/http/__init__.py | 18 +++--------------- tests/components/cloud/test_http_api.py | 2 -- tests/components/cloud/test_init.py | 2 ++ tests/components/cloud/test_prefs.py | 1 + .../components/cloud/test_strict_connection.py | 1 + tests/components/http/test_init.py | 2 ++ tests/helpers/test_service.py | 5 ++--- tests/scripts/test_check_config.py | 2 -- 10 files changed, 12 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2552fe4bf5c..13f1d34b5cd 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -30,7 +30,6 @@ from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, - SupportsResponse, callback, ) from homeassistant.exceptions import ( @@ -458,10 +457,3 @@ def _setup_services(hass: HomeAssistant, prefs: CloudPreferences) -> None: "url": f"https://login.home-assistant.io?u={quote_plus(url)}", "direct_url": url, } - - hass.services.async_register( - DOMAIN, - "create_temporary_strict_connection_url", - create_temporary_strict_connection_url, - supports_response=SupportsResponse.ONLY, - ) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 72207513ca9..b4e692d02c4 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -365,16 +365,7 @@ class CloudPreferences: @property def strict_connection(self) -> http.const.StrictConnectionMode: """Return the strict connection mode.""" - mode = self._prefs.get(PREF_STRICT_CONNECTION) - - if mode is None: - # Set to default value - # We store None in the store as the default value to detect if the user has changed the - # value or not. - mode = http.const.StrictConnectionMode.DISABLED - elif not isinstance(mode, http.const.StrictConnectionMode): - mode = http.const.StrictConnectionMode(mode) - return mode + return http.const.StrictConnectionMode.DISABLED async def get_cloud_user(self) -> str: """Return ID of Home Assistant Cloud system user.""" diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 83601599d88..c783d2f0b71 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -10,7 +10,7 @@ import os import socket import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, Required, TypedDict, cast +from typing import Any, Final, TypedDict, cast from urllib.parse import quote_plus, urljoin from aiohttp import web @@ -36,7 +36,6 @@ from homeassistant.core import ( HomeAssistant, ServiceCall, ServiceResponse, - SupportsResponse, callback, ) from homeassistant.exceptions import ( @@ -146,9 +145,6 @@ HTTP_SCHEMA: Final = vol.All( [SSL_INTERMEDIATE, SSL_MODERN] ), vol.Optional(CONF_USE_X_FRAME_OPTIONS, default=True): cv.boolean, - vol.Optional( - CONF_STRICT_CONNECTION, default=StrictConnectionMode.DISABLED - ): vol.Coerce(StrictConnectionMode), } ), ) @@ -172,7 +168,6 @@ class ConfData(TypedDict, total=False): login_attempts_threshold: int ip_ban_enabled: bool ssl_profile: str - strict_connection: Required[StrictConnectionMode] @bind_hass @@ -239,7 +234,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: login_threshold=login_threshold, is_ban_enabled=is_ban_enabled, use_x_frame_options=use_x_frame_options, - strict_connection_non_cloud=conf[CONF_STRICT_CONNECTION], + strict_connection_non_cloud=StrictConnectionMode.DISABLED, ) async def stop_server(event: Event) -> None: @@ -620,7 +615,7 @@ def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: if not user.is_admin: raise Unauthorized(context=call.context) - if conf[CONF_STRICT_CONNECTION] is StrictConnectionMode.DISABLED: + if StrictConnectionMode.DISABLED is StrictConnectionMode.DISABLED: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="strict_connection_not_enabled_non_cloud", @@ -652,10 +647,3 @@ def _setup_services(hass: HomeAssistant, conf: ConfData) -> None: "url": f"https://login.home-assistant.io?u={quote_plus(url)}", "direct_url": url, } - - hass.services.async_register( - DOMAIN, - "create_temporary_strict_connection_url", - create_temporary_strict_connection_url, - supports_response=SupportsResponse.ONLY, - ) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index d9d2b5c6742..1e4dc3173e2 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -915,7 +915,6 @@ async def test_websocket_update_preferences( "google_secure_devices_pin": "1234", "tts_default_voice": ["en-GB", "RyanNeural"], "remote_allow_remote_enable": False, - "strict_connection": StrictConnectionMode.DROP_CONNECTION, } ) response = await client.receive_json() @@ -926,7 +925,6 @@ async def test_websocket_update_preferences( assert cloud.client.prefs.google_secure_devices_pin == "1234" assert cloud.client.prefs.remote_allow_remote_enable is False assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural") - assert cloud.client.prefs.strict_connection is StrictConnectionMode.DROP_CONNECTION @pytest.mark.parametrize( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index bc4526975da..d917dc12a7c 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -303,6 +303,7 @@ async def test_cloud_logout( assert cloud.is_logged_in is False +@pytest.mark.skip(reason="Remove strict connection config option") async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( hass: HomeAssistant, ) -> None: @@ -323,6 +324,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( ("mode"), [ diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 57715fe2bdf..a8ce88f5700 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -181,6 +181,7 @@ async def test_tts_default_voice_legacy_gender( assert cloud.client.prefs.tts_default_voice == (expected_language, voice) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize("mode", list(StrictConnectionMode)) async def test_strict_connection_convertion( hass: HomeAssistant, diff --git a/tests/components/cloud/test_strict_connection.py b/tests/components/cloud/test_strict_connection.py index f275bc4d2dd..c3329740207 100644 --- a/tests/components/cloud/test_strict_connection.py +++ b/tests/components/cloud/test_strict_connection.py @@ -226,6 +226,7 @@ async def _guard_page_unauthorized_request( assert await req.text() == await hass.async_add_executor_job(read_guard_page) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( "test_func", [ diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index b554737e7b3..9e576e10f4d 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -527,6 +527,7 @@ async def test_logging( assert "GET /api/states/logging.entity" not in caplog.text +@pytest.mark.skip(reason="Remove strict connection config option") async def test_service_create_temporary_strict_connection_url_strict_connection_disabled( hass: HomeAssistant, ) -> None: @@ -544,6 +545,7 @@ async def test_service_create_temporary_strict_connection_url_strict_connection_ ) +@pytest.mark.skip(reason="Remove strict connection config option") @pytest.mark.parametrize( ("mode"), [ diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index e32768ee33e..c9d92c2f25a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -800,11 +800,10 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: assert proxy_load_services_files.mock_calls[0][1][1] == unordered( [ await async_get_integration(hass, DOMAIN_GROUP), - await async_get_integration(hass, "http"), # system_health requires http ] ) - assert len(descriptions) == 2 + assert len(descriptions) == 1 assert DOMAIN_GROUP in descriptions assert "description" in descriptions[DOMAIN_GROUP]["reload"] assert "fields" in descriptions[DOMAIN_GROUP]["reload"] @@ -838,7 +837,7 @@ async def test_async_get_all_descriptions(hass: HomeAssistant) -> None: await async_setup_component(hass, DOMAIN_LOGGER, logger_config) descriptions = await service.async_get_all_descriptions(hass) - assert len(descriptions) == 3 + assert len(descriptions) == 2 assert DOMAIN_LOGGER in descriptions assert descriptions[DOMAIN_LOGGER]["set_default_level"]["name"] == "Translated name" assert ( diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 76acb2ff678..79c64259f8b 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pytest -from homeassistant.components.http.const import StrictConnectionMode from homeassistant.config import YAML_CONFIG_FILE from homeassistant.scripts import check_config @@ -135,7 +134,6 @@ def test_secrets(mock_is_file, event_loop, mock_hass_config_yaml: None) -> None: "login_attempts_threshold": -1, "server_port": 8123, "ssl_profile": "modern", - "strict_connection": StrictConnectionMode.DISABLED, "use_x_frame_options": True, "server_host": ["0.0.0.0", "::"], } From 06e032b8386280537c2f8b3bf152c523002bf97b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 29 Apr 2024 18:34:20 +0200 Subject: [PATCH 60/96] Update frontend to 20240429.0 (#116404) --- 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 a5446f688ba..e271903a27d 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==20240426.0"] + "requirements": ["home-assistant-frontend==20240429.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b4223d7b33..a2eb0f1254c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f83f00bb53..4e788f9aa80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 060287d637a..16975128dae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240426.0 +home-assistant-frontend==20240429.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From a7faf2710f2c5ed62a414b7ec275de7244ce91f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 29 Apr 2024 19:44:22 +0200 Subject: [PATCH 61/96] Bump version to 2024.5.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 07f4058ea19..35be5835088 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 68b38bb516b..575063541e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b2" +version = "2024.5.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8843780aab7469fb3b928da3744fc40ad422f378 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 09:49:35 +0200 Subject: [PATCH 62/96] Set Synology camera device name as entity name (#109123) --- homeassistant/components/synology_dsm/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 901fcb1d565..1d03fd4f027 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -74,7 +74,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C api_key=SynoSurveillanceStation.CAMERA_API_KEY, key=str(camera_id), camera_id=camera_id, - name=coordinator.data["cameras"][camera_id].name, + name=None, entity_registry_enabled_default=coordinator.data["cameras"][ camera_id ].is_enabled, From 5d9abf9ac52b49c210650626b1c75c8d31479d17 Mon Sep 17 00:00:00 2001 From: Collin Fair Date: Tue, 30 Apr 2024 01:18:09 -0600 Subject: [PATCH 63/96] Fix stale prayer times from `islamic-prayer-times` (#115683) --- .../islamic_prayer_times/coordinator.py | 83 +++++++++---------- .../islamic_prayer_times/__init__.py | 23 ++++- .../islamic_prayer_times/test_init.py | 49 ++++++++++- .../islamic_prayer_times/test_sensor.py | 31 +++++-- 4 files changed, 131 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/coordinator.py b/homeassistant/components/islamic_prayer_times/coordinator.py index 2785f69534c..7005bee3585 100644 --- a/homeassistant/components/islamic_prayer_times/coordinator.py +++ b/homeassistant/components/islamic_prayer_times/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta import logging from typing import Any, cast @@ -70,8 +70,8 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim """Return the school.""" return self.config_entry.options.get(CONF_SCHOOL, DEFAULT_SCHOOL) - def get_new_prayer_times(self) -> dict[str, Any]: - """Fetch prayer times for today.""" + def get_new_prayer_times(self, for_date: date) -> dict[str, Any]: + """Fetch prayer times for the specified date.""" calc = PrayerTimesCalculator( latitude=self.latitude, longitude=self.longitude, @@ -79,7 +79,7 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim latitudeAdjustmentMethod=self.lat_adj_method, midnightMode=self.midnight_mode, school=self.school, - date=str(dt_util.now().date()), + date=str(for_date), iso8601=True, ) return cast(dict[str, Any], calc.fetch_prayer_times()) @@ -88,51 +88,18 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim def async_schedule_future_update(self, midnight_dt: datetime) -> None: """Schedule future update for sensors. - Midnight is a calculated time. The specifics of the calculation - depends on the method of the prayer time calculation. This calculated - midnight is the time at which the time to pray the Isha prayers have - expired. + The least surprising behaviour is to load the next day's prayer times only + after the current day's prayers are complete. We will take the fiqhi opinion + that Isha should be prayed before Islamic midnight (which may be before or after 12:00 midnight), + and thus we will switch to the next day's timings at Islamic midnight. - Calculated Midnight: The Islamic midnight. - Traditional Midnight: 12:00AM - - Update logic for prayer times: - - If the Calculated Midnight is before the traditional midnight then wait - until the traditional midnight to run the update. This way the day - will have changed over and we don't need to do any fancy calculations. - - If the Calculated Midnight is after the traditional midnight, then wait - until after the calculated Midnight. We don't want to update the prayer - times too early or else the timings might be incorrect. - - Example: - calculated midnight = 11:23PM (before traditional midnight) - Update time: 12:00AM - - calculated midnight = 1:35AM (after traditional midnight) - update time: 1:36AM. + The +1s is to ensure that any automations predicated on the arrival of Islamic midnight will run. """ _LOGGER.debug("Scheduling next update for Islamic prayer times") - now = dt_util.utcnow() - - if now > midnight_dt: - next_update_at = midnight_dt + timedelta(days=1, minutes=1) - _LOGGER.debug( - "Midnight is after the day changes so schedule update for after Midnight the next day" - ) - else: - _LOGGER.debug( - "Midnight is before the day changes so schedule update for the next start of day" - ) - next_update_at = dt_util.start_of_local_day(now + timedelta(days=1)) - - _LOGGER.debug("Next update scheduled for: %s", next_update_at) - self.event_unsub = async_track_point_in_time( - self.hass, self.async_request_update, next_update_at + self.hass, self.async_request_update, midnight_dt + timedelta(seconds=1) ) async def async_request_update(self, _: datetime) -> None: @@ -140,8 +107,34 @@ class IslamicPrayerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, datetim await self.async_request_refresh() async def _async_update_data(self) -> dict[str, datetime]: - """Update sensors with new prayer times.""" - prayer_times = self.get_new_prayer_times() + """Update sensors with new prayer times. + + Prayer time calculations "roll over" at 12:00 midnight - but this does not mean that all prayers + occur within that Gregorian calendar day. For instance Jasper, Alta. sees Isha occur after 00:00 in the summer. + It is similarly possible (albeit less likely) that Fajr occurs before 00:00. + + As such, to ensure that no prayer times are "unreachable" (e.g. we always see the Isha timestamp pass before loading the next day's times), + we calculate 3 days' worth of times (-1, 0, +1 days) and select the appropriate set based on Islamic midnight. + + The calculation is inexpensive, so there is no need to cache it. + """ + + # Zero out the us component to maintain consistent rollover at T+1s + now = dt_util.now().replace(microsecond=0) + yesterday_times = self.get_new_prayer_times((now - timedelta(days=1)).date()) + today_times = self.get_new_prayer_times(now.date()) + tomorrow_times = self.get_new_prayer_times((now + timedelta(days=1)).date()) + + if ( + yesterday_midnight := dt_util.parse_datetime(yesterday_times["Midnight"]) + ) and now <= yesterday_midnight: + prayer_times = yesterday_times + elif ( + tomorrow_midnight := dt_util.parse_datetime(today_times["Midnight"]) + ) and now > tomorrow_midnight: + prayer_times = tomorrow_times + else: + prayer_times = today_times # introduced in prayer-times-calculator 0.0.8 prayer_times.pop("date", None) diff --git a/tests/components/islamic_prayer_times/__init__.py b/tests/components/islamic_prayer_times/__init__.py index 1e6d6815921..522006b0847 100644 --- a/tests/components/islamic_prayer_times/__init__.py +++ b/tests/components/islamic_prayer_times/__init__.py @@ -12,6 +12,17 @@ MOCK_USER_INPUT = { MOCK_CONFIG = {CONF_LATITUDE: 12.34, CONF_LONGITUDE: 23.45} + +PRAYER_TIMES_YESTERDAY = { + "Fajr": "2019-12-31T06:09:00+00:00", + "Sunrise": "2019-12-31T07:24:00+00:00", + "Dhuhr": "2019-12-31T12:29:00+00:00", + "Asr": "2019-12-31T15:31:00+00:00", + "Maghrib": "2019-12-31T17:34:00+00:00", + "Isha": "2019-12-31T18:52:00+00:00", + "Midnight": "2020-01-01T00:44:00+00:00", +} + PRAYER_TIMES = { "Fajr": "2020-01-01T06:10:00+00:00", "Sunrise": "2020-01-01T07:25:00+00:00", @@ -19,7 +30,17 @@ PRAYER_TIMES = { "Asr": "2020-01-01T15:32:00+00:00", "Maghrib": "2020-01-01T17:35:00+00:00", "Isha": "2020-01-01T18:53:00+00:00", - "Midnight": "2020-01-01T00:45:00+00:00", + "Midnight": "2020-01-02T00:45:00+00:00", +} + +PRAYER_TIMES_TOMORROW = { + "Fajr": "2020-01-02T06:11:00+00:00", + "Sunrise": "2020-01-02T07:26:00+00:00", + "Dhuhr": "2020-01-02T12:31:00+00:00", + "Asr": "2020-01-02T15:33:00+00:00", + "Maghrib": "2020-01-02T17:36:00+00:00", + "Isha": "2020-01-02T18:54:00+00:00", + "Midnight": "2020-01-03T00:46:00+00:00", } NOW = datetime(2020, 1, 1, 00, 00, 0, tzinfo=dt_util.UTC) diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index c5d4933e24a..2a2597ef0ce 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,5 +1,6 @@ """Tests for Islamic Prayer Times init.""" +from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time @@ -12,10 +13,11 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture(autouse=True) @@ -76,13 +78,16 @@ async def test_options_listener(hass: HomeAssistant) -> None: ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert mock_fetch_prayer_times.call_count == 1 + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 + mock_fetch_prayer_times.reset_mock() hass.config_entries.async_update_entry( entry, options={CONF_CALC_METHOD: "makkah"} ) await hass.async_block_till_done() - assert mock_fetch_prayer_times.call_count == 2 + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 @pytest.mark.parametrize( @@ -155,3 +160,41 @@ async def test_migration_from_1_1_to_1_2(hass: HomeAssistant) -> None: CONF_LONGITUDE: hass.config.longitude, } assert entry.minor_version == 2 + + +async def test_update_scheduling(hass: HomeAssistant) -> None: + """Test that integration schedules update immediately after Islamic midnight.""" + entry = MockConfigEntry(domain=islamic_prayer_times.DOMAIN, data={}) + entry.add_to_hass(hass) + + with ( + patch( + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ), + freeze_time(NOW), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + with patch( + "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", + return_value=PRAYER_TIMES, + ) as mock_fetch_prayer_times: + midnight_time = dt_util.parse_datetime(PRAYER_TIMES["Midnight"]) + assert midnight_time + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + mock_fetch_prayer_times.assert_not_called() + + midnight_time += timedelta(seconds=1) + with freeze_time(midnight_time): + async_fire_time_changed(hass, midnight_time) + await hass.async_block_till_done() + + # Each scheduling run calls this 3 times (yesterday, today, tomorrow) + assert mock_fetch_prayer_times.call_count == 3 diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 1f8d28dfb6f..7bd1a1192ad 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Islamic prayer times sensor platform.""" +from datetime import timedelta from unittest.mock import patch from freezegun import freeze_time @@ -8,7 +9,7 @@ import pytest from homeassistant.components.islamic_prayer_times.const import DOMAIN from homeassistant.core import HomeAssistant -from . import NOW, PRAYER_TIMES +from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TOMORROW, PRAYER_TIMES_YESTERDAY from tests.common import MockConfigEntry @@ -31,20 +32,38 @@ def set_utc(hass: HomeAssistant) -> None: ("Midnight", "sensor.islamic_prayer_times_midnight_time"), ], ) +# In our example data, Islamic midnight occurs at 00:44 (yesterday's times, occurs today) and 00:45 (today's times, occurs tomorrow), +# hence we check that the times roll over at exactly the desired minute +@pytest.mark.parametrize( + ("offset", "prayer_times"), + [ + (timedelta(days=-1), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44), PRAYER_TIMES_YESTERDAY), + (timedelta(minutes=44, seconds=1), PRAYER_TIMES), # Rolls over at 00:44 + 1 sec + (timedelta(days=1, minutes=45), PRAYER_TIMES), + ( + timedelta(days=1, minutes=45, seconds=1), # Rolls over at 00:45 + 1 sec + PRAYER_TIMES_TOMORROW, + ), + ], +) async def test_islamic_prayer_times_sensors( - hass: HomeAssistant, key: str, sensor_name: str + hass: HomeAssistant, + key: str, + sensor_name: str, + offset: timedelta, + prayer_times: dict[str, str], ) -> None: """Test minimum Islamic prayer times configuration.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) - with ( patch( "prayer_times_calculator_offline.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, + side_effect=(PRAYER_TIMES_YESTERDAY, PRAYER_TIMES, PRAYER_TIMES_TOMORROW), ), - freeze_time(NOW), + freeze_time(NOW + offset), ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(sensor_name).state == PRAYER_TIMES[key] + assert hass.states.get(sensor_name).state == prayer_times[key] From 3477c81ed1c8146c4a30aa794b1f55651b0cfdf6 Mon Sep 17 00:00:00 2001 From: Graham Wetzler Date: Tue, 30 Apr 2024 02:47:06 -0500 Subject: [PATCH 64/96] Bump smart_meter_texas to 0.5.5 (#116321) --- homeassistant/components/smart_meter_texas/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smart_meter_texas/conftest.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smart_meter_texas/manifest.json b/homeassistant/components/smart_meter_texas/manifest.json index 1b18bbb2bc9..8bf44fbed15 100644 --- a/homeassistant/components/smart_meter_texas/manifest.json +++ b/homeassistant/components/smart_meter_texas/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/smart_meter_texas", "iot_class": "cloud_polling", "loggers": ["smart_meter_texas"], - "requirements": ["smart-meter-texas==0.4.7"] + "requirements": ["smart-meter-texas==0.5.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4e788f9aa80..dca62841c08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2563,7 +2563,7 @@ slackclient==2.5.0 slixmpp==1.8.5 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16975128dae..17411a81818 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1982,7 +1982,7 @@ simplisafe-python==2024.01.0 slackclient==2.5.0 # homeassistant.components.smart_meter_texas -smart-meter-texas==0.4.7 +smart-meter-texas==0.5.5 # homeassistant.components.smhi smhi-pkg==1.0.16 diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 04a3344b5cc..d06571fe05e 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -58,7 +58,7 @@ def mock_connection( """Mock all calls to the API.""" aioclient_mock.get(BASE_URL) - auth_endpoint = f"{BASE_ENDPOINT}{AUTH_ENDPOINT}" + auth_endpoint = AUTH_ENDPOINT if not auth_fail and not auth_timeout: aioclient_mock.post( auth_endpoint, From 1a1dfbd4891f277810a1b53d5d623467089be862 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 29 Apr 2024 20:13:36 +0200 Subject: [PATCH 65/96] Remove semicolon in Modbus (#116399) --- homeassistant/components/modbus/modbus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index bd7eed8235c..a5c0867dedb 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -245,7 +245,7 @@ async def async_modbus_setup( translation_key="deprecated_restart", ) _LOGGER.warning( - "`modbus.restart`: is deprecated and will be removed in version 2024.11" + "`modbus.restart` is deprecated and will be removed in version 2024.11" ) async_dispatcher_send(hass, SIGNAL_START_ENTITY) hub = hub_collect[service.data[ATTR_HUB]] From bd8ded1e55c4b148dc323f5843381ccd761f5724 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 29 Apr 2024 20:19:14 +0200 Subject: [PATCH 66/96] Fix error handling in Shell Command integration (#116409) * raise proper HomeAssistantError on command timeout * raise proper HomeAssistantError on non-utf8 command output * add error translation and test it * Update homeassistant/components/shell_command/strings.json * Update tests/components/shell_command/test_init.py --------- Co-authored-by: G Johansson --- .../components/shell_command/__init__.py | 21 ++++++++++++++----- .../components/shell_command/strings.json | 10 +++++++++ tests/components/shell_command/test_init.py | 12 ++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/shell_command/strings.json diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 95bbb01bcfb..c2c384e39aa 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -15,7 +15,7 @@ from homeassistant.core import ( ServiceResponse, SupportsResponse, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import JsonObjectType @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: try: async with asyncio.timeout(COMMAND_TIMEOUT): stdout_data, stderr_data = await process.communicate() - except TimeoutError: + except TimeoutError as err: _LOGGER.error( "Timed out running command: `%s`, after: %ss", cmd, COMMAND_TIMEOUT ) @@ -103,7 +103,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: process._transport.close() # type: ignore[attr-defined] del process - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="timeout", + translation_placeholders={ + "command": cmd, + "timeout": str(COMMAND_TIMEOUT), + }, + ) from err if stdout_data: _LOGGER.debug( @@ -135,11 +142,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: service_response["stdout"] = stdout_data.decode("utf-8").strip() if stderr_data: service_response["stderr"] = stderr_data.decode("utf-8").strip() - except UnicodeDecodeError: + except UnicodeDecodeError as err: _LOGGER.exception( "Unable to handle non-utf8 output of command: `%s`", cmd ) - raise + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="non_utf8_output", + translation_placeholders={"command": cmd}, + ) from err return service_response return None diff --git a/homeassistant/components/shell_command/strings.json b/homeassistant/components/shell_command/strings.json new file mode 100644 index 00000000000..c87dac15b2d --- /dev/null +++ b/homeassistant/components/shell_command/strings.json @@ -0,0 +1,10 @@ +{ + "exceptions": { + "timeout": { + "message": "Timed out running command: `{command}`, after: {timeout} seconds" + }, + "non_utf8_output": { + "message": "Unable to handle non-utf8 output of command: `{command}`" + } + } +} diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 93b06ddf9d8..526ac1643ec 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components import shell_command from homeassistant.core import HomeAssistant -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.setup import async_setup_component @@ -199,7 +199,10 @@ async def test_non_text_stdout_capture( assert not response # Non-text output throws with 'return_response' - with pytest.raises(UnicodeDecodeError): + with pytest.raises( + HomeAssistantError, + match="Unable to handle non-utf8 output of command: `curl -o - https://raw.githubusercontent.com/home-assistant/assets/master/misc/loading-screen.gif`", + ): response = await hass.services.async_call( "shell_command", "output_image", blocking=True, return_response=True ) @@ -258,7 +261,10 @@ async def test_do_not_run_forever( side_effect=mock_create_subprocess_shell, ), ): - with pytest.raises(TimeoutError): + with pytest.raises( + HomeAssistantError, + match="Timed out running command: `mock_sleep 10000`, after: 0.001 seconds", + ): await hass.services.async_call( shell_command.DOMAIN, "test_service", From 5510315b87916402877116dbdf480bc3b95458f2 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 09:48:58 +0200 Subject: [PATCH 67/96] Fix zoneminder async (#116436) --- homeassistant/components/zoneminder/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 0510ff58d35..b4a406cec4e 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -55,7 +55,7 @@ SET_RUN_STATE_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ZoneMinder component.""" hass.data[DOMAIN] = {} @@ -99,7 +99,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: state_name, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA ) From 7cbb2892c115d97510adfffa429ee5c19c4c8929 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 12:41:34 +0200 Subject: [PATCH 68/96] Add user id to coordinator name in Withings (#116440) * Add user id to coordinator name in Withings * Add user id to coordinator name in Withings * Fix --- homeassistant/components/withings/coordinator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 19b362dfa0a..0aef11aaa6b 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -45,9 +45,10 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): super().__init__( hass, LOGGER, - name=f"Withings {self.coordinator_name}", + name="", update_interval=self._default_update_interval, ) + self.name = f"Withings {self.config_entry.unique_id} {self.coordinator_name}" self._client = client self.notification_categories: set[NotificationCategory] = set() @@ -63,7 +64,11 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[_T]): self, notification_category: NotificationCategory ) -> None: """Update data when webhook is called.""" - LOGGER.debug("Withings webhook triggered for %s", notification_category) + LOGGER.debug( + "Withings webhook triggered for category %s for user %s", + notification_category, + self.config_entry.unique_id, + ) await self.async_request_refresh() async def _async_update_data(self) -> _T: From 5b7e09b8868d77e6abd5694ee2c729eb23b14d42 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 30 Apr 2024 12:47:51 +0200 Subject: [PATCH 69/96] Bump version to 2024.5.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 35be5835088..3a0d35b8324 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 575063541e9..a04e9fd218a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b3" +version = "2024.5.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From c0d529b072c0f5ef6d62f42e7e9029003a2b77c5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 11:46:52 +0200 Subject: [PATCH 70/96] Some fixes for the Matter light discovery schema (#116108) * Fix discovery schema for light platform * fix switch platform discovery schema * extend light tests * Update switch.py * clarify comment * use parameter for supported_color_modes --- homeassistant/components/matter/light.py | 41 +-- homeassistant/components/matter/switch.py | 6 +- ...onoff-light-with-levelcontrol-present.json | 244 ++++++++++++++++++ tests/components/matter/test_light.py | 29 ++- 4 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index fce780896a4..c9556fd2e2e 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -295,7 +295,10 @@ class MatterLight(MatterEntity, LightEntity): # brightness support if self._entity_info.endpoint.has_attribute( None, clusters.LevelControl.Attributes.CurrentLevel - ): + ) and self._entity_info.endpoint.device_types != {device_types.OnOffLight}: + # We need to filter out the OnOffLight device type here because + # that can have an optional LevelControl cluster present + # which we should ignore. supported_color_modes.add(ColorMode.BRIGHTNESS) self._supports_brightness = True # colormode(s) @@ -406,11 +409,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentHue, clusters.ColorControl.Attributes.CurrentSaturation, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentX, @@ -426,11 +429,11 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterLight, required_attributes=( clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.CurrentX, clusters.ColorControl.Attributes.CurrentY, ), optional_attributes=( + clusters.LevelControl.Attributes.CurrentLevel, clusters.ColorControl.Attributes.ColorTemperatureMireds, clusters.ColorControl.Attributes.ColorMode, clusters.ColorControl.Attributes.CurrentHue, @@ -451,36 +454,4 @@ DISCOVERY_SCHEMAS = [ ), optional_attributes=(clusters.ColorControl.Attributes.ColorMode,), ), - # Additional schema to match generic dimmable lights with incorrect/missing device type - MatterDiscoverySchema( - platform=Platform.LIGHT, - entity_description=LightEntityDescription( - key="MatterDimmableLightFallback", name=None - ), - entity_class=MatterLight, - required_attributes=( - clusters.OnOff.Attributes.OnOff, - clusters.LevelControl.Attributes.CurrentLevel, - ), - optional_attributes=( - clusters.ColorControl.Attributes.ColorMode, - clusters.ColorControl.Attributes.CurrentHue, - clusters.ColorControl.Attributes.CurrentSaturation, - clusters.ColorControl.Attributes.CurrentX, - clusters.ColorControl.Attributes.CurrentY, - clusters.ColorControl.Attributes.ColorTemperatureMireds, - ), - # important: make sure to rule out all device types that are also based on the - # onoff and levelcontrol clusters ! - not_device_type=( - device_types.Fan, - device_types.GenericSwitch, - device_types.OnOffPlugInUnit, - device_types.HeatingCoolingUnit, - device_types.Pump, - device_types.CastingVideoClient, - device_types.VideoRemoteControl, - device_types.Speaker, - ), - ), ] diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 9bc858d40c0..f148102cfcd 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -81,12 +81,8 @@ DISCOVERY_SCHEMAS = [ device_types.ColorTemperatureLight, device_types.DimmableLight, device_types.ExtendedColorLight, - device_types.OnOffLight, - device_types.DoorLock, device_types.ColorDimmerSwitch, - device_types.DimmerSwitch, - device_types.Thermostat, - device_types.RoomAirConditioner, + device_types.OnOffLight, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json new file mode 100644 index 00000000000..c1264f5b7ea --- /dev/null +++ b/tests/components/matter/fixtures/nodes/onoff-light-with-levelcontrol-present.json @@ -0,0 +1,244 @@ +{ + "node_id": 8, + "date_commissioned": "2024-03-07T01:39:20.590755", + "last_interview": "2024-04-02T14:16:31.045880", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 54, 60, 62, 63], + "0/29/2": [41], + "0/29/3": [1], + "0/29/65532": 0, + "0/29/65533": 1, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "254": 1 + }, + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 2 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 3, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 1, + "0/40/1": "Leviton", + "0/40/2": 4251, + "0/40/3": "D215S", + "0/40/4": 4097, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 1, + "0/40/8": "1.0", + "0/40/9": 131365, + "0/40/10": "2.1.25", + "0/40/15": "12345678", + "0/40/18": "abcdefgh", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 18, 19, 65528, 65529, 65531, 65532, + 65533 + ], + "0/42/0": [], + "0/42/1": true, + "0/42/2": 1, + "0/42/3": null, + "0/42/65532": 0, + "0/42/65533": 1, + "0/42/65528": [], + "0/42/65529": [0], + "0/42/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/43/0": "en-US", + "0/43/1": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ], + "0/43/65532": 0, + "0/43/65533": 1, + "0/43/65528": [], + "0/43/65529": [], + "0/43/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "0/44/0": 0, + "0/44/1": 0, + "0/44/2": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7], + "0/44/65532": 0, + "0/44/65533": 1, + "0/44/65528": [], + "0/44/65529": [], + "0/44/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 1, + "0/49/2": 10, + "0/49/3": 20, + "0/49/4": true, + "0/49/5": 0, + "0/49/7": null, + "0/49/65532": 1, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/50/65532": 0, + "0/50/65533": 1, + "0/50/65528": [1], + "0/50/65529": [0], + "0/50/65531": [65528, 65529, 65531, 65532, 65533], + "0/51/1": 1, + "0/51/2": 2380987, + "0/51/3": 661, + "0/51/4": 0, + "0/51/5": [], + "0/51/6": [], + "0/51/7": [], + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [0], + "0/51/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ], + "0/52/1": 49792, + "0/52/2": 262528, + "0/52/3": 272704, + "0/52/65532": 1, + "0/52/65533": 1, + "0/52/65528": [], + "0/52/65529": [0], + "0/52/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/54/0": "blah", + "0/54/1": 4, + "0/54/2": 3, + "0/54/3": 11, + "0/54/4": -43, + "0/54/65532": 0, + "0/54/65533": 1, + "0/54/65528": [], + "0/54/65529": [], + "0/54/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/5": 2, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 3, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 1, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 2, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/4/0": 128, + "1/4/65532": 1, + "1/4/65533": 4, + "1/4/65528": [0, 1, 2, 3], + "1/4/65529": [0, 1, 2, 3, 4, 5], + "1/4/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/16384": true, + "1/6/16385": 0, + "1/6/16386": 0, + "1/6/16387": null, + "1/6/65532": 1, + "1/6/65533": 4, + "1/6/65528": [], + "1/6/65529": [0, 1, 2, 64, 65, 66], + "1/6/65531": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ], + "1/8/0": 254, + "1/8/1": 0, + "1/8/2": 1, + "1/8/3": 254, + "1/8/15": 0, + "1/8/16": 0, + "1/8/17": null, + "1/8/20": 50, + "1/8/16384": null, + "1/8/65532": 3, + "1/8/65533": 5, + "1/8/65528": [], + "1/8/65529": [0, 1, 2, 3, 4, 5, 6, 7], + "1/8/65531": [ + 0, 1, 2, 3, 15, 16, 17, 20, 16384, 65528, 65529, 65531, 65532, 65533 + ], + "1/29/0": [ + { + "0": 256, + "1": 1 + } + ], + "1/29/1": [3, 4, 6, 8, 29], + "1/29/2": [], + "1/29/3": [], + "1/29/65532": 0, + "1/29/65533": 1, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_light.py b/tests/components/matter/test_light.py index 9c3c2610d92..775790701d1 100644 --- a/tests/components/matter/test_light.py +++ b/tests/components/matter/test_light.py @@ -18,21 +18,31 @@ from .common import ( # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) @pytest.mark.parametrize( - ("fixture", "entity_id"), + ("fixture", "entity_id", "supported_color_modes"), [ - ("extended-color-light", "light.mock_extended_color_light"), - ("color-temperature-light", "light.mock_color_temperature_light"), - ("dimmable-light", "light.mock_dimmable_light"), - ("onoff-light", "light.mock_onoff_light"), + ( + "extended-color-light", + "light.mock_extended_color_light", + ["color_temp", "hs", "xy"], + ), + ( + "color-temperature-light", + "light.mock_color_temperature_light", + ["color_temp"], + ), + ("dimmable-light", "light.mock_dimmable_light", ["brightness"]), + ("onoff-light", "light.mock_onoff_light", ["onoff"]), + ("onoff-light-with-levelcontrol-present", "light.d215s", ["onoff"]), ], ) -async def test_on_off_light( +async def test_light_turn_on_off( hass: HomeAssistant, matter_client: MagicMock, fixture: str, entity_id: str, + supported_color_modes: list[str], ) -> None: - """Test an on/off light.""" + """Test basic light discovery and turn on/off.""" light_node = await setup_integration_with_node_fixture( hass, @@ -48,6 +58,11 @@ async def test_on_off_light( assert state is not None assert state.state == "off" + # check the supported_color_modes + # especially important is the onoff light device type that does have + # a levelcontrol cluster present which we should ignore + assert state.attributes["supported_color_modes"] == supported_color_modes + # Test that the light is on set_node_attribute(light_node, 1, 6, 0, True) await trigger_subscription_callback(hass, matter_client) From 78d19854dda6f60a6b36d20efc889c32c71a91b8 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:49:28 +0200 Subject: [PATCH 71/96] Bump bimmer_connected to 0.15.2 (#116424) Co-authored-by: Richard --- .../bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/conftest.py | 3 +- .../snapshots/test_diagnostics.ambr | 1618 +++++++++-------- 5 files changed, 871 insertions(+), 756 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 854a2f87410..c6b180ca728 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.14.6"] + "requirements": ["bimmer-connected[china]==0.15.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index dca62841c08..04097f0a9bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -544,7 +544,7 @@ beautifulsoup4==4.12.3 bellows==0.38.2 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17411a81818..bbc60f01547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -469,7 +469,7 @@ beautifulsoup4==4.12.3 bellows==0.38.2 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.14.6 +bimmer-connected[china]==0.15.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index c3a89e28bd6..f43a7c089c7 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator -from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_STATES +from bimmer_connected.tests import ALL_CHARGING_SETTINGS, ALL_PROFILES, ALL_STATES from bimmer_connected.tests.common import MyBMWMockRouter from bimmer_connected.vehicle import remote_services import pytest @@ -23,6 +23,7 @@ def bmw_fixture( "WBA00000000DEMO03", "WBY00000000REXI01", ], + profiles=ALL_PROFILES, states=ALL_STATES, charging_settings=ALL_CHARGING_SETTINGS, ) diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index b3af5bc59b6..351c0f062fd 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -140,19 +140,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -206,9 +195,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'BLUETOOTH', 'bodyType': 'I20', 'brand': 'BMW_I', 'color': 4285537312, @@ -223,7 +210,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'iX xDrive50', 'softwareVersionCurrent': dict({ 'iStep': 300, @@ -413,14 +399,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -786,19 +764,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'CHARGING', 'charging_target': 80, 'is_charger_connected': True, @@ -829,6 +796,7 @@ 'software_version': '07/2021.00', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': True, 'is_remote_charge_stop_enabled': True, @@ -1026,19 +994,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'HEATING', 'activity_end_time': '2022-07-10T11:29:50+00:00', - 'activity_end_time_no_tz': '2022-07-10T11:29:50', 'is_climate_on': True, }), 'condition_based_services': dict({ @@ -1092,9 +1049,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G26', 'brand': 'BMW', 'color': 4284245350, @@ -1109,7 +1064,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'i4 eDrive40', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1287,14 +1241,6 @@ 'servicePack': 'WAVE_01', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -1651,19 +1597,8 @@ 'is_pre_entry_climatization_enabled', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': '2022-07-10T11:10:00+00:00', 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': 'NOT_CHARGING', 'charging_target': 80, 'is_charger_connected': False, @@ -1694,6 +1629,7 @@ 'software_version': '11/2021.70', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': True, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -1770,23 +1706,7 @@ 'windows', ]), 'brand': 'bmw', - 'charging_profile': dict({ - 'ac_available_limits': None, - 'ac_current_limit': None, - 'charging_mode': 'IMMEDIATE_CHARGING', - 'charging_preferences': 'NO_PRESELECTION', - 'charging_preferences_service_pack': None, - 'departure_times': list([ - ]), - 'is_pre_entry_climatization_enabled': False, - 'preferred_charging_window': dict({ - '_window_dict': dict({ - }), - 'end_time': '00:00:00', - 'start_time': '00:00:00', - }), - 'timer_type': 'UNKNOWN', - }), + 'charging_profile': None, 'check_control_messages': dict({ 'has_check_control_messages': False, 'messages': list([ @@ -1803,19 +1723,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'INACTIVE', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -1878,9 +1787,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'DEMO', 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', 'bodyType': 'G20', 'brand': 'BMW', 'color': 4280233344, @@ -1895,7 +1802,6 @@ 'headUnitRaw': 'HU_MGU', 'headUnitType': 'MGU', 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', 'model': 'M340i xDrive', 'softwareVersionCurrent': dict({ 'iStep': 470, @@ -1974,17 +1880,8 @@ 'vehicleFinder': True, 'vehicleStateSource': 'LAST_STATE_CALL', }), - 'charging_settings': dict({ - }), + 'charging_settings': None, 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingMode': 'IMMEDIATE_CHARGING', @@ -2288,19 +2185,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': None, - 'charging_start_time_no_tz': None, 'charging_status': None, 'charging_target': None, 'is_charger_connected': False, @@ -2331,6 +2217,7 @@ 'software_version': '07/2021.70', }), 'is_charging_plan_supported': False, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -2540,19 +2427,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -2588,9 +2464,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -2602,9 +2476,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -2622,6 +2496,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -2731,10 +2606,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -2989,19 +2860,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -3032,6 +2892,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -3066,204 +2927,297 @@ ]), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -4875,19 +4829,8 @@ ]), }), 'climate': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'activity': 'UNKNOWN', 'activity_end_time': None, - 'activity_end_time_no_tz': None, 'is_climate_on': False, }), 'condition_based_services': dict({ @@ -4923,9 +4866,7 @@ ]), }), 'data': dict({ - 'appVehicleType': 'CONNECTED', 'attributes': dict({ - 'a4aType': 'USB_ONLY', 'bodyType': 'I01', 'brand': 'BMW_I', 'color': 4284110934, @@ -4937,9 +4878,9 @@ 'iosAppScheme': 'bmwdriversguide:///open', 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', }), + 'headUnitRaw': 'MGU_02_L', 'headUnitType': 'NBT', 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', 'model': 'i3 (+ REX)', 'softwareVersionCurrent': dict({ 'iStep': 510, @@ -4957,6 +4898,7 @@ }), 'seriesCluster': 'I001', }), + 'telematicsUnit': 'WAVE01', 'year': 2015, }), 'capabilities': dict({ @@ -5066,10 +5008,6 @@ 'servicePack': 'TCB1', }), 'fetched_at': '2022-07-10T11:00:00+00:00', - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), 'state': dict({ 'chargingProfile': dict({ 'chargingControlType': 'WEEKLY_PLANNER', @@ -5324,19 +5262,8 @@ 'remaining_fuel_percent', ]), 'fuel_and_battery': dict({ - 'account_timezone': dict({ - '_dst_offset': '0:00:00', - '_dst_saved': '0:00:00', - '_hasdst': False, - '_std_offset': '0:00:00', - '_tznames': list([ - 'UTC', - 'UTC', - ]), - }), 'charging_end_time': None, 'charging_start_time': '2022-07-10T18:01:00+00:00', - 'charging_start_time_no_tz': '2022-07-10T18:01:00', 'charging_status': 'WAITING_FOR_CHARGING', 'charging_target': 100, 'is_charger_connected': True, @@ -5367,6 +5294,7 @@ 'software_version': '11/2021.10', }), 'is_charging_plan_supported': True, + 'is_charging_settings_supported': False, 'is_lsc_enabled': True, 'is_remote_charge_start_enabled': False, 'is_remote_charge_stop_enabled': False, @@ -5400,204 +5328,297 @@ }), 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ @@ -7062,204 +7083,297 @@ 'data': None, 'fingerprint': list([ dict({ - 'content': list([ - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'BLUETOOTH', - 'bodyType': 'I20', - 'brand': 'BMW_I', - 'color': 4285537312, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'iX xDrive50', - 'softwareVersionCurrent': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 300, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S21A', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, + 'content': dict({ + 'capabilities': dict({ + 'climateFunction': 'AIR_CONDITIONING', + 'climateNow': True, + 'climateTimerTrigger': 'DEPARTURE_TIMER', + 'horn': True, + 'isBmwChargingSupported': True, + 'isCarSharingSupported': False, + 'isChargeNowForBusinessSupported': False, + 'isChargingHistorySupported': True, + 'isChargingHospitalityEnabled': False, + 'isChargingLoudnessEnabled': False, + 'isChargingPlanSupported': True, + 'isChargingPowerLimitEnabled': False, + 'isChargingSettingsEnabled': False, + 'isChargingTargetSocEnabled': False, + 'isClimateTimerSupported': True, + 'isCustomerEsimSupported': False, + 'isDCSContractManagementSupported': True, + 'isDataPrivacyEnabled': False, + 'isEasyChargeEnabled': False, + 'isEvGoChargingSupported': False, + 'isMiniChargingSupported': False, + 'isNonLscFeatureEnabled': False, + 'isRemoteEngineStartSupported': False, + 'isRemoteHistoryDeletionSupported': False, + 'isRemoteHistorySupported': True, + 'isRemoteParkingSupported': False, + 'isRemoteServicesActivationRequired': False, + 'isRemoteServicesBookingRequired': False, + 'isScanAndChargeSupported': False, + 'isSustainabilitySupported': False, + 'isWifiHotspotServiceSupported': False, + 'lastStateCallState': 'ACTIVATED', + 'lights': True, + 'lock': True, + 'remoteChargingCommands': dict({ }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ + 'sendPoi': True, + 'specialThemeSupport': list([ + ]), + 'unlock': True, + 'vehicleFinder': False, + 'vehicleStateSource': 'LAST_STATE_CALL', + }), + 'state': dict({ + 'chargingProfile': dict({ + 'chargingControlType': 'WEEKLY_PLANNER', + 'chargingMode': 'DELAYED_CHARGING', + 'chargingPreference': 'CHARGING_WINDOW', + 'chargingSettings': dict({ + 'hospitality': 'NO_ACTION', + 'idcc': 'NO_ACTION', + 'targetSoc': 100, + }), + 'climatisationOn': False, + 'departureTimes': list([ + dict({ + 'action': 'DEACTIVATE', + 'id': 1, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 35, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 2, + 'timeStamp': dict({ + 'hour': 18, + 'minute': 0, + }), + 'timerWeekDays': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 3, + 'timeStamp': dict({ + 'hour': 7, + 'minute': 0, + }), + 'timerWeekDays': list([ + ]), + }), + dict({ + 'action': 'DEACTIVATE', + 'id': 4, + 'timerWeekDays': list([ + ]), + }), ]), - 'mappingStatus': 'CONFIRMED', + 'reductionOfChargeCurrent': dict({ + 'end': dict({ + 'hour': 1, + 'minute': 30, + }), + 'start': dict({ + 'hour': 18, + 'minute': 1, + }), + }), + }), + 'checkControlMessages': list([ + ]), + 'climateTimers': list([ + dict({ + 'departureTime': dict({ + 'hour': 6, + 'minute': 40, + }), + 'isWeeklyTimer': True, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'THURSDAY', + 'SUNDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 12, + 'minute': 50, + }), + 'isWeeklyTimer': False, + 'timerAction': 'ACTIVATE', + 'timerWeekDays': list([ + 'MONDAY', + ]), + }), + dict({ + 'departureTime': dict({ + 'hour': 18, + 'minute': 59, + }), + 'isWeeklyTimer': True, + 'timerAction': 'DEACTIVATE', + 'timerWeekDays': list([ + 'WEDNESDAY', + ]), + }), + ]), + 'combustionFuelLevel': dict({ + 'range': 105, + 'remainingFuelLiters': 6, + }), + 'currentMileage': 137009, + 'doorsState': dict({ + 'combinedSecurityState': 'UNLOCKED', + 'combinedState': 'CLOSED', + 'hood': 'CLOSED', + 'leftFront': 'CLOSED', + 'leftRear': 'CLOSED', + 'rightFront': 'CLOSED', + 'rightRear': 'CLOSED', + 'trunk': 'CLOSED', + }), + 'driverPreferences': dict({ + 'lscPrivacyMode': 'OFF', + }), + 'electricChargingState': dict({ + 'chargingConnectionType': 'CONDUCTIVE', + 'chargingLevelPercent': 82, + 'chargingStatus': 'WAITING_FOR_CHARGING', + 'chargingTarget': 100, + 'isChargerConnected': True, + 'range': 174, + }), + 'isLeftSteering': True, + 'isLscSupported': True, + 'lastFetched': '2022-06-22T14:24:23.982Z', + 'lastUpdatedAt': '2022-06-22T13:58:52Z', + 'range': 174, + 'requiredServices': list([ + dict({ + 'dateTime': '2022-10-01T00:00:00.000Z', + 'description': 'Next service due by the specified date.', + 'status': 'OK', + 'type': 'BRAKE_FLUID', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next vehicle check due after the specified distance or date.', + 'status': 'OK', + 'type': 'VEHICLE_CHECK', + }), + dict({ + 'dateTime': '2023-05-01T00:00:00.000Z', + 'description': 'Next state inspection due by the specified date.', + 'status': 'OK', + 'type': 'VEHICLE_TUV', + }), + ]), + 'roofState': dict({ + 'roofState': 'CLOSED', + 'roofStateType': 'SUN_ROOF', + }), + 'windowsState': dict({ + 'combinedState': 'CLOSED', + 'leftFront': 'CLOSED', + 'rightFront': 'CLOSED', }), - 'vin': '**REDACTED**', }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G26', - 'brand': 'BMW', - 'color': 4284245350, - 'countryOfOrigin': 'DE', - 'driveTrain': 'ELECTRIC', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID8', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'i4 eDrive40', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'G026', - }), - 'telematicsUnit': 'WAVE01', - 'year': 2021, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'DEMO', - 'attributes': dict({ - 'a4aType': 'NOT_SUPPORTED', - 'bodyType': 'G20', - 'brand': 'BMW', - 'color': 4280233344, - 'countryOfOrigin': 'PT', - 'driveTrain': 'COMBUSTION', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitRaw': 'HU_MGU', - 'headUnitType': 'MGU', - 'hmiVersion': 'ID7', - 'lastFetched': '2023-01-04T14:57:06.019Z', - 'model': 'M340i xDrive', - 'softwareVersionCurrent': dict({ - 'iStep': 470, - 'puStep': dict({ - 'month': 7, - 'year': 21, - }), - 'seriesCluster': 'S18A', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 420, - 'puStep': dict({ - 'month': 7, - 'year': 20, - }), - 'seriesCluster': 'S18A', - }), - 'telematicsUnit': 'ATM2', - 'year': 2022, - }), - 'mappingInfo': dict({ - 'isAssociated': False, - 'isLmmEnabled': False, - 'isPrimaryUser': True, - 'lmmStatusReasons': list([ - ]), - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - dict({ - 'appVehicleType': 'CONNECTED', - 'attributes': dict({ - 'a4aType': 'USB_ONLY', - 'bodyType': 'I01', - 'brand': 'BMW_I', - 'color': 4284110934, - 'countryOfOrigin': 'CZ', - 'driveTrain': 'ELECTRIC_WITH_RANGE_EXTENDER', - 'driverGuideInfo': dict({ - 'androidAppScheme': 'com.bmwgroup.driversguide.row', - 'androidStoreUrl': 'https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row', - 'iosAppScheme': 'bmwdriversguide:///open', - 'iosStoreUrl': 'https://apps.apple.com/de/app/id714042749?mt=8', - }), - 'headUnitType': 'NBT', - 'hmiVersion': 'ID4', - 'lastFetched': '2022-06-01T19:48:46.540Z', - 'model': 'i3 (+ REX)', - 'softwareVersionCurrent': dict({ - 'iStep': 510, - 'puStep': dict({ - 'month': 11, - 'year': 21, - }), - 'seriesCluster': 'I001', - }), - 'softwareVersionExFactory': dict({ - 'iStep': 502, - 'puStep': dict({ - 'month': 3, - 'year': 15, - }), - 'seriesCluster': 'I001', - }), - 'year': 2015, - }), - 'mappingInfo': dict({ - 'isPrimaryUser': True, - 'mappingStatus': 'CONFIRMED', - }), - 'vin': '**REDACTED**', - }), - ]), - 'filename': 'bmw-eadrax-vcs_v4_vehicles.json', + }), + 'filename': 'bmw-eadrax-vcs_v4_vehicles_state_WBY0FINGERPRINT04.json', }), dict({ - 'content': list([ - ]), - 'filename': 'mini-eadrax-vcs_v4_vehicles.json', + 'content': dict({ + 'chargeAndClimateSettings': dict({ + 'chargeAndClimateTimer': dict({ + 'showDepartureTimers': False, + }), + }), + 'chargeAndClimateTimerDetail': dict({ + 'chargingMode': dict({ + 'chargingPreference': 'CHARGING_WINDOW', + 'endTimeSlot': '0001-01-01T01:30:00', + 'startTimeSlot': '0001-01-01T18:01:00', + 'type': 'TIME_SLOT', + }), + 'departureTimer': dict({ + 'type': 'WEEKLY_DEPARTURE_TIMER', + 'weeklyTimers': list([ + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + ]), + 'id': 1, + 'time': '0001-01-01T07:35:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ]), + 'id': 2, + 'time': '0001-01-01T18:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 3, + 'time': '0001-01-01T07:00:00', + 'timerAction': 'DEACTIVATE', + }), + dict({ + 'daysOfTheWeek': list([ + ]), + 'id': 4, + 'time': '0001-01-01T00:00:00', + 'timerAction': 'DEACTIVATE', + }), + ]), + }), + 'isPreconditionForDepartureActive': False, + }), + 'servicePack': 'TCB1', + }), + 'filename': 'bmw-eadrax-crccs_v2_vehicles_WBY0FINGERPRINT04.json', + }), + dict({ + 'content': dict({ + 'gcid': 'ceb64158-d2ca-47e9-9ee6-cbffb881434e', + 'mappingInfos': list([ + ]), + }), + 'filename': 'mini-eadrax-vcs_v5_vehicle-list.json', }), dict({ 'content': dict({ From 3351b826672f228257f232dd3122e2064db9f369 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 30 Apr 2024 15:07:15 +0200 Subject: [PATCH 72/96] Fix zoneminder async v2 (#116451) --- homeassistant/components/zoneminder/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index b4a406cec4e..e87a2b1531d 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -78,7 +78,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN][host_name] = zm_client try: - success = zm_client.login() and success + success = await hass.async_add_executor_job(zm_client.login) and success except RequestsConnectionError as ex: _LOGGER.error( "ZoneMinder connection failure to %s: %s", From c77cef039107d67e44a1dfb2f19d6befdd7ff759 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 11:53:55 -0500 Subject: [PATCH 73/96] Bump bluetooth-adapters to 0.19.1 (#116465) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ed1e11d8ddd..4bb84ab6dc3 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -16,7 +16,7 @@ "requirements": [ "bleak==0.21.1", "bleak-retry-connector==3.5.0", - "bluetooth-adapters==0.19.0", + "bluetooth-adapters==0.19.1", "bluetooth-auto-recovery==1.4.2", "bluetooth-data-tools==1.19.0", "dbus-fast==2.21.1", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a2eb0f1254c..4ba38346e83 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 bleak-retry-connector==3.5.0 bleak==0.21.1 -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 bluetooth-auto-recovery==1.4.2 bluetooth-data-tools==1.19.0 cached_ipaddress==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index 04097f0a9bc..8d926776063 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -579,7 +579,7 @@ bluemaestro-ble==0.2.3 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbc60f01547..ab140526378 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ bluecurrent-api==1.2.3 bluemaestro-ble==0.2.3 # homeassistant.components.bluetooth -bluetooth-adapters==0.19.0 +bluetooth-adapters==0.19.1 # homeassistant.components.bluetooth bluetooth-auto-recovery==1.4.2 From 3d13345575d490565001e72e5d8dcb513fd34cd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:31:40 -0500 Subject: [PATCH 74/96] Ensure MQTT resubscribes happen before birth message (#116471) --- homeassistant/components/mqtt/client.py | 56 +++++++++++++++---------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7f58a21a1f1..74fa8fb3302 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -878,6 +878,22 @@ class MQTT: await self._wait_for_mid(mid) + async def _async_resubscribe_and_publish_birth_message( + self, birth_message: PublishMessage + ) -> None: + """Resubscribe to all topics and publish birth message.""" + await self._async_perform_subscriptions() + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + # Update subscribe cooldown period to a shorter time + self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + @callback def _async_mqtt_on_connect( self, @@ -919,36 +935,33 @@ class MQTT: result_code, ) - self.hass.async_create_task(self._async_resubscribe()) - + self._async_queue_resubscribe() + birth: dict[str, Any] if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): - - async def publish_birth_message(birth_message: PublishMessage) -> None: - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - # Update subscribe cooldown period to a shorter time - self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - birth_message = PublishMessage(**birth) self.config_entry.async_create_background_task( self.hass, - publish_birth_message(birth_message), - name="mqtt birth message", + self._async_resubscribe_and_publish_birth_message(birth_message), + name="mqtt re-subscribe and birth", ) else: # Update subscribe cooldown period to a shorter time + self.config_entry.async_create_background_task( + self.hass, + self._async_perform_subscriptions(), + name="mqtt re-subscribe", + ) self._subscribe_debouncer.set_timeout(SUBSCRIBE_COOLDOWN) self._async_connection_result(True) - async def _async_resubscribe(self) -> None: - """Resubscribe on reconnect.""" + @callback + def _async_queue_resubscribe(self) -> None: + """Queue subscriptions on reconnect. + + self._async_perform_subscriptions must be called + after this method to actually subscribe. + """ self._max_qos.clear() self._retained_topics.clear() # Group subscriptions to only re-subscribe once for each topic. @@ -963,7 +976,6 @@ class MQTT: ], queue_only=True, ) - await self._async_perform_subscriptions() @lru_cache(None) # pylint: disable=method-cache-max-size-none def _matching_subscriptions(self, topic: str) -> list[Subscription]: @@ -1052,7 +1064,9 @@ class MQTT: # The callback signature for on_unsubscribe is different from on_subscribe # see https://github.com/eclipse/paho.mqtt.python/issues/687 # properties and reasoncodes are not used in Home Assistant - self.hass.async_create_task(self._mqtt_handle_mid(mid)) + self.config_entry.async_create_task( + self.hass, self._mqtt_handle_mid(mid), name=f"mqtt handle mid {mid}" + ) async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid From c574d86ddbafd6c18995ad9efb297fda3ce4292c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:31:09 -0500 Subject: [PATCH 75/96] Fix local_todo blocking the event loop (#116473) --- homeassistant/components/local_todo/todo.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 5b25abf8e21..ccd3d8db759 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util import dt as dt_util from .const import CONF_TODO_LIST_NAME, DOMAIN @@ -67,9 +68,16 @@ async def async_setup_entry( ) -> None: """Set up the local_todo todo platform.""" - store = hass.data[DOMAIN][config_entry.entry_id] + store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id] ics = await store.async_load() - calendar = IcsCalendarStream.calendar_from_ics(ics) + + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # calendar_from_ics will dynamically load packages + # the first time it is called, so we need to do it + # in a separate thread to avoid blocking the event loop + calendar: Calendar = await hass.async_add_import_executor_job( + IcsCalendarStream.calendar_from_ics, ics + ) migrated = _migrate_calendar(calendar) calendar.prodid = PRODID From c54d53b88a1dd5f4dea719924d9e395bb6b51060 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 30 Apr 2024 16:47:17 -0400 Subject: [PATCH 76/96] Change SkyConnect integration type back to `hardware` and fix multi-PAN migration bug (#116474) Co-authored-by: Joost Lekkerkerker --- .../homeassistant_sky_connect/config_flow.py | 15 ++++++++ .../homeassistant_sky_connect/manifest.json | 2 +- homeassistant/generated/integrations.json | 5 --- .../test_config_flow.py | 38 +++++++++++++++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 6ffb2783165..9d0aa902cc4 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -597,6 +597,21 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler( """Return the name of the hardware.""" return self._hw_variant.full_name + async def async_step_flashing_complete( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Finish flashing and update the config entry.""" + self.hass.config_entries.async_update_entry( + entry=self.config_entry, + data={ + **self.config_entry.data, + "firmware": ApplicationType.EZSP.value, + }, + options=self.config_entry.options, + ) + + return await super().async_step_flashing_complete(user_input) + class HomeAssistantSkyConnectOptionsFlowHandler( BaseFirmwareInstallFlow, OptionsFlowWithConfigEntry diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index c90ea2c075f..f56fd24de61 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["hardware", "usb", "homeassistant_hardware"], "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", - "integration_type": "device", + "integration_type": "hardware", "usb": [ { "vid": "10C4", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index cf5f352f22c..e6a103989d1 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2565,11 +2565,6 @@ "integration_type": "virtual", "supported_by": "netatmo" }, - "homeassistant_sky_connect": { - "name": "Home Assistant SkyConnect", - "integration_type": "device", - "config_flow": true - }, "homematic": { "name": "Homematic", "integrations": { diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index c34e3ebe186..611dda4a917 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -11,6 +11,8 @@ from universal_silabs_flasher.const import ApplicationType from homeassistant.components import usb from homeassistant.components.hassio.addon_manager import AddonInfo, AddonState from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + CONF_DISABLE_MULTI_PAN, + get_flasher_addon_manager, get_multiprotocol_addon_manager, ) from homeassistant.components.homeassistant_sky_connect.config_flow import ( @@ -869,11 +871,25 @@ async def test_options_flow_multipan_uninstall( version="1.0.0", ) + mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass)) + mock_flasher_manager.async_get_addon_info.return_value = AddonInfo( + available=True, + hostname=None, + options={}, + state=AddonState.NOT_RUNNING, + update_available=False, + version="1.0.0", + ) + with ( patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager", return_value=mock_multipan_manager, ), + patch( + "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager", + return_value=mock_flasher_manager, + ), patch( "homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio", return_value=True, @@ -883,3 +899,25 @@ async def test_options_flow_multipan_uninstall( assert result["type"] is FlowResultType.MENU assert result["step_id"] == "addon_menu" assert "uninstall_addon" in result["menu_options"] + + # Pick the uninstall option + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"next_step_id": "uninstall_addon"}, + ) + + # Check the box + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True} + ) + + # Finish the flow + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + await hass.async_block_till_done(wait_background_tasks=True) + result = await hass.config_entries.options.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.CREATE_ENTRY + + # We've reverted the firmware back to Zigbee + assert config_entry.data["firmware"] == "ezsp" From 6971898a43deaefa94c0ad3e46864be90aaae819 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 15:47:27 -0500 Subject: [PATCH 77/96] Fix non-thread-safe operation in roon volume callback (#116475) --- homeassistant/components/roon/event.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/roon/event.py b/homeassistant/components/roon/event.py index ea5014c8755..073b58160f6 100644 --- a/homeassistant/components/roon/event.py +++ b/homeassistant/components/roon/event.py @@ -72,7 +72,6 @@ class RoonEventEntity(EventEntity): via_device=(DOMAIN, self._server.roon_id), ) - @callback def _roonapi_volume_callback( self, control_key: str, event: str, value: int ) -> None: @@ -88,7 +87,7 @@ class RoonEventEntity(EventEntity): event = "volume_down" self._trigger_event(event) - self.async_write_ha_state() + self.schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Register volume hooks with the roon api.""" From 3d86577cabc2a6e645055e7ecc3d09db258317e0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 1 May 2024 01:15:46 +0200 Subject: [PATCH 78/96] Add test MQTT subscription is completed when birth message is sent (#116476) --- tests/components/mqtt/test_init.py | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cfb8ce7ac04..f948889fd80 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2534,6 +2534,75 @@ async def test_delayed_birth_message( ) +@pytest.mark.parametrize( + "mqtt_config_entry_data", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 0, + mqtt.ATTR_RETAIN: False, + }, + } + ], +) +async def test_subscription_done_when_birth_message_is_sent( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_config_entry_data, + mqtt_mock_entry: MqttMockHAClientGenerator, +) -> None: + """Test sending birth message until initial subscription has been completed.""" + mqtt_mock = await mqtt_mock_entry() + + hass.set_state(CoreState.starting) + birth = asyncio.Event() + + await hass.async_block_till_done() + + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mqtt_component_mock = MagicMock( + return_value=hass.data["mqtt"].client, + wraps=hass.data["mqtt"].client, + ) + mqtt_component_mock._mqttc = mqtt_client_mock + + hass.data["mqtt"].client = mqtt_component_mock + mqtt_mock = hass.data["mqtt"].client + mqtt_mock.reset_mock() + + @callback + def wait_birth(msg: ReceiveMessage) -> None: + """Handle birth message.""" + birth.set() + + mqtt_client_mock.reset_mock() + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0): + await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) + mqtt_client_mock.on_connect(None, None, 0, 0) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + # We wait until we receive a birth message + await asyncio.wait_for(birth.wait(), 1) + # Assert we already have subscribed at the client + # for new config payloads at the time we the birth message is received + assert ("homeassistant/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + assert ("homeassistant/+/+/+/config", 0) in help_all_subscribe_calls( + mqtt_client_mock + ) + mqtt_client_mock.publish.assert_called_with( + "homeassistant/status", "online", 0, False + ) + + @pytest.mark.parametrize( "mqtt_config_entry_data", [ From ac241057772073ed51f7816e064b74aef45326c1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 May 2024 00:46:25 +0200 Subject: [PATCH 79/96] Update frontend to 20240430.0 (#116481) --- 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 e271903a27d..aa1d8ee3d3c 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==20240429.0"] + "requirements": ["home-assistant-frontend==20240430.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4ba38346e83..afb7d894a51 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8d926776063..6e2b7a55a13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab140526378..2d7fe7158bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240429.0 +home-assistant-frontend==20240430.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 7d51556e1ea06dba2892f36541666220d79dc3ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Apr 2024 18:47:12 -0500 Subject: [PATCH 80/96] Hold a lock to prevent concurrent setup of config entries (#116482) --- homeassistant/config_entries.py | 30 +++-- homeassistant/setup.py | 2 +- .../androidtv_remote/test_config_flow.py | 3 + .../components/config/test_config_entries.py | 5 + tests/components/mqtt/test_init.py | 1 + tests/components/opower/test_config_flow.py | 2 + tests/test_config_entries.py | 104 +++++++++++++++++- 7 files changed, 129 insertions(+), 18 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 619b2a4b48a..f982f63b948 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -292,7 +292,7 @@ class ConfigEntry: update_listeners: list[UpdateListenerType] _async_cancel_retry_setup: Callable[[], Any] | None _on_unload: list[Callable[[], Coroutine[Any, Any, None] | None]] | None - reload_lock: asyncio.Lock + setup_lock: asyncio.Lock _reauth_lock: asyncio.Lock _reconfigure_lock: asyncio.Lock _tasks: set[asyncio.Future[Any]] @@ -400,7 +400,7 @@ class ConfigEntry: _setter(self, "_on_unload", None) # Reload lock to prevent conflicting reloads - _setter(self, "reload_lock", asyncio.Lock()) + _setter(self, "setup_lock", asyncio.Lock()) # Reauth lock to prevent concurrent reauth flows _setter(self, "_reauth_lock", asyncio.Lock()) # Reconfigure lock to prevent concurrent reconfigure flows @@ -699,19 +699,17 @@ class ConfigEntry: # has started so we do not block shutdown if not hass.is_stopping: hass.async_create_background_task( - self._async_setup_retry(hass), + self.async_setup_locked(hass), f"config entry retry {self.domain} {self.title}", eager_start=True, ) - async def _async_setup_retry(self, hass: HomeAssistant) -> None: - """Retry setup. - - We hold the reload lock during setup retry to ensure - that nothing can reload the entry while we are retrying. - """ - async with self.reload_lock: - await self.async_setup(hass) + async def async_setup_locked( + self, hass: HomeAssistant, integration: loader.Integration | None = None + ) -> None: + """Set up while holding the setup lock.""" + async with self.setup_lock: + await self.async_setup(hass, integration=integration) @callback def async_shutdown(self) -> None: @@ -1791,7 +1789,15 @@ class ConfigEntries: # attempts. entry.async_cancel_retry_setup() - async with entry.reload_lock: + if entry.domain not in self.hass.config.components: + # If the component is not loaded, just load it as + # the config entry will be loaded as well. We need + # to do this before holding the lock to avoid a + # deadlock. + await async_setup_component(self.hass, entry.domain, self._hass_config) + return entry.state is ConfigEntryState.LOADED + + async with entry.setup_lock: unload_result = await self.async_unload(entry_id) if not unload_result or entry.disabled_by: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 7ba51b644e5..86df6417169 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -449,7 +449,7 @@ async def _async_setup_component( await asyncio.gather( *( create_eager_task( - entry.async_setup(hass, integration=integration), + entry.async_setup_locked(hass, integration=integration), name=f"config entry setup {entry.title} {entry.domain} {entry.entry_id}", ) for entry in entries diff --git a/tests/components/androidtv_remote/test_config_flow.py b/tests/components/androidtv_remote/test_config_flow.py index 8778630be8d..062b9a4a55c 100644 --- a/tests/components/androidtv_remote/test_config_flow.py +++ b/tests/components/androidtv_remote/test_config_flow.py @@ -324,6 +324,7 @@ async def test_user_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -640,6 +641,7 @@ async def test_zeroconf_flow_already_configured_host_changed_reloads_entry( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -769,6 +771,7 @@ async def test_reauth_flow_success( state=ConfigEntryState.LOADED, ) mock_config_entry.add_to_hass(hass) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index dd46921c339..87c712b3716 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -251,6 +251,7 @@ async def test_reload_entry(hass: HomeAssistant, client) -> None: domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -298,6 +299,7 @@ async def test_reload_entry_in_failed_state( """Test reloading an entry via the API that has already failed to unload.""" entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) entry.add_to_hass(hass) + hass.config.components.add("demo") resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) @@ -326,6 +328,7 @@ async def test_reload_entry_in_setup_retry( entry = MockConfigEntry(domain="comp", state=core_ce.ConfigEntryState.SETUP_RETRY) entry.supports_unload = True entry.add_to_hass(hass) + hass.config.components.add("comp") with patch.dict(HANDLERS, {"comp": ConfigFlow, "test": ConfigFlow}): resp = await client.post( @@ -1109,6 +1112,7 @@ async def test_update_prefrences( domain="kitchen_sink", state=core_ce.ConfigEntryState.LOADED ) entry.add_to_hass(hass) + hass.config.components.add("kitchen_sink") assert entry.pref_disable_new_entities is False assert entry.pref_disable_polling is False @@ -1209,6 +1213,7 @@ async def test_disable_entry( ) entry.add_to_hass(hass) assert entry.disabled_by is None + hass.config.components.add("kitchen_sink") # Disable await ws_client.send_json( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index f948889fd80..a1264b52739 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1873,6 +1873,7 @@ async def test_reload_entry_with_restored_subscriptions( # Setup the MQTT entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) + hass.config.components.add(mqtt.DOMAIN) mqtt_client_mock.connect.return_value = 0 with patch("homeassistant.config.load_yaml_config_file", return_value={}): await entry.async_setup(hass) diff --git a/tests/components/opower/test_config_flow.py b/tests/components/opower/test_config_flow.py index 512a602a043..18a7caf23df 100644 --- a/tests/components/opower/test_config_flow.py +++ b/tests/components/opower/test_config_flow.py @@ -279,6 +279,7 @@ async def test_form_valid_reauth( ) -> None: """Test that we can handle a valid reauth.""" mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() @@ -328,6 +329,7 @@ async def test_form_valid_reauth_with_mfa( }, ) mock_config_entry.mock_state(hass, ConfigEntryState.LOADED) + hass.config.components.add(DOMAIN) mock_config_entry.async_start_reauth(hass) await hass.async_block_till_done() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 68f770631ed..8d7efad8918 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -825,7 +825,7 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: "error_reason_translation_placeholders", "_async_cancel_retry_setup", "_on_unload", - "reload_lock", + "setup_lock", "_reauth_lock", "_tasks", "_background_tasks", @@ -1632,7 +1632,6 @@ async def test_entry_reload_succeed( mock_platform(hass, "comp.config_flow", None) assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1707,6 +1706,8 @@ async def test_entry_reload_error( ), ) + hass.config.components.add("comp") + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): assert await manager.async_reload(entry.entry_id) @@ -1738,8 +1739,11 @@ async def test_entry_disable_succeed( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER ) @@ -1751,7 +1755,7 @@ async def test_entry_disable_succeed( # Enable assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_unload_entry.mock_calls) == 1 - assert len(async_setup.mock_calls) == 1 + assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 1 assert entry.state is config_entries.ConfigEntryState.LOADED @@ -1775,6 +1779,7 @@ async def test_entry_disable_without_reload_support( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") # Disable assert not await manager.async_set_disabled_by( @@ -1951,7 +1956,7 @@ async def test_reload_entry_entity_registry_works( ) await hass.async_block_till_done() - assert len(mock_unload_entry.mock_calls) == 2 + assert len(mock_unload_entry.mock_calls) == 1 async def test_unique_id_persisted( @@ -3392,6 +3397,7 @@ async def test_entry_reload_calls_on_unload_listeners( ), ) mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") mock_unload_callback = Mock() @@ -3944,8 +3950,9 @@ async def test_deprecated_disabled_by_str_set( caplog: pytest.LogCaptureFixture, ) -> None: """Test deprecated str set disabled_by enumizes and logs a warning.""" - entry = MockConfigEntry() + entry = MockConfigEntry(domain="comp") entry.add_to_manager(manager) + hass.config.components.add("comp") assert await manager.async_set_disabled_by( entry.entry_id, config_entries.ConfigEntryDisabler.USER.value ) @@ -3963,6 +3970,47 @@ async def test_entry_reload_concurrency( async_setup = AsyncMock(return_value=True) loaded = 1 + async def _async_setup_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded += 1 + return loaded == 1 + + async def _async_unload_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded -= 1 + return loaded == 0 + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=_async_setup_entry, + async_unload_entry=_async_unload_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + hass.config.components.add("comp") + tasks = [ + asyncio.create_task(manager.async_reload(entry.entry_id)) for _ in range(15) + ] + await asyncio.gather(*tasks) + assert entry.state is config_entries.ConfigEntryState.LOADED + assert loaded == 1 + + +async def test_entry_reload_concurrency_not_setup_setup( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test multiple reload calls do not cause a reload race.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + loaded = 0 + async def _async_setup_entry(*args, **kwargs): await asyncio.sleep(0) nonlocal loaded @@ -4074,6 +4122,7 @@ async def test_disallow_entry_reload_with_setup_in_progress( domain="comp", state=config_entries.ConfigEntryState.SETUP_IN_PROGRESS ) entry.add_to_hass(hass) + hass.config.components.add("comp") with pytest.raises( config_entries.OperationNotAllowed, @@ -5016,3 +5065,48 @@ async def test_updating_non_added_entry_raises(hass: HomeAssistant) -> None: with pytest.raises(config_entries.UnknownEntry, match=entry.entry_id): hass.config_entries.async_update_entry(entry, unique_id="new_id") + + +async def test_reload_during_setup(hass: HomeAssistant) -> None: + """Test reload during setup waits.""" + entry = MockConfigEntry(domain="comp", data={"value": "initial"}) + entry.add_to_hass(hass) + + setup_start_future = hass.loop.create_future() + setup_finish_future = hass.loop.create_future() + in_setup = False + setup_calls = 0 + + async def mock_async_setup_entry(hass, entry): + """Mock setting up an entry.""" + nonlocal in_setup + nonlocal setup_calls + setup_calls += 1 + assert not in_setup + in_setup = True + setup_start_future.set_result(None) + await setup_finish_future + in_setup = False + return True + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_async_setup_entry, + async_unload_entry=AsyncMock(return_value=True), + ), + ) + mock_platform(hass, "comp.config_flow", None) + + setup_task = hass.async_create_task(async_setup_component(hass, "comp", {})) + + await setup_start_future # ensure we are in the setup + reload_task = hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + await asyncio.sleep(0) + setup_finish_future.set_result(None) + await setup_task + await reload_task + assert setup_calls == 2 From 31cfabc44d8c340757865c6194c5f9b00173c306 Mon Sep 17 00:00:00 2001 From: Luke Lashley Date: Wed, 1 May 2024 05:56:02 -0400 Subject: [PATCH 81/96] Fix roborock image crashes (#116487) --- .../components/roborock/coordinator.py | 2 +- homeassistant/components/roborock/image.py | 31 ++++++++-- tests/components/roborock/conftest.py | 4 ++ tests/components/roborock/test_image.py | 62 +++++++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/roborock/coordinator.py b/homeassistant/components/roborock/coordinator.py index 293415360bd..32b7a487ac8 100644 --- a/homeassistant/components/roborock/coordinator.py +++ b/homeassistant/components/roborock/coordinator.py @@ -49,7 +49,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]): ) device_data = DeviceData(device, product_info.model, device_networking.ip) self.api: RoborockLocalClientV1 | RoborockMqttClientV1 = RoborockLocalClientV1( - device_data + device_data, queue_timeout=5 ) self.cloud_api = cloud_api self.device_info = DeviceInfo( diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 775ab98fd59..2aef39ce59b 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -66,17 +66,26 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): ) self._attr_image_last_updated = dt_util.utcnow() self.map_flag = map_flag - self.cached_map = self._create_image(starting_map) + try: + self.cached_map = self._create_image(starting_map) + except HomeAssistantError: + # If we failed to update the image on init, we set cached_map to empty bytes so that we are unavailable and can try again later. + self.cached_map = b"" self._attr_entity_category = EntityCategory.DIAGNOSTIC + @property + def available(self): + """Determines if the entity is available.""" + return self.cached_map != b"" + @property def is_selected(self) -> bool: """Return if this map is the currently selected map.""" return self.map_flag == self.coordinator.current_map def is_map_valid(self) -> bool: - """Update this map if it is the current active map, and the vacuum is cleaning.""" - return ( + """Update this map if it is the current active map, and the vacuum is cleaning or if it has never been set at all.""" + return self.cached_map == b"" or ( self.is_selected and self.image_last_updated is not None and self.coordinator.roborock_device_info.props.status is not None @@ -96,7 +105,16 @@ class RoborockMap(RoborockCoordinatedEntity, ImageEntity): async def async_image(self) -> bytes | None: """Update the image if it is not cached.""" if self.is_map_valid(): - map_data: bytes = await self.cloud_api.get_map_v1() + response = await asyncio.gather( + *(self.cloud_api.get_map_v1(), self.coordinator.get_rooms()), + return_exceptions=True, + ) + if not isinstance(response[0], bytes): + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="map_failure", + ) + map_data = response[0] self.cached_map = self._create_image(map_data) return self.cached_map @@ -141,9 +159,10 @@ async def create_coordinator_maps( await asyncio.sleep(MAP_SLEEP) # Get the map data map_update = await asyncio.gather( - *[coord.cloud_api.get_map_v1(), coord.get_rooms()] + *[coord.cloud_api.get_map_v1(), coord.get_rooms()], return_exceptions=True ) - api_data: bytes = map_update[0] + # If we fail to get the map -> We should set it to empty byte, still create it, and set it as unavailable. + api_data: bytes = map_update[0] if isinstance(map_update[0], bytes) else b"" entities.append( RoborockMap( f"{slugify(coord.roborock_device_info.device.duid)}_map_{map_info.name}", diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 0f3689da161..d3bb0a221b1 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -91,6 +91,10 @@ def bypass_api_fixture() -> None: RoomMapping(18, "2362041"), ], ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + return_value=b"123", + ), ): yield diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 445f90f4a05..bc45c6dec05 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -5,7 +5,12 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from roborock import RoborockException + +from homeassistant.components.roborock import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed @@ -82,3 +87,60 @@ async def test_floorplan_image_failed_parse( async_fire_time_changed(hass, now) resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") assert not resp.ok + + +async def test_fail_parse_on_startup( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_roborock_entry: MockConfigEntry, + bypass_api_fixture, +) -> None: + """Test that if we fail parsing on startup, we create the entity but set it as unavailable.""" + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + with patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert ( + image_entity := hass.states.get("image.roborock_s7_maxv_upstairs") + ) is not None + assert image_entity.state == STATE_UNAVAILABLE + + +async def test_fail_updating_image( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test that we handle failing getting the image after it has already been setup..""" + client = await hass_client() + map_data = copy.deepcopy(MAP_DATA) + map_data.image = None + now = dt_util.utcnow() + timedelta(seconds=91) + # Copy the device prop so we don't override it + prop = copy.deepcopy(PROP) + prop.status.in_cleaning = 1 + # Update image, but get none for parse image. + with ( + patch( + "homeassistant.components.roborock.image.RoborockMapDataParser.parse", + return_value=map_data, + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockLocalClientV1.get_prop", + return_value=prop, + ), + patch( + "homeassistant.components.roborock.image.dt_util.utcnow", return_value=now + ), + patch( + "homeassistant.components.roborock.coordinator.RoborockMqttClientV1.get_map_v1", + side_effect=RoborockException, + ), + ): + async_fire_time_changed(hass, now) + resp = await client.get("/api/image_proxy/image.roborock_s7_maxv_upstairs") + assert not resp.ok From fabc3d751e01f4043a0c39c378fd3383bc80f700 Mon Sep 17 00:00:00 2001 From: max2697 <143563471+max2697@users.noreply.github.com> Date: Wed, 1 May 2024 00:11:47 -0500 Subject: [PATCH 82/96] Bump opower to 0.4.4 (#116489) --- 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 51ad669733b..91e4fbc960c 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.3"] + "requirements": ["opower==0.4.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6e2b7a55a13..2b9fbebcd8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d7fe7158bf..28f1ffb7d62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1183,7 +1183,7 @@ openhomedevice==2.2.0 openwebifpy==4.2.4 # homeassistant.components.opower -opower==0.4.3 +opower==0.4.4 # homeassistant.components.oralb oralb-ble==0.17.6 From ad16c5bc254d8e95c4e3e14de6de9bbcf71e474b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 May 2024 12:53:45 +0200 Subject: [PATCH 83/96] Update frontend to 20240501.0 (#116503) --- 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 aa1d8ee3d3c..6abe8df1d7c 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==20240430.0"] + "requirements": ["home-assistant-frontend==20240501.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afb7d894a51..b1c0391022a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ habluetooth==2.8.0 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 home-assistant-intents==2024.4.24 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b9fbebcd8b..e23a81ccb4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1078,7 +1078,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28f1ffb7d62..2393cdd9db1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ hole==0.8.0 holidays==0.47 # homeassistant.components.frontend -home-assistant-frontend==20240430.0 +home-assistant-frontend==20240501.0 # homeassistant.components.conversation home-assistant-intents==2024.4.24 From 0eb734b6bfbfad902616fab30918d86bed126130 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 13:41:34 +0200 Subject: [PATCH 84/96] Bump version to 2024.5.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3a0d35b8324..1d0486c75c7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index a04e9fd218a..7dcbc5afdd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b4" +version = "2024.5.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f89677cd763762f28206440a042006c2e9c9a428 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 1 May 2024 10:00:17 -0400 Subject: [PATCH 85/96] Bump ZHA dependencies (#116509) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 452f11db85b..b1511b2f5bb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "universal_silabs_flasher" ], "requirements": [ - "bellows==0.38.2", + "bellows==0.38.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.115", diff --git a/requirements_all.txt b/requirements_all.txt index e23a81ccb4d..8c653ec38e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -541,7 +541,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.zha -bellows==0.38.2 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2393cdd9db1..9a0477cea8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.zha -bellows==0.38.2 +bellows==0.38.3 # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.2 From 4312f36dbe010ac1f9e087e900731cabcc57b5b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 08:23:33 -0500 Subject: [PATCH 86/96] Fix non-thread-safe operations in ihc (#116513) --- homeassistant/components/ihc/service_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ihc/service_functions.py b/homeassistant/components/ihc/service_functions.py index cfd91f0960c..61eba4791ac 100644 --- a/homeassistant/components/ihc/service_functions.py +++ b/homeassistant/components/ihc/service_functions.py @@ -90,24 +90,24 @@ def setup_service_functions(hass: HomeAssistant) -> None: ihc_controller = _get_controller(call) await async_pulse(hass, ihc_controller, ihc_id) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_BOOL, async_set_runtime_value_bool, schema=SET_RUNTIME_VALUE_BOOL_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_INT, async_set_runtime_value_int, schema=SET_RUNTIME_VALUE_INT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_SET_RUNTIME_VALUE_FLOAT, async_set_runtime_value_float, schema=SET_RUNTIME_VALUE_FLOAT_SCHEMA, ) - hass.services.async_register( + hass.services.register( DOMAIN, SERVICE_PULSE, async_pulse_runtime_input, schema=PULSE_SCHEMA ) From 082721e1ab0a1aefd71469e38b9bd44801b77662 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 15:42:53 +0200 Subject: [PATCH 87/96] Bump python matter server library to 5.10.0 (#116514) --- homeassistant/components/matter/entity.py | 3 --- homeassistant/components/matter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index dcb3586934b..a47147e874a 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -107,9 +107,6 @@ class MatterEntity(Entity): attr_path_filter=attr_path, ) ) - await self.matter_client.subscribe_attribute( - self._endpoint.node.node_id, sub_paths - ) # subscribe to node (availability changes) self._unsubscribes.append( self.matter_client.subscribe_events( diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index b3acc0d547c..20988e387fe 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -6,6 +6,6 @@ "dependencies": ["websocket_api"], "documentation": "https://www.home-assistant.io/integrations/matter", "iot_class": "local_push", - "requirements": ["python-matter-server==5.7.0"], + "requirements": ["python-matter-server==5.10.0"], "zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 8c653ec38e1..f391511e607 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2257,7 +2257,7 @@ python-kasa[speedups]==0.6.2.1 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a0477cea8b..140741518d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1751,7 +1751,7 @@ python-juicenet==1.1.0 python-kasa[speedups]==0.6.2.1 # homeassistant.components.matter -python-matter-server==5.7.0 +python-matter-server==5.10.0 # homeassistant.components.xiaomi_miio python-miio==0.5.12 From 780a6b314ff39d3e6caec5edbbbcaea99217ed0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 08:22:50 -0500 Subject: [PATCH 88/96] Fix blocking I/O to import modules in mysensors (#116516) --- homeassistant/components/mysensors/gateway.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 0a037dfce31..11f27f8a108 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -10,7 +10,7 @@ import socket import sys from typing import Any -from mysensors import BaseAsyncGateway, Message, Sensor, mysensors +from mysensors import BaseAsyncGateway, Message, Sensor, get_const, mysensors import voluptuous as vol from homeassistant.components.mqtt import ( @@ -24,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.setup import SetupPhases, async_pause_setup from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( @@ -162,6 +163,12 @@ async def _get_gateway( ) -> BaseAsyncGateway | None: """Return gateway after setup of the gateway.""" + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # get_const will import a const module based on the version + # so we need to import it here to avoid it being imported + # in the event loop + await hass.async_add_import_executor_job(get_const, version) + if persistence_file is not None: # Interpret relative paths to be in hass config folder. # Absolute paths will be left as they are. From 15aa8949eee2b6f43d44d9e76e87e5ea05d497a4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 1 May 2024 16:22:25 +0200 Subject: [PATCH 89/96] Improve scrape strings (#116519) --- homeassistant/components/scrape/strings.json | 42 +++++++++++--------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json index 217e69b27df..9b534aed77b 100644 --- a/homeassistant/components/scrape/strings.json +++ b/homeassistant/components/scrape/strings.json @@ -21,13 +21,13 @@ "encoding": "Character encoding" }, "data_description": { - "resource": "The URL to the website that contains the value", - "authentication": "Type of the HTTP authentication. Either basic or digest", - "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", - "headers": "Headers to use for the web request", - "timeout": "Timeout for connection to website", - "encoding": "Character encoding to use. Defaults to UTF-8", - "payload": "Payload to use when method is POST" + "resource": "The URL to the website that contains the value.", + "authentication": "Type of the HTTP authentication. Either basic or digest.", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed.", + "headers": "Headers to use for the web request.", + "timeout": "Timeout for connection to website.", + "encoding": "Character encoding to use. Defaults to UTF-8.", + "payload": "Payload to use when method is POST." } }, "sensor": { @@ -36,19 +36,21 @@ "attribute": "Attribute", "index": "Index", "select": "Select", - "value_template": "Value Template", - "device_class": "Device Class", - "state_class": "State Class", - "unit_of_measurement": "Unit of Measurement" + "value_template": "Value template", + "availability": "Availability template", + "device_class": "Device class", + "state_class": "State class", + "unit_of_measurement": "Unit of measurement" }, "data_description": { - "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", - "attribute": "Get value of an attribute on the selected tag", - "index": "Defines which of the elements returned by the CSS selector to use", - "value_template": "Defines a template to get the state of the sensor", - "device_class": "The type/class of the sensor to set the icon in the frontend", - "state_class": "The state_class of the sensor", - "unit_of_measurement": "Choose temperature measurement or create your own" + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details.", + "attribute": "Get value of an attribute on the selected tag.", + "index": "Defines which of the elements returned by the CSS selector to use.", + "value_template": "Defines a template to get the state of the sensor.", + "availability": "Defines a template to get the availability of the sensor.", + "device_class": "The type/class of the sensor to set the icon in the frontend.", + "state_class": "The state_class of the sensor.", + "unit_of_measurement": "Choose unit of measurement or create your own." } } } @@ -70,6 +72,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -79,6 +82,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" @@ -91,6 +95,7 @@ "index": "[%key:component::scrape::config::step::sensor::data::index%]", "select": "[%key:component::scrape::config::step::sensor::data::select%]", "value_template": "[%key:component::scrape::config::step::sensor::data::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data::unit_of_measurement%]" @@ -100,6 +105,7 @@ "attribute": "[%key:component::scrape::config::step::sensor::data_description::attribute%]", "index": "[%key:component::scrape::config::step::sensor::data_description::index%]", "value_template": "[%key:component::scrape::config::step::sensor::data_description::value_template%]", + "availability": "[%key:component::scrape::config::step::sensor::data_description::availability%]", "device_class": "[%key:component::scrape::config::step::sensor::data_description::device_class%]", "state_class": "[%key:component::scrape::config::step::sensor::data_description::state_class%]", "unit_of_measurement": "[%key:component::scrape::config::step::sensor::data_description::unit_of_measurement%]" From 1e4e891f0b95b373e6c593935721d906adc86e70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 16:24:03 +0200 Subject: [PATCH 90/96] Bump version to 2024.5.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1d0486c75c7..3c3787c7e80 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 7dcbc5afdd5..118f2f91d2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b5" +version = "2024.5.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b42f3671288c9b7948792886c2051b34d6927597 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 May 2024 18:30:59 +0200 Subject: [PATCH 91/96] Add blocklist for known Matter devices with faulty transitions (#116524) --- homeassistant/components/matter/light.py | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index c9556fd2e2e..9d80ebc38f6 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -43,6 +43,18 @@ COLOR_MODE_MAP = { } DEFAULT_TRANSITION = 0.2 +# there's a bug in (at least) Espressif's implementation of light transitions +# on devices based on Matter 1.0. Mark potential devices with this issue. +# https://github.com/home-assistant/core/issues/113775 +# vendorid (attributeKey 0/40/2) +# productid (attributeKey 0/40/4) +# hw version (attributeKey 0/40/8) +# sw version (attributeKey 0/40/10) +TRANSITION_BLOCKLIST = ( + (4488, 514, "1.0", "1.0.0"), + (5010, 769, "3.0", "1.0.0"), +) + async def async_setup_entry( hass: HomeAssistant, @@ -61,6 +73,7 @@ class MatterLight(MatterEntity, LightEntity): _supports_brightness = False _supports_color = False _supports_color_temperature = False + _transitions_disabled = False async def _set_xy_color( self, xy_color: tuple[float, float], transition: float = 0.0 @@ -260,6 +273,8 @@ class MatterLight(MatterEntity, LightEntity): color_temp = kwargs.get(ATTR_COLOR_TEMP) brightness = kwargs.get(ATTR_BRIGHTNESS) transition = kwargs.get(ATTR_TRANSITION, DEFAULT_TRANSITION) + if self._transitions_disabled: + transition = 0 if self.supported_color_modes is not None: if hs_color is not None and ColorMode.HS in self.supported_color_modes: @@ -336,8 +351,12 @@ class MatterLight(MatterEntity, LightEntity): supported_color_modes = filter_supported_color_modes(supported_color_modes) self._attr_supported_color_modes = supported_color_modes + self._check_transition_blocklist() # flag support for transition as soon as we support setting brightness and/or color - if supported_color_modes != {ColorMode.ONOFF}: + if ( + supported_color_modes != {ColorMode.ONOFF} + and not self._transitions_disabled + ): self._attr_supported_features |= LightEntityFeature.TRANSITION LOGGER.debug( @@ -376,6 +395,21 @@ class MatterLight(MatterEntity, LightEntity): else: self._attr_color_mode = ColorMode.ONOFF + def _check_transition_blocklist(self) -> None: + """Check if this device is reported to have non working transitions.""" + device_info = self._endpoint.device_info + if ( + device_info.vendorID, + device_info.productID, + device_info.hardwareVersionString, + device_info.softwareVersionString, + ) in TRANSITION_BLOCKLIST: + self._transitions_disabled = True + LOGGER.warning( + "Detected a device that has been reported to have firmware issues " + "with light transitions. Transitions will be disabled for this light" + ) + # Discovery schema(s) to map Matter Attributes to HA entities DISCOVERY_SCHEMAS = [ From e1c08959b0d5dfa8fcacfb1e1d68c712c14ee81f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 10:22:50 -0500 Subject: [PATCH 92/96] Fix stop event cleanup when reloading MQTT (#116525) --- homeassistant/components/mqtt/client.py | 42 ++++++++++--------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 74fa8fb3302..d79492ccb27 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -25,19 +25,12 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import ( - CALLBACK_TYPE, - CoreState, - Event, - HassJob, - HomeAssistant, - callback, -) +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.start import async_at_started from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -429,25 +422,22 @@ class MQTT: UNSUBSCRIBE_COOLDOWN, self._async_perform_unsubscribes ) self._pending_unsubscribes: set[str] = set() # topic - - if self.hass.state is CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_: Event) -> None: - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - async def async_stop_mqtt(_event: Event) -> None: - """Stop MQTT component.""" - await self.async_disconnect() - - self._cleanup_on_unload.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + self._cleanup_on_unload.extend( + ( + async_at_started(hass, self._async_ha_started), + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self._async_ha_stop), + ) ) + @callback + def _async_ha_started(self, _hass: HomeAssistant) -> None: + """Handle HA started.""" + self._ha_started.set() + + async def _async_ha_stop(self, _event: Event) -> None: + """Handle HA stop.""" + await self.async_disconnect() + def start( self, mqtt_data: MqttData, From 1641f24314d6181e1f91ed61fd90e8e112342406 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 18:32:41 +0200 Subject: [PATCH 93/96] Bump version to 2024.5.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3c3787c7e80..38457221bc9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 118f2f91d2c..57489b42fec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b6" +version = "2024.5.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 858874f0dafc274d27414d0761fda39142714078 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 May 2024 18:59:07 +0200 Subject: [PATCH 94/96] Bump version to 2024.5.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 38457221bc9..eb46817bd34 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 57489b42fec..4dd5653f8ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.0b7" +version = "2024.5.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 21466180aad4ca1d0fb1487007c6e2419ba0ca77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 12:45:47 -0500 Subject: [PATCH 95/96] Ensure mock mqtt handler is cleaned up after test_bootstrap_dependencies (#116544) --- tests/test_bootstrap.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 96caf5d10c8..782b082e639 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1143,16 +1143,10 @@ async def test_bootstrap_empty_integrations( await hass.async_block_till_done() -@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) -@pytest.mark.parametrize("load_registries", [False]) -async def test_bootstrap_dependencies( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - integration: str, -) -> None: - """Test dependencies are set up correctly,.""" +@pytest.fixture(name="mock_mqtt_config_flow") +def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: + """Mock MQTT config flow.""" - # Prepare MQTT config entry @HANDLERS.register("mqtt") class MockConfigFlow: """Mock the MQTT config flow.""" @@ -1160,6 +1154,19 @@ async def test_bootstrap_dependencies( VERSION = 1 MINOR_VERSION = 1 + yield + HANDLERS.pop("mqtt") + + +@pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"]) +@pytest.mark.parametrize("load_registries", [False]) +async def test_bootstrap_dependencies( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + integration: str, + mock_mqtt_config_flow: None, +) -> None: + """Test dependencies are set up correctly,.""" entry = MockConfigEntry(domain="mqtt", data={"broker": "test-broker"}) entry.add_to_hass(hass) From 343d97527c52bc2a4b18e322aab5dfdbd080ecb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 May 2024 13:22:18 -0500 Subject: [PATCH 96/96] Ensure mqtt handler is restored if its already registered in bootstrap test (#116549) --- tests/test_bootstrap.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 782b082e639..3d2735d9c1c 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1146,6 +1146,7 @@ async def test_bootstrap_empty_integrations( @pytest.fixture(name="mock_mqtt_config_flow") def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: """Mock MQTT config flow.""" + original_mqtt = HANDLERS.get("mqtt") @HANDLERS.register("mqtt") class MockConfigFlow: @@ -1155,7 +1156,10 @@ def mock_mqtt_config_flow_fixture() -> Generator[None, None, None]: MINOR_VERSION = 1 yield - HANDLERS.pop("mqtt") + if original_mqtt: + HANDLERS["mqtt"] = original_mqtt + else: + HANDLERS.pop("mqtt") @pytest.mark.parametrize("integration", ["mqtt_eventstream", "mqtt_statestream"])