From 2d0593168373536d461d8816ec03e7a45f214631 Mon Sep 17 00:00:00 2001 From: th3spis <69475918+th3spis@users.noreply.github.com> Date: Fri, 8 May 2026 00:06:23 +0200 Subject: [PATCH 01/27] Added wfsens as a occupancy source in wiz (#166799) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/wiz/__init__.py | 4 ++-- homeassistant/components/wiz/binary_sensor.py | 6 ++---- homeassistant/components/wiz/const.py | 2 ++ tests/components/wiz/test_binary_sensor.py | 15 ++++++++++++--- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index f66df15f6b40d6..bcdc7a8cca02c5 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -6,7 +6,6 @@ from typing import Any from pywizlight import PilotParser, wizlight -from pywizlight.bulb import PIR_SOURCE from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant, callback @@ -20,6 +19,7 @@ DISCOVER_SCAN_TIMEOUT, DISCOVERY_INTERVAL, DOMAIN, + OCCUPANCY_SOURCES, SIGNAL_WIZ_PIR, WIZ_CONNECT_EXCEPTIONS, ) @@ -101,7 +101,7 @@ def _async_push_update(state: PilotParser) -> None: """Receive a push update.""" _LOGGER.debug("%s: Got push update: %s", bulb.mac, state.pilotResult) coordinator.async_set_updated_data(coordinator.data) - if state.get_source() == PIR_SOURCE: + if state.get_source() in OCCUPANCY_SOURCES: async_dispatcher_send(hass, SIGNAL_WIZ_PIR.format(bulb.mac)) await bulb.start_push(_async_push_update) diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 9f5e548d5523e9..77d6fa0c63bfb7 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -4,8 +4,6 @@ from collections.abc import Callable -from pywizlight.bulb import PIR_SOURCE - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -16,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import DOMAIN, SIGNAL_WIZ_PIR +from .const import DOMAIN, OCCUPANCY_SOURCES, SIGNAL_WIZ_PIR from .coordinator import WizConfigEntry, WizData from .entity import WizEntity @@ -75,5 +73,5 @@ def __init__(self, wiz_data: WizData, name: str) -> None: @callback def _async_update_attrs(self) -> None: """Handle updating _attr values.""" - if self._device.state.get_source() == PIR_SOURCE: + if self._device.state.get_source() in OCCUPANCY_SOURCES: self._attr_is_on = self._device.status diff --git a/homeassistant/components/wiz/const.py b/homeassistant/components/wiz/const.py index 78074a3d5fbc52..59cc7788a74fcc 100644 --- a/homeassistant/components/wiz/const.py +++ b/homeassistant/components/wiz/const.py @@ -2,6 +2,7 @@ from datetime import timedelta +from pywizlight.bulb import PIR_SOURCE from pywizlight.exceptions import ( WizLightConnectionError, WizLightNotKnownBulb, @@ -24,3 +25,4 @@ WIZ_CONNECT_EXCEPTIONS = (WizLightNotKnownBulb, *WIZ_EXCEPTIONS) SIGNAL_WIZ_PIR = "wiz_pir_{}" +OCCUPANCY_SOURCES = frozenset({PIR_SOURCE, "wfsens"}) diff --git a/tests/components/wiz/test_binary_sensor.py b/tests/components/wiz/test_binary_sensor.py index c7e5541d91ec7f..85705dadfd6b43 100644 --- a/tests/components/wiz/test_binary_sensor.py +++ b/tests/components/wiz/test_binary_sensor.py @@ -1,5 +1,7 @@ """Tests for WiZ binary_sensor platform.""" +import pytest + from homeassistant.components import wiz from homeassistant.components.wiz.binary_sensor import OCCUPANCY_UNIQUE_ID from homeassistant.config_entries import ConfigEntryState @@ -21,20 +23,27 @@ from tests.common import MockConfigEntry +@pytest.mark.parametrize("occupancy_source", ["pir", "wfsens"]) async def test_binary_sensor_created_from_push_updates( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + occupancy_source: str, ) -> None: """Test a binary sensor created from push updates.""" bulb, _ = await async_setup_integration(hass) - await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": True}) + await async_push_update( + hass, bulb, {"mac": FAKE_MAC, "src": occupancy_source, "state": True} + ) entity_id = "binary_sensor.mock_title_occupancy" assert entity_registry.async_get(entity_id).unique_id == f"{FAKE_MAC}_occupancy" state = hass.states.get(entity_id) assert state.state == STATE_ON - await async_push_update(hass, bulb, {"mac": FAKE_MAC, "src": "pir", "state": False}) + await async_push_update( + hass, bulb, {"mac": FAKE_MAC, "src": occupancy_source, "state": False} + ) state = hass.states.get(entity_id) assert state.state == STATE_OFF From 76b878b1369db7c4d7388993fa4f4941c39be527 Mon Sep 17 00:00:00 2001 From: Rob Treacy Date: Fri, 8 May 2026 09:35:13 +0100 Subject: [PATCH 02/27] Fix WiZ Light config flow timeout by properly closing UDP connections (#168456) --- homeassistant/components/wiz/config_flow.py | 6 +++ tests/components/wiz/test_config_flow.py | 42 +++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index a676c77688d042..84bee623ad99e8 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -81,6 +81,8 @@ async def _async_connect_discovered_or_abort(self) -> None: exc_info=True, ) raise AbortFlow("cannot_connect") from ex + finally: + await bulb.async_close() self._name = name_from_bulb_type_and_mac(bulbtype, device.mac_address) async def async_step_discovery_confirm( @@ -118,6 +120,8 @@ async def async_step_pick_device( bulbtype = await bulb.get_bulbtype() except WIZ_CONNECT_EXCEPTIONS: return self.async_abort(reason="cannot_connect") + finally: + await bulb.async_close() return self.async_create_entry( title=name_from_bulb_type_and_mac(bulbtype, device.mac_address), @@ -182,6 +186,8 @@ async def async_step_user( title=name, data=user_input, ) + finally: + await bulb.async_close() return self.async_show_form( step_id="user", diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index 946eb032f8ede2..b58ba138f9fb00 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -47,6 +47,8 @@ async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" + bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -54,7 +56,7 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] == {} # Patch functions with ( - _patch_wizlight(), + _patch_wizlight(device=bulb), patch( "homeassistant.components.wiz.async_setup_entry", return_value=True, @@ -76,6 +78,7 @@ async def test_form(hass: HomeAssistant) -> None: } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + bulb.async_close.assert_awaited_once() async def test_user_flow_enters_dns_name(hass: HomeAssistant) -> None: @@ -137,10 +140,10 @@ async def test_user_form_exceptions( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.wiz.wizlight.getBulbConfig", - side_effect=side_effect, - ): + bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB) + bulb.get_bulbtype = AsyncMock(side_effect=side_effect) + + with _patch_wizlight(device=bulb): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_CONNECTION, @@ -148,6 +151,7 @@ async def test_user_form_exceptions( assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": error_base} + bulb.async_close.assert_awaited_once() async def test_form_updates_unique_id(hass: HomeAssistant) -> None: @@ -185,10 +189,10 @@ async def test_discovered_by_dhcp_connection_fails( hass: HomeAssistant, source, data ) -> None: """Test we abort on connection failure.""" - with patch( - "homeassistant.components.wiz.wizlight.getBulbConfig", - side_effect=WizLightTimeOutError, - ): + bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB) + bulb.get_bulbtype = AsyncMock(side_effect=WizLightTimeOutError) + + with _patch_wizlight(device=bulb): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) @@ -196,6 +200,7 @@ async def test_discovered_by_dhcp_connection_fails( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" + bulb.async_close.assert_awaited_once() @pytest.mark.parametrize( @@ -263,9 +268,9 @@ async def test_discovered_by_dhcp_or_integration_discovery( hass: HomeAssistant, source, data, bulb_type, extended_white_range, name ) -> None: """Test we can configure when discovered from dhcp or discovery.""" - with _patch_wizlight( - device=None, extended_white_range=extended_white_range, bulb_type=bulb_type - ): + bulb = _mocked_wizlight(None, extended_white_range, bulb_type) + + with _patch_wizlight(device=bulb): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) @@ -273,11 +278,12 @@ async def test_discovered_by_dhcp_or_integration_discovery( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" + bulb.async_close.assert_awaited_once() + + bulb.async_close.reset_mock() with ( - _patch_wizlight( - device=None, extended_white_range=extended_white_range, bulb_type=bulb_type - ), + _patch_wizlight(device=bulb), patch( "homeassistant.components.wiz.async_setup_entry", return_value=True, @@ -299,6 +305,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + bulb.async_close.assert_awaited_once() @pytest.mark.parametrize( @@ -393,8 +400,10 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: assert result2["step_id"] == "pick_device" assert not result2["errors"] + bulb = _mocked_wizlight(None, None, FAKE_DIMMABLE_BULB) + with ( - _patch_wizlight(), + _patch_wizlight(device=bulb), patch( "homeassistant.components.wiz.async_setup", return_value=True ) as mock_setup, @@ -415,6 +424,7 @@ async def test_setup_via_discovery(hass: HomeAssistant) -> None: } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + bulb.async_close.assert_awaited_once() # ignore configured devices result = await hass.config_entries.flow.async_init( From aac49a567fe0dfe33703275c904d1c936bba6326 Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 7 May 2026 13:55:17 -0600 Subject: [PATCH 03/27] Fix IntelliFire setup recovery (#169739) --- .../components/intellifire/__init__.py | 5 +++ .../components/intellifire/config_flow.py | 7 ++-- .../intellifire/test_config_flow.py | 32 +++++++++++++++++++ tests/components/intellifire/test_init.py | 20 ++++++++---- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 77171044e9b9a7..8fc2477ef17028 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -4,6 +4,7 @@ import asyncio +import aiohttp from intellifire4py import UnifiedFireplace from intellifire4py.cloud_interface import IntelliFireCloudInterface from intellifire4py.const import IntelliFireApiMode @@ -155,6 +156,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry) raise ConfigEntryNotReady( "Initialization of fireplace timed out after 10 minutes" ) from err + except (aiohttp.ClientConnectionError, ConnectionError) as err: + raise ConfigEntryNotReady( + "Error communicating with fireplace during initialization" + ) from err # Construct coordinator data_update_coordinator = IntellifireDataUpdateCoordinator(hass, entry, fireplace) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index e58a5e46559a23..83248bd0ff4f4c 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, + ConfigEntryState, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -289,10 +290,8 @@ async def async_step_init( errors: dict[str, str] = {} if user_input is not None: - # Validate connectivity for requested modes if runtime data is available - coordinator = self.config_entry.runtime_data - if coordinator is not None: - fireplace = coordinator.fireplace + if self.config_entry.state is ConfigEntryState.LOADED: + fireplace = self.config_entry.runtime_data.fireplace # Refresh connectivity status before validating await fireplace.async_validate_connectivity() diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 20223d255e67da..2d10873748a218 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -272,6 +272,38 @@ async def test_options_flow( } +async def test_options_flow_allows_submit_when_not_loaded( + hass: HomeAssistant, + mock_config_entry_current: MockConfigEntry, +) -> None: + """Test options flow allows submit when runtime data is missing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + version=mock_config_entry_current.version, + minor_version=mock_config_entry_current.minor_version, + data=dict(mock_config_entry_current.data), + options=dict(mock_config_entry_current.options), + unique_id=mock_config_entry_current.unique_id, + state=config_entries.ConfigEntryState.SETUP_ERROR, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {CONF_READ_MODE: API_MODE_CLOUD, CONF_CONTROL_MODE: API_MODE_LOCAL}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_READ_MODE: API_MODE_CLOUD, + CONF_CONTROL_MODE: API_MODE_LOCAL, + } + + async def test_options_flow_local_read_unavailable( hass: HomeAssistant, mock_config_entry_current: MockConfigEntry, diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index ac689a164b5bab..f71681c81e6fef 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -1,8 +1,10 @@ """Test the IntelliFire config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp from intellifire4py.const import IntelliFireApiMode +import pytest from homeassistant.components.intellifire import CONF_USER_ID from homeassistant.components.intellifire.const import ( @@ -159,22 +161,28 @@ async def test_init_with_no_username(hass: HomeAssistant, mock_apis_single_fp) - assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR -async def test_connectivity_bad( +@pytest.mark.parametrize( + "setup_error", + [aiohttp.ClientConnectionError, ConnectionError, TimeoutError], +) +async def test_connectivity_error_during_setup_retries( hass: HomeAssistant, - mock_config_entry_current, - mock_apis_single_fp, + mock_config_entry_current: MockConfigEntry, + mock_apis_single_fp: tuple[AsyncMock, AsyncMock, MagicMock], + setup_error: type[Exception], ) -> None: - """Test a timeout error on the setup flow.""" + """Test a connection error during setup retries the config entry.""" with patch( "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", new_callable=AsyncMock, - side_effect=TimeoutError, + side_effect=setup_error, ): mock_config_entry_current.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry_current.entry_id) await hass.async_block_till_done() + assert mock_config_entry_current.state is ConfigEntryState.SETUP_RETRY assert len(hass.states.async_all()) == 0 From a4227ef1bcd1cbf08fd96f2ae74976b34abbebc0 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 6 May 2026 17:48:35 +0200 Subject: [PATCH 04/27] Fix hassio auth IndexError on Supervisor Unix socket requests (#169911) --- homeassistant/components/hassio/auth.py | 21 ++++++----- tests/components/hassio/test_auth.py | 47 ++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 8589bc0f134705..9c9d7cc710ef8a 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -12,6 +12,7 @@ from homeassistant.auth.models import User from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView +from homeassistant.components.http.const import is_supervisor_unix_socket_request from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -41,14 +42,18 @@ def __init__(self, hass: HomeAssistant, user: User) -> None: def _check_access(self, request: web.Request) -> None: """Check if this call is from Supervisor.""" - # Check caller IP - hassio_ip = os.environ["SUPERVISOR"].split(":")[0] - assert request.transport - if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( - hassio_ip - ): - _LOGGER.error("Invalid auth request from %s", request.remote) - raise HTTPUnauthorized + # Requests over the Supervisor Unix socket are authenticated by the + # http auth middleware as the Supervisor user, so the caller-IP check + # below does not apply (and would crash, since `peername` is empty for + # Unix sockets). The user-ID check still runs to ensure only the + # Supervisor user can reach this endpoint. + if not is_supervisor_unix_socket_request(request): + hassio_ip = os.environ["SUPERVISOR"].split(":")[0] + assert request.transport + peername = request.transport.get_extra_info("peername") + if not peername or ip_address(peername[0]) != ip_address(hassio_ip): + _LOGGER.error("Invalid auth request from %s", request.remote) + raise HTTPUnauthorized # Check caller token if request[KEY_HASS_USER].id != self.user.id: diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index ad96b58e99db8e..7f5e4ba331b661 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,11 +1,17 @@ """The tests for the hassio component.""" +from contextlib import AbstractContextManager, ExitStack as DefaultContext from http import HTTPStatus -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from aiohttp.test_utils import TestClient +from aiohttp.web_exceptions import HTTPUnauthorized +import pytest from homeassistant.auth.providers.homeassistant import InvalidAuth +from homeassistant.components.hassio.auth import HassIOBaseAuth +from homeassistant.components.hassio.const import DATA_CONFIG_STORE +from homeassistant.core import HomeAssistant async def test_auth_success(hassio_client_supervisor: TestClient) -> None: @@ -162,6 +168,45 @@ async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None: assert resp.status == HTTPStatus.UNAUTHORIZED +@pytest.mark.parametrize( + ("peername", "unix_socket", "expectation"), + [ + # Unix socket transports report an empty string for peername. Before + # the fix this raised IndexError on `peername[0]`. + ("", True, DefaultContext()), + # Defensive: a TCP transport with no peername at all should be + # rejected, not crash. + (None, False, pytest.raises(HTTPUnauthorized)), + ], +) +@pytest.mark.usefixtures("hassio_stubs") +async def test_check_access_unix_socket_or_missing_peername( + hass: HomeAssistant, + peername: str | None, + unix_socket: bool, + expectation: AbstractContextManager, +) -> None: + """Test _check_access handles Unix socket requests and missing peername.""" + hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user + assert hassio_user_id is not None + user = await hass.auth.async_get_user(hassio_user_id) + assert user is not None + + auth_view = HassIOBaseAuth(hass, user) + request = MagicMock() + request.transport.get_extra_info.return_value = peername + request.__getitem__.return_value = user + + with ( + patch( + "homeassistant.components.hassio.auth.is_supervisor_unix_socket_request", + return_value=unix_socket, + ), + expectation, + ): + auth_view._check_access(request) + + async def test_password_no_user(hassio_client_supervisor: TestClient) -> None: """Test changing password for invalid user.""" resp = await hassio_client_supervisor.post( From 348f6149b4d676b35becc3bcbe731084fcaf0a4f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 6 May 2026 16:25:37 +0200 Subject: [PATCH 05/27] Update gardena ble to 2.8.1 (#169914) --- homeassistant/components/gardena_bluetooth/manifest.json | 2 +- homeassistant/components/husqvarna_automower_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/manifest.json b/homeassistant/components/gardena_bluetooth/manifest.json index 08e73c9bf4fe1c..284615c014b3f8 100644 --- a/homeassistant/components/gardena_bluetooth/manifest.json +++ b/homeassistant/components/gardena_bluetooth/manifest.json @@ -15,5 +15,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"], - "requirements": ["gardena-bluetooth==2.4.0"] + "requirements": ["gardena-bluetooth==2.8.1"] } diff --git a/homeassistant/components/husqvarna_automower_ble/manifest.json b/homeassistant/components/husqvarna_automower_ble/manifest.json index e3e710a4fed3d6..190891e3b3c794 100644 --- a/homeassistant/components/husqvarna_automower_ble/manifest.json +++ b/homeassistant/components/husqvarna_automower_ble/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.4.0"] + "requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index afb3110f8ea1d5..06a0047dd698ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1048,7 +1048,7 @@ gTTS==2.5.3 # homeassistant.components.gardena_bluetooth # homeassistant.components.husqvarna_automower_ble -gardena-bluetooth==2.4.0 +gardena-bluetooth==2.8.1 # homeassistant.components.google_assistant_sdk gassist-text==0.0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86ae19aa457864..883d750241f8ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -930,7 +930,7 @@ gTTS==2.5.3 # homeassistant.components.gardena_bluetooth # homeassistant.components.husqvarna_automower_ble -gardena-bluetooth==2.4.0 +gardena-bluetooth==2.8.1 # homeassistant.components.google_assistant_sdk gassist-text==0.0.14 From 6f87d02b72ce935ec47d59a3ab50c26066151fd4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 6 May 2026 14:04:13 -0400 Subject: [PATCH 06/27] Bump serialx to 1.7.1 (#169928) --- homeassistant/components/acer_projector/manifest.json | 2 +- homeassistant/components/serial/manifest.json | 2 +- homeassistant/components/usb/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 480a72bd09ece9..45d6256f0e8ec3 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/acer_projector", "iot_class": "local_polling", "quality_scale": "legacy", - "requirements": ["serialx==1.7.0"] + "requirements": ["serialx==1.7.1"] } diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index b7296c584c96b8..b87fc69491840f 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/serial", "iot_class": "local_polling", - "requirements": ["serialx==1.7.0"] + "requirements": ["serialx==1.7.1"] } diff --git a/homeassistant/components/usb/manifest.json b/homeassistant/components/usb/manifest.json index e2f6c3db62bc64..222df3e2c06e41 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.0"] + "requirements": ["aiousbwatcher==1.1.2", "serialx==1.7.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8ab0eea85f9f93..6bde0a182b8db4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -63,7 +63,7 @@ PyTurboJPEG==1.8.3 PyYAML==6.0.3 requests==2.33.1 securetar==2026.4.1 -serialx==1.7.0 +serialx==1.7.1 SQLAlchemy==2.0.49 standard-aifc==3.13.0 standard-telnetlib==3.13.0 diff --git a/requirements_all.txt b/requirements_all.txt index 06a0047dd698ae..1a538d530772e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2945,7 +2945,7 @@ sentry-sdk==2.48.0 # homeassistant.components.acer_projector # homeassistant.components.serial # homeassistant.components.usb -serialx==1.7.0 +serialx==1.7.1 # homeassistant.components.sfr_box sfrbox-api==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 883d750241f8ce..bdea50092def1f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2511,7 +2511,7 @@ sentry-sdk==2.48.0 # homeassistant.components.acer_projector # homeassistant.components.serial # homeassistant.components.usb -serialx==1.7.0 +serialx==1.7.1 # homeassistant.components.sfr_box sfrbox-api==0.1.1 From 4e61581cd8f096b113e62f4dc7fcc06199219d09 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 6 May 2026 22:08:38 +0200 Subject: [PATCH 07/27] Bump holidays to 0.96 (#169939) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index 7bb5d03af95208..b2199a39ad1906 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.95", "babel==2.15.0"] + "requirements": ["holidays==0.96", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 09d58507668adf..e061de1eba867d 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.95"] + "requirements": ["holidays==0.96"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1a538d530772e6..b090be3a960b9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1242,7 +1242,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.95 +holidays==0.96 # homeassistant.components.frontend home-assistant-frontend==20260429.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdea50092def1f..d39be649de590e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1106,7 +1106,7 @@ hole==0.9.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.95 +holidays==0.96 # homeassistant.components.frontend home-assistant-frontend==20260429.3 From f644448d0f674f8f54af89076ff623bd2b4ea9c4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 May 2026 22:16:55 +0200 Subject: [PATCH 08/27] Add support for options to todo triggers (#169947) --- homeassistant/components/todo/trigger.py | 3 +- tests/components/todo/test_trigger.py | 45 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/todo/trigger.py b/homeassistant/components/todo/trigger.py index 8387850f6e588a..cdb3bd5dd64093 100644 --- a/homeassistant/components/todo/trigger.py +++ b/homeassistant/components/todo/trigger.py @@ -10,7 +10,7 @@ import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_TARGET +from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -25,6 +25,7 @@ ITEM_TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_TARGET): cv.TARGET_FIELDS, + vol.Required(CONF_OPTIONS, default={}): {}, } ) diff --git a/tests/components/todo/test_trigger.py b/tests/components/todo/test_trigger.py index dabc9f29c37c2f..bbbcdb08686e50 100644 --- a/tests/components/todo/test_trigger.py +++ b/tests/components/todo/test_trigger.py @@ -36,6 +36,10 @@ from . import MockTodoListEntity, create_mock_platform from tests.common import async_mock_service, mock_device_registry +from tests.components.common import ( + assert_trigger_gated_by_labs_flag, + assert_trigger_options_supported, +) TODO_ENTITY_ID1 = "todo.list_one" TODO_ENTITY_ID2 = "todo.list_two" @@ -122,6 +126,47 @@ def service_calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "item_added") +@pytest.mark.parametrize( + "trigger_key", + [ + "todo.item_added", + "todo.item_completed", + "todo.item_removed", + ], +) +async def test_todo_triggers_gated_by_labs_flag( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str +) -> None: + """Test the todo triggers are gated by the labs flag.""" + await assert_trigger_gated_by_labs_flag(hass, caplog, trigger_key) + + +@pytest.mark.usefixtures("enable_labs_preview_features") +@pytest.mark.parametrize( + ("trigger_key", "base_options", "supports_behavior", "supports_duration"), + [ + ("todo.item_added", None, False, False), + ("todo.item_completed", None, False, False), + ("todo.item_removed", None, False, False), + ], +) +async def test_todo_trigger_options_validation( + hass: HomeAssistant, + trigger_key: str, + base_options: dict[str, Any] | None, + supports_behavior: bool, + supports_duration: bool, +) -> None: + """Test that todo triggers support the expected options.""" + await assert_trigger_options_supported( + hass, + trigger_key, + base_options, + supports_behavior=supports_behavior, + supports_duration=supports_duration, + ) + + def _assert_service_calls( service_calls: list[ServiceCall], expected_calls: list[dict[str, Any]] ) -> None: From 6fabbb354be0ce8248ac739eb840a9134435dee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 7 May 2026 09:41:08 +0200 Subject: [PATCH 09/27] Bump pyTibber to 0.37.5 (#169981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 00b4120a727efc..9877b62f369a6d 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["tibber"], - "requirements": ["pyTibber==0.37.4"] + "requirements": ["pyTibber==0.37.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index b090be3a960b9a..bdb8e46d0755f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1941,7 +1941,7 @@ pyRFXtrx==0.31.1 pySDCP==1 # homeassistant.components.tibber -pyTibber==0.37.4 +pyTibber==0.37.5 # homeassistant.components.dlink pyW215==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d39be649de590e..faacd8eb7368ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1684,7 +1684,7 @@ pyHomee==1.3.8 pyRFXtrx==0.31.1 # homeassistant.components.tibber -pyTibber==0.37.4 +pyTibber==0.37.5 # homeassistant.components.dlink pyW215==0.8.0 From 03aa979309ed232afd98479953ed335a3782f51b Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Tue, 5 May 2026 08:55:22 +0200 Subject: [PATCH 10/27] Bump python-duco-client to 0.4.0 (#169776) --- homeassistant/components/duco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/duco/snapshots/test_diagnostics.ambr | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duco/manifest.json b/homeassistant/components/duco/manifest.json index aa7749962888d8..a047e3f87a714b 100644 --- a/homeassistant/components/duco/manifest.json +++ b/homeassistant/components/duco/manifest.json @@ -13,7 +13,7 @@ "iot_class": "local_polling", "loggers": ["duco"], "quality_scale": "platinum", - "requirements": ["python-duco-client==0.3.10"], + "requirements": ["python-duco-client==0.4.0"], "zeroconf": [ { "name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*", diff --git a/requirements_all.txt b/requirements_all.txt index bdb8e46d0755f7..799c9f8dc339f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2581,7 +2581,7 @@ python-digitalocean==1.13.2 python-dropbox-api==0.1.3 # homeassistant.components.duco -python-duco-client==0.3.10 +python-duco-client==0.4.0 # homeassistant.components.ecobee python-ecobee-api==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index faacd8eb7368ca..2610790ec444d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2207,7 +2207,7 @@ python-citybikes==0.3.3 python-dropbox-api==0.1.3 # homeassistant.components.duco -python-duco-client==0.3.10 +python-duco-client==0.4.0 # homeassistant.components.ecobee python-ecobee-api==0.3.2 diff --git a/tests/components/duco/snapshots/test_diagnostics.ambr b/tests/components/duco/snapshots/test_diagnostics.ambr index 029af1a1798c41..76b108b65e3b39 100644 --- a/tests/components/duco/snapshots/test_diagnostics.ambr +++ b/tests/components/duco/snapshots/test_diagnostics.ambr @@ -4,10 +4,12 @@ 'board_info': dict({ 'box_name': 'SILENT_CONNECT', 'box_sub_type_name': 'Eu', + 'public_api_version': None, 'serial_board_box': '**REDACTED**', 'serial_board_comm': '**REDACTED**', 'serial_duco_box': '**REDACTED**', 'serial_duco_comm': '**REDACTED**', + 'software_version': None, }), 'duco_diagnostics': list([ dict({ From 5dd04363b22ce33cfea88fbaf8cdda828d715fcc Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Thu, 7 May 2026 13:26:22 +0200 Subject: [PATCH 11/27] Bump python-duco-client to 0.4.1 (#169991) --- homeassistant/components/duco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duco/manifest.json b/homeassistant/components/duco/manifest.json index a047e3f87a714b..180733ca3113b1 100644 --- a/homeassistant/components/duco/manifest.json +++ b/homeassistant/components/duco/manifest.json @@ -13,7 +13,7 @@ "iot_class": "local_polling", "loggers": ["duco"], "quality_scale": "platinum", - "requirements": ["python-duco-client==0.4.0"], + "requirements": ["python-duco-client==0.4.1"], "zeroconf": [ { "name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*", diff --git a/requirements_all.txt b/requirements_all.txt index 799c9f8dc339f2..38203aa676b271 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2581,7 +2581,7 @@ python-digitalocean==1.13.2 python-dropbox-api==0.1.3 # homeassistant.components.duco -python-duco-client==0.4.0 +python-duco-client==0.4.1 # homeassistant.components.ecobee python-ecobee-api==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2610790ec444d9..dc6788c525cfee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2207,7 +2207,7 @@ python-citybikes==0.3.3 python-dropbox-api==0.1.3 # homeassistant.components.duco -python-duco-client==0.4.0 +python-duco-client==0.4.1 # homeassistant.components.ecobee python-ecobee-api==0.3.2 From 44b1fea745c118f304a6c8342ed0153eeb7d4720 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 7 May 2026 19:59:06 +0200 Subject: [PATCH 12/27] Proper handling of malformed data during FRITZ!Box Tools setup (#170030) --- homeassistant/components/fritz/coordinator.py | 11 ++++++++-- homeassistant/components/fritz/strings.json | 3 +++ tests/components/fritz/test_init.py | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritz/coordinator.py b/homeassistant/components/fritz/coordinator.py index 686f5d98c71c7d..2016921bbc240c 100644 --- a/homeassistant/components/fritz/coordinator.py +++ b/homeassistant/components/fritz/coordinator.py @@ -10,6 +10,7 @@ import logging import re from typing import Any, TypedDict, cast +from xml.etree.ElementTree import ParseError from fritzconnection import FritzConnection from fritzconnection.core.exceptions import FritzActionError @@ -26,7 +27,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -228,7 +229,13 @@ def setup(self) -> None: self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) self.fritz_call = FritzCall(fc=self.connection) - info = self.fritz_status.get_device_info() + try: + info = self.fritz_status.get_device_info() + except ParseError as ex: + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="error_parse_device_info", + ) from ex _LOGGER.debug( "gathered device info of %s %s", diff --git a/homeassistant/components/fritz/strings.json b/homeassistant/components/fritz/strings.json index 67326ae4ea2c4b..0ff85b8d5c8035 100644 --- a/homeassistant/components/fritz/strings.json +++ b/homeassistant/components/fritz/strings.json @@ -185,6 +185,9 @@ "config_entry_not_found": { "message": "Failed to perform action \"{service}\". Config entry for target not found" }, + "error_parse_device_info": { + "message": "Error parsing device info. Please check the system event log of your FRITZ!Box for malformed data and clear the event list." + }, "error_refresh_hosts_info": { "message": "Error refreshing hosts info" }, diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index 024160dad68c8d..9d7a23be096736 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -2,6 +2,7 @@ import re from unittest.mock import patch +from xml.etree.ElementTree import ParseError from freezegun.api import FrozenDateTimeFactory import pytest @@ -116,6 +117,25 @@ async def test_setup_fail(hass: HomeAssistant, error) -> None: assert entry.state is ConfigEntryState.SETUP_RETRY +async def test_setup_fail_parse_error(hass: HomeAssistant, fc_class_mock) -> None: + """Test setup failure due to parse error while fetching device data.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + with ( + patch( + "homeassistant.components.fritz.coordinator.FritzStatus.get_device_info" + ) as fs_mock, + ): + fs_mock.side_effect = ParseError("boom") + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + assert entry.error_reason_translation_key == "error_parse_device_info" + + async def test_upnp_missing( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From e1ad765414fe011109d564dfd7ad8312cba995f0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 May 2026 16:18:24 +0200 Subject: [PATCH 13/27] Fix websocket certificate verification Bump axis to v70 (#170038) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index ca90c2d0e03ab7..e37183cb576158 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -29,7 +29,7 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["axis"], - "requirements": ["axis==69"], + "requirements": ["axis==70"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 38203aa676b271..8d33f1c21d057c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -600,7 +600,7 @@ avea==1.6.1 # avion==0.10 # homeassistant.components.axis -axis==69 +axis==70 # homeassistant.components.fujitsu_fglair ayla-iot-unofficial==1.4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc6788c525cfee..dc7f4c1decc776 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -552,7 +552,7 @@ autoskope_client==1.4.1 av==16.0.1 # homeassistant.components.axis -axis==69 +axis==70 # homeassistant.components.fujitsu_fglair ayla-iot-unofficial==1.4.7 From cc140be85c889914cfb0e0d80c98c954641ebe09 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 09:04:49 +0200 Subject: [PATCH 14/27] Fix `is_closed` state for DynamicGarageDoor in Overkiz (#170052) --- homeassistant/components/overkiz/cover.py | 28 ++- .../setup/cloud_somfy_tahoma_v2_europe.json | 232 ++++++++++++++++++ .../overkiz/snapshots/test_cover.ambr | 159 ++++++++++++ tests/components/overkiz/test_cover.py | 33 +++ 4 files changed, 451 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index c6a7bebd805a8e..ace1a16b440ae4 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -125,6 +125,32 @@ class OverkizCoverDescription(CoverEntityDescription): close_tilt_command_args=(15, 1), # position (1-127), speed (1-15) stop_tilt_command=OverkizCommand.STOP, ), + # Needs override since PositionableGarageDoor reports + # core:OpenClosedUnknownState instead of core:OpenClosedState + # uiClass is GarageDoor + OverkizCoverDescription( + key=UIWidget.POSITIONABLE_GARAGE_DOOR, + device_class=CoverDeviceClass.GARAGE, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN, + ), + # Needs override since PositionableGarageDoorWithPartialPosition reports + # core:OpenClosedPartialState instead of core:OpenClosedState + # uiClass is GarageDoor + OverkizCoverDescription( + key=UIWidget.POSITIONABLE_GARAGE_DOOR_WITH_PARTIAL_POSITION, + device_class=CoverDeviceClass.GARAGE, + current_position_state=OverkizState.CORE_CLOSURE, + set_position_command=OverkizCommand.SET_CLOSURE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + is_closed_state=OverkizState.CORE_OPEN_CLOSED_PARTIAL, + ), # Needs override to support this Generic device (rts:GenericRTSComponent) # uiClass is Generic (not mapped to cover as this is a Generic device class) OverkizCoverDescription( @@ -201,7 +227,7 @@ class OverkizCoverDescription(CoverEntityDescription): set_position_command=OverkizCommand.SET_CLOSURE, open_command=OverkizCommand.OPEN, close_command=OverkizCommand.CLOSE, - is_closed_state=OverkizState.CORE_OPEN_CLOSED_UNKNOWN, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, stop_command=OverkizCommand.STOP, ), OverkizCoverDescription( diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json index b20d0c4c58b997..767327d82d3605 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json @@ -7370,6 +7370,238 @@ "widget": "DiscreteGateWithPedestrianPosition", "oid": "6ba9b1de-8037-41d7-9150-21f7d5f49a3f", "uiClass": "Gate" + }, + { + "creationTime": 1521964729000, + "lastUpdateTime": 1521964729000, + "label": "Garage Door", + "deviceURL": "io://1234-1234-6233/16730050", + "shortcut": false, + "controllableName": "io:DynamicGarageDoorComponent", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "getName", + "nparams": 0 + }, + { + "commandName": "identify", + "nparams": 0 + }, + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "stop", + "nparams": 0 + } + ], + "states": [ + { + "type": "DiscreteState", + "values": ["closed", "open"], + "qualifiedName": "core:OpenClosedState" + }, + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:StatusState" + } + ], + "widgetName": "DynamicGarageDoor", + "uiProfiles": ["OpenClose"], + "uiClass": "GarageDoor", + "qualifiedName": "io:DynamicGarageDoorComponent", + "type": "ACTUATOR" + }, + "states": [ + { + "name": "core:OpenClosedState", + "type": 3, + "value": "closed" + }, + { + "name": "core:StatusState", + "type": 3, + "value": "available" + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "type": 3, + "value": "Somfy" + } + ], + "available": true, + "enabled": true, + "placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3", + "widget": "DynamicGarageDoor", + "type": 1, + "oid": "a5d47351-52d0-4a4c-a52d-abcdef123456", + "uiClass": "GarageDoor" + }, + { + "creationTime": 1521964729000, + "lastUpdateTime": 1521964729000, + "label": "OGP Garage Door", + "deviceURL": "ogp://1234-1234-6233/6632544", + "shortcut": false, + "controllableName": "ogp:GarageDoor", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "identify", + "nparams": 0 + }, + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "stop", + "nparams": 0 + } + ], + "states": [ + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:AvailabilityState" + }, + { + "type": "DiscreteState", + "values": ["closed", "open"], + "qualifiedName": "core:OpenClosedState" + }, + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:StatusState" + } + ], + "widgetName": "DynamicGarageDoor", + "uiProfiles": ["StatefulOpenClose", "OpenClose"], + "uiClass": "GarageDoor", + "qualifiedName": "ogp:GarageDoor", + "type": "ACTUATOR" + }, + "states": [ + { + "name": "core:AvailabilityState", + "type": 3, + "value": "available" + }, + { + "name": "core:StatusState", + "type": 3, + "value": "available" + }, + { + "name": "core:OpenClosedState", + "type": 3, + "value": "closed" + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "type": 3, + "value": "Somfy" + }, + { + "name": "core:Technology", + "type": 3, + "value": "io2way" + } + ], + "available": true, + "enabled": true, + "placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3", + "widget": "DynamicGarageDoor", + "type": 1, + "oid": "2492f7ae-3711-4160-9dae-e8910b708ce1", + "uiClass": "GarageDoor" + }, + { + "creationTime": 1521964729000, + "lastUpdateTime": 1521964729000, + "label": "Partial Garage Door", + "deviceURL": "io://1234-1234-6233/7433515", + "shortcut": false, + "controllableName": "io:DiscreteGarageOpenerWithPartialPositionIOComponent", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "partialPosition", + "nparams": 0 + }, + { + "commandName": "stop", + "nparams": 0 + } + ], + "states": [ + { + "type": "DiscreteState", + "values": ["closed", "open", "partial"], + "qualifiedName": "core:OpenClosedPartialState" + }, + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:StatusState" + } + ], + "widgetName": "PositionableGarageDoorWithPartialPosition", + "uiProfiles": ["OpenClose"], + "uiClass": "GarageDoor", + "qualifiedName": "io:DiscreteGarageOpenerWithPartialPositionIOComponent", + "type": "ACTUATOR" + }, + "states": [ + { + "name": "core:StatusState", + "type": 3, + "value": "available" + }, + { + "name": "core:OpenClosedPartialState", + "type": 3, + "value": "closed" + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "type": 3, + "value": "Somfy" + } + ], + "available": true, + "enabled": true, + "placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3", + "widget": "PositionableGarageDoorWithPartialPosition", + "type": 1, + "oid": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", + "uiClass": "GarageDoor" } ], "zones": [], diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index dc35a2d803bc97..4683840c5f8066 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -1081,6 +1081,59 @@ 'state': 'closed', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garage_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.garage_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'io://1234-1234-6233/16730050', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garage_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Garage Door', + 'is_closed': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.garage_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.garden_gate-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -1512,6 +1565,112 @@ 'state': 'closed', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_garage_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.ogp_garage_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'ogp://1234-1234-6233/6632544', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_garage_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'OGP Garage Door', + 'is_closed': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.ogp_garage_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.partial_garage_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.partial_garage_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'io://1234-1234-6233/7433515', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.partial_garage_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Partial Garage Door', + 'is_closed': True, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.partial_garage_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'closed', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.patio_shutter-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index caaf0dfdce31cc..304d255724a0c8 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -90,6 +90,21 @@ "io://1234-5678-5010/12931361", "cover.basement_roller_shutter", ) +DYNAMIC_GARAGE_DOOR = FixtureDevice( + "setup/cloud_somfy_tahoma_v2_europe.json", + "io://1234-1234-6233/16730050", + "cover.garage_door", +) +DYNAMIC_GARAGE_DOOR_OGP = FixtureDevice( + "setup/cloud_somfy_tahoma_v2_europe.json", + "ogp://1234-1234-6233/6632544", + "cover.ogp_garage_door", +) +PARTIAL_GARAGE_DOOR = FixtureDevice( + "setup/cloud_somfy_tahoma_v2_europe.json", + "io://1234-1234-6233/7433515", + "cover.partial_garage_door", +) SNAPSHOT_FIXTURES = [ AWNING, @@ -135,23 +150,41 @@ async def test_cover_entities_snapshot( (SHUTTER, SERVICE_OPEN_COVER, "open", CoverState.OPENING), (AWNING, SERVICE_OPEN_COVER, "deploy", CoverState.OPENING), (GARAGE, SERVICE_OPEN_COVER, "open", CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", CoverState.OPENING), + (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), (SHUTTER, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), (AWNING, SERVICE_CLOSE_COVER, "undeploy", CoverState.CLOSING), (GARAGE, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), + (DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), + (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), (SHUTTER, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), (AWNING, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), (GARAGE, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), + (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), ], ids=[ "open-roller-shutter", "open-awning", "open-garage-door", + "open-dynamic-garage-door", + "open-dynamic-garage-door-ogp", + "open-partial-garage-door", "close-roller-shutter", "close-awning", "close-garage-door", + "close-dynamic-garage-door", + "close-dynamic-garage-door-ogp", + "close-partial-garage-door", "stop-roller-shutter", "stop-awning", "stop-garage-door", + "stop-dynamic-garage-door", + "stop-dynamic-garage-door-ogp", + "stop-partial-garage-door", ], ) async def test_cover_service_actions( From 7b749b95ce822dccca3ccbae69ac9b3861361b22 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 11:57:37 +0200 Subject: [PATCH 15/27] Fix tilt controls for TiltOnlyVenetianBlind in Overkiz (#170055) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/overkiz/cover.py | 9 +- homeassistant/components/overkiz/executor.py | 2 + .../setup/cloud_somfy_connexoon_rts_asia.json | 57 +++++++++++ .../overkiz/snapshots/test_cover.ambr | 54 ++++++++++ tests/components/overkiz/test_cover.py | 98 ++++++++++++++----- 5 files changed, 197 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index ace1a16b440ae4..e069b01c274393 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -101,14 +101,21 @@ class OverkizCoverDescription(CoverEntityDescription): close_tilt_command=OverkizCommand.LOWER_CLOSE, stop_tilt_command=OverkizCommand.STOP, ), - # Needs override to remove open/close commands + # Needs override to add support for very specific tilt commands # uiClass is VenetianBlind OverkizCoverDescription( key=UIWidget.TILT_ONLY_VENETIAN_BLIND, device_class=CoverDeviceClass.BLIND, is_closed_state=OverkizState.CORE_OPEN_CLOSED, + # Position commands fully open/close the tilt + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + # Tilt commands move the tilt with a few degrees open_tilt_command=OverkizCommand.TILT_POSITIVE, + open_tilt_command_args=(1, 0), close_tilt_command=OverkizCommand.TILT_NEGATIVE, + close_tilt_command_args=(1, 0), stop_tilt_command=OverkizCommand.STOP, ), # Needs override to support very specific tilt commands (rts:ExteriorVenetianBlindRTSComponent) diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index 220c6fe7cb224d..2fbfbdfeda5bcb 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -22,6 +22,8 @@ OverkizCommand.ON, OverkizCommand.ON_WITH_TIMER, OverkizCommand.TEST, + OverkizCommand.TILT_POSITIVE, + OverkizCommand.TILT_NEGATIVE, ] diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json index d2fad670bd2bf7..8ea89c840ca9e3 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json @@ -323,6 +323,63 @@ "type": 1, "oid": "ef870fe7-daf3-4f33-b91e-62b2a809ef4e", "uiClass": "RollerShutter" + }, + { + "creationTime": 1613676700000, + "lastUpdateTime": 1613676700000, + "label": "Jaloezie", + "deviceURL": "rts://1234-1234-6362/16730044", + "shortcut": false, + "controllableName": "rts:TiltOnlyVenetianBlindRTSComponent", + "definition": { + "commands": [ + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "stop", + "nparams": 0 + }, + { + "commandName": "tiltPositive", + "nparams": 2 + }, + { + "commandName": "tiltNegative", + "nparams": 2 + }, + { + "commandName": "my", + "nparams": 1 + }, + { + "commandName": "identify", + "nparams": 0 + } + ], + "states": [], + "attributes": [], + "dataProperties": [], + "widgetName": "TiltOnlyVenetianBlind", + "uiProfiles": ["OpenClose"], + "uiClass": "VenetianBlind", + "qualifiedName": "rts:TiltOnlyVenetianBlindRTSComponent", + "type": "ACTUATOR" + }, + "states": [], + "attributes": [], + "available": true, + "enabled": true, + "placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2", + "widget": "TiltOnlyVenetianBlind", + "type": 1, + "oid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "uiClass": "VenetianBlind" } ], "zones": [], diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index 4683840c5f8066..4fc0d4c9abc1ab 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -323,6 +323,60 @@ 'state': 'open', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.jaloezie', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'rts://1234-1234-6362/16730044', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.jaloezie-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'device_class': 'blind', + 'friendly_name': 'Jaloezie', + 'is_closed': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.jaloezie', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.kitchen_shutter-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 304d255724a0c8..7d3300f3373494 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -29,7 +29,12 @@ CoverEntityFeature, CoverState, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -90,6 +95,11 @@ "io://1234-5678-5010/12931361", "cover.basement_roller_shutter", ) +TILT_ONLY_VENETIAN_BLIND = FixtureDevice( + "setup/cloud_somfy_connexoon_rts_asia.json", + "rts://1234-1234-6362/16730044", + "cover.jaloezie", +) DYNAMIC_GARAGE_DOOR = FixtureDevice( "setup/cloud_somfy_tahoma_v2_europe.json", "io://1234-1234-6233/16730050", @@ -145,26 +155,62 @@ async def test_cover_entities_snapshot( @pytest.mark.parametrize( - ("device", "service", "command_name", "expected_state"), + ("device", "service", "command_name", "parameters", "expected_state"), [ - (SHUTTER, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (AWNING, SERVICE_OPEN_COVER, "deploy", CoverState.OPENING), - (GARAGE, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", CoverState.OPENING), - (SHUTTER, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (AWNING, SERVICE_CLOSE_COVER, "undeploy", CoverState.CLOSING), - (GARAGE, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", CoverState.CLOSING), - (SHUTTER, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (AWNING, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (GARAGE, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), - (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", CoverState.CLOSED), + (SHUTTER, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (AWNING, SERVICE_OPEN_COVER, "deploy", None, CoverState.OPENING), + (GARAGE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (TILT_ONLY_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING), + (SHUTTER, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + (AWNING, SERVICE_CLOSE_COVER, "undeploy", None, CoverState.CLOSING), + (GARAGE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + (DYNAMIC_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + ( + DYNAMIC_GARAGE_DOOR_OGP, + SERVICE_CLOSE_COVER, + "close", + None, + CoverState.CLOSING, + ), + (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_CLOSE_COVER, + "close", + [0], + CoverState.CLOSING, + ), + (SHUTTER, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (AWNING, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (GARAGE, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (TILT_ONLY_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_OPEN_COVER_TILT, + "tiltPositive", + [1, 0], + CoverState.OPENING, + ), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_CLOSE_COVER_TILT, + "tiltNegative", + [1, 0], + CoverState.CLOSING, + ), + ( + TILT_ONLY_VENETIAN_BLIND, + SERVICE_STOP_COVER_TILT, + "stop", + [0], + STATE_UNKNOWN, + ), ], ids=[ "open-roller-shutter", @@ -173,18 +219,24 @@ async def test_cover_entities_snapshot( "open-dynamic-garage-door", "open-dynamic-garage-door-ogp", "open-partial-garage-door", + "open-tilt-only-venetian-blind", "close-roller-shutter", "close-awning", "close-garage-door", "close-dynamic-garage-door", "close-dynamic-garage-door-ogp", "close-partial-garage-door", + "close-tilt-only-venetian-blind", "stop-roller-shutter", "stop-awning", "stop-garage-door", "stop-dynamic-garage-door", "stop-dynamic-garage-door-ogp", "stop-partial-garage-door", + "stop-tilt-only-venetian-blind", + "open-tilt-tilt-only-venetian-blind", + "close-tilt-tilt-only-venetian-blind", + "stop-tilt-tilt-only-venetian-blind", ], ) async def test_cover_service_actions( @@ -194,9 +246,10 @@ async def test_cover_service_actions( device: FixtureDevice, service: str, command_name: str, - expected_state: CoverState, + parameters: list[Any] | None, + expected_state: CoverState | str, ) -> None: - """Test open, close, and stop cover services.""" + """Test open, close, and stop cover and tilt services.""" await setup_overkiz_integration(fixture=device.fixture) await hass.services.async_call( @@ -213,6 +266,7 @@ async def test_cover_service_actions( mock_client, device_url=device.device_url, command_name=command_name, + parameters=parameters, ) From 89649df20de88eafb2b66a974466e92d86bbb0fb Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 12:09:04 +0200 Subject: [PATCH 16/27] Fix cover controls for UpDownBioclimaticPergola in Overkiz (#170058) --- homeassistant/components/overkiz/cover.py | 3 + .../setup/local_somfy_tahoma_v2_europe.json | 65 +++++++++++++++++++ .../overkiz/snapshots/test_cover.ambr | 54 +++++++++++++++ tests/components/overkiz/test_cover.py | 29 +++++++++ 4 files changed, 151 insertions(+) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index e069b01c274393..190c34a85b8b51 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -248,6 +248,9 @@ class OverkizCoverDescription(CoverEntityDescription): OverkizCoverDescription( key=UIClass.PERGOLA, device_class=CoverDeviceClass.AWNING, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, is_closed_state=OverkizState.CORE_SLATS_OPEN_CLOSED, current_tilt_position_state=OverkizState.CORE_SLATE_ORIENTATION, set_tilt_position_command=OverkizCommand.SET_ORIENTATION, diff --git a/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_v2_europe.json b/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_v2_europe.json index fad9e83dfc1eea..e309c17526f552 100644 --- a/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_v2_europe.json +++ b/tests/components/overkiz/fixtures/setup/local_somfy_tahoma_v2_europe.json @@ -741,6 +741,71 @@ "uiClass": "Pergola" } }, + { + "deviceURL": "rts://1234-5678-3293/16757826", + "available": true, + "synced": true, + "type": 1, + "states": [], + "label": "Kitchen Pergola", + "subsystemId": 0, + "attributes": [], + "enabled": true, + "controllableName": "rts:BioclimaticPergolaRTSComponent", + "definition": { + "states": [], + "widgetName": "UpDownBioclimaticPergola", + "type": "ACTUATOR", + "attributes": [], + "commands": [ + { + "commandName": "identify", + "nparams": 0 + }, + { + "nparams": 2, + "commandName": "tiltPositive", + "paramsSig": "p1,p2" + }, + { + "nparams": 0, + "commandName": "my", + "paramsSig": "*p1" + }, + { + "nparams": 0, + "commandName": "stop", + "paramsSig": "*p1" + }, + { + "nparams": 2, + "commandName": "tiltNegative", + "paramsSig": "p1,p2" + }, + { + "nparams": 0, + "commandName": "close", + "paramsSig": "*p1" + }, + { + "nparams": 0, + "commandName": "down", + "paramsSig": "*p1" + }, + { + "nparams": 0, + "commandName": "open", + "paramsSig": "*p1" + }, + { + "nparams": 0, + "commandName": "up", + "paramsSig": "*p1" + } + ], + "uiClass": "Pergola" + } + }, { "creationTime": 1686173907452, "deviceURL": "rts://1234-5678-3293/16757362", diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index 4fc0d4c9abc1ab..d82da83ecc8e45 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -3303,6 +3303,60 @@ 'state': 'closed', }) # --- +# name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.kitchen_pergola-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.kitchen_pergola', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'rts://1234-5678-3293/16757826', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.kitchen_pergola-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'device_class': 'awning', + 'friendly_name': 'Kitchen Pergola', + 'is_closed': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.kitchen_pergola', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_cover_entities_snapshot[local_somfy_tahoma_v2_europe.json][cover.living_room_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 7d3300f3373494..883c5ca7086298 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -58,6 +58,11 @@ "io://1234-5678-3293/7614902", "cover.garden_pergola", ) +UP_DOWN_BIOCLIMATIC_PERGOLA = FixtureDevice( + "setup/local_somfy_tahoma_v2_europe.json", + "rts://1234-5678-3293/16757826", + "cover.kitchen_pergola", +) RTS = FixtureDevice( "setup/cloud_somfy_connexoon_rts_asia.json", "rts://1234-1234-6362/16730022", @@ -163,6 +168,13 @@ async def test_cover_entities_snapshot( (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + ( + UP_DOWN_BIOCLIMATIC_PERGOLA, + SERVICE_OPEN_COVER, + "open", + [0], + CoverState.OPENING, + ), (TILT_ONLY_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING), (SHUTTER, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), (AWNING, SERVICE_CLOSE_COVER, "undeploy", None, CoverState.CLOSING), @@ -176,6 +188,13 @@ async def test_cover_entities_snapshot( CoverState.CLOSING, ), (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), + ( + UP_DOWN_BIOCLIMATIC_PERGOLA, + SERVICE_CLOSE_COVER, + "close", + [0], + CoverState.CLOSING, + ), ( TILT_ONLY_VENETIAN_BLIND, SERVICE_CLOSE_COVER, @@ -189,6 +208,13 @@ async def test_cover_entities_snapshot( (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + ( + UP_DOWN_BIOCLIMATIC_PERGOLA, + SERVICE_STOP_COVER, + "stop", + [0], + STATE_UNKNOWN, + ), (TILT_ONLY_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN), ( TILT_ONLY_VENETIAN_BLIND, @@ -219,6 +245,7 @@ async def test_cover_entities_snapshot( "open-dynamic-garage-door", "open-dynamic-garage-door-ogp", "open-partial-garage-door", + "open-up-down-bioclimatic-pergola", "open-tilt-only-venetian-blind", "close-roller-shutter", "close-awning", @@ -226,6 +253,7 @@ async def test_cover_entities_snapshot( "close-dynamic-garage-door", "close-dynamic-garage-door-ogp", "close-partial-garage-door", + "close-up-down-bioclimatic-pergola", "close-tilt-only-venetian-blind", "stop-roller-shutter", "stop-awning", @@ -233,6 +261,7 @@ async def test_cover_entities_snapshot( "stop-dynamic-garage-door", "stop-dynamic-garage-door-ogp", "stop-partial-garage-door", + "stop-up-down-bioclimatic-pergola", "stop-tilt-only-venetian-blind", "open-tilt-tilt-only-venetian-blind", "close-tilt-tilt-only-venetian-blind", From 85c11672d81a6ae1e7b64c62d06d19fd14d62330 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 01:23:43 +0200 Subject: [PATCH 17/27] Bump pyOverkiz to 1.20.3 (#170060) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 8905e53603a411..52a51d0316f8c6 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.20.0"], + "requirements": ["pyoverkiz==1.20.3"], "zeroconf": [ { "name": "gateway*", diff --git a/requirements_all.txt b/requirements_all.txt index 8d33f1c21d057c..9dfef458867284 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2386,7 +2386,7 @@ pyotgw==2.2.3 pyotp==2.9.0 # homeassistant.components.overkiz -pyoverkiz==1.20.0 +pyoverkiz==1.20.3 # homeassistant.components.palazzetti pypalazzetti==0.1.20 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc7f4c1decc776..198552984764c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2045,7 +2045,7 @@ pyotgw==2.2.3 pyotp==2.9.0 # homeassistant.components.overkiz -pyoverkiz==1.20.0 +pyoverkiz==1.20.3 # homeassistant.components.palazzetti pypalazzetti==0.1.20 From 3a902e1a16da9b7405c5d139f5b7a37396fd08a2 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Fri, 8 May 2026 01:43:54 +0200 Subject: [PATCH 18/27] Bump deebot-client to 18.3.0 (#170066) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 4f5e0d7e84844a..a0c1997bc2fec8 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.11", "deebot-client==18.2.0"] + "requirements": ["py-sucks==0.9.11", "deebot-client==18.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9dfef458867284..f11c663965ca05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -794,7 +794,7 @@ debugpy==1.8.17 decora-wifi==1.4 # homeassistant.components.ecovacs -deebot-client==18.2.0 +deebot-client==18.3.0 # homeassistant.components.ihc # homeassistant.components.ohmconnect diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 198552984764c1..b68b89c64f3f33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -706,7 +706,7 @@ debugpy==1.8.17 decora-wifi==1.4 # homeassistant.components.ecovacs -deebot-client==18.2.0 +deebot-client==18.3.0 # homeassistant.components.ihc # homeassistant.components.ohmconnect From 167757762b27d621d9f4da4eed1cfb39166937df Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 16:20:10 +0200 Subject: [PATCH 19/27] Set `is_closed` state to `None` when a cover state returns "unknown" in Overkiz (#170081) --- homeassistant/components/overkiz/cover.py | 2 + .../setup/cloud_somfy_tahoma_v2_europe.json | 70 +++++++++++++++++++ .../overkiz/snapshots/test_cover.ambr | 53 ++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index 190c34a85b8b51..c9fee171c0e185 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -428,6 +428,8 @@ def is_closed(self) -> bool | None: """Return if the cover is closed.""" if is_closed_state := self.entity_description.is_closed_state: if state := self.device.states.get(is_closed_state): + if state.value == OverkizCommandParam.UNKNOWN: + return None return state.value == OverkizCommandParam.CLOSED if (position := self.current_cover_position) is not None: diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json index 767327d82d3605..866eb301aa2eed 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json @@ -644,6 +644,76 @@ "oid": "0df95043-359c-4b3d-9147-f49b2b35053c", "uiClass": "GarageDoor" }, + { + "creationTime": 1521964729000, + "lastUpdateTime": 1521964729000, + "label": "Basement Garage Door", + "deviceURL": "io://1234-1234-6233/1166864", + "shortcut": false, + "controllableName": "io:GarageOpenerIOComponent", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "setClosure", + "nparams": 1 + }, + { + "commandName": "stop", + "nparams": 0 + } + ], + "states": [ + { + "type": "ContinuousState", + "qualifiedName": "core:ClosureState" + }, + { + "type": "DiscreteState", + "values": ["closed", "open", "unknown"], + "qualifiedName": "core:OpenClosedUnknownState" + } + ], + "widgetName": "PositionableGarageDoor", + "uiProfiles": [ + "StatefulCloseableGarageOpener", + "StatefulCloseable", + "Closeable", + "OpenClose" + ], + "uiClass": "GarageDoor", + "qualifiedName": "io:GarageOpenerIOComponent", + "type": "ACTUATOR" + }, + "states": [ + { + "name": "core:OpenClosedUnknownState", + "type": 3, + "value": "unknown" + } + ], + "attributes": [ + { + "name": "core:Manufacturer", + "type": 3, + "value": "Somfy" + } + ], + "available": true, + "enabled": true, + "placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3", + "widget": "PositionableGarageDoor", + "type": 1, + "oid": "1df95043-359c-4b3d-9147-f49b2b35053d", + "uiClass": "GarageDoor" + }, { "creationTime": 1552163547000, "lastUpdateTime": 1552163547000, diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index d82da83ecc8e45..d69a4e346b3276 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -973,6 +973,59 @@ 'state': 'open', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.basement_garage_door-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.basement_garage_door', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'io://1234-1234-6233/1166864', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.basement_garage_door-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Basement Garage Door', + 'is_closed': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.basement_garage_door', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.bedroom_blinds-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ From 106f815a1e448a624ad3dab2304f72187024e318 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 10:15:20 +0200 Subject: [PATCH 20/27] Fix sensors getting wrong unit from MeasuredValueType attribute in Overkiz (#170088) --- homeassistant/components/overkiz/sensor.py | 18 +++++++++++++++++- .../overkiz/snapshots/test_sensor.ambr | 12 ++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 0636d69a3ebccf..5e80bb3074d54d 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -10,6 +10,7 @@ from pyoverkiz.types import StateType as OverkizStateType from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -606,10 +607,25 @@ def native_unit_of_measurement(self) -> str | None: if (unit := attrs[OverkizAttribute.CORE_MEASURED_VALUE_TYPE]) and ( unit_value := unit.value_as_str ): - return OVERKIZ_UNIT_TO_HA.get(unit_value, default_unit) + ha_unit = OVERKIZ_UNIT_TO_HA.get(unit_value, default_unit) + if self._is_unit_valid_for_device_class(ha_unit): + return ha_unit return default_unit + def _is_unit_valid_for_device_class(self, unit: str) -> bool: + """Check if a unit is valid for this sensor's device class. + + The device-level core:MeasuredValueType attribute describes the primary + sensor (e.g. luminance/temperature), but must not override the unit of + unrelated sensors on the same device (e.g. RSSI). + """ + if not (device_class := self.entity_description.device_class): + return True + if (valid_units := DEVICE_CLASS_UNITS.get(device_class)) is None: + return True + return unit in valid_units + class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity): """Representation of an Overkiz HomeKit Setup Code.""" diff --git a/tests/components/overkiz/snapshots/test_sensor.ambr b/tests/components/overkiz/snapshots/test_sensor.ambr index 645ac420b98b32..2e13bab9f662da 100644 --- a/tests/components/overkiz/snapshots/test_sensor.ambr +++ b/tests/components/overkiz/snapshots/test_sensor.ambr @@ -1810,7 +1810,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'io://1234-5678-1698/15702199#2-core:RSSILevelState', - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }) # --- # name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.garden_radiator_bathroom_temperature_sensor_rssi_level-state] @@ -1819,7 +1819,7 @@ 'device_class': 'signal_strength', 'friendly_name': 'Garden Radiator Bathroom Temperature Sensor RSSI level', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }), 'context': , 'entity_id': 'sensor.garden_radiator_bathroom_temperature_sensor_rssi_level', @@ -2804,7 +2804,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'io://1234-5678-1698/9253412#2-core:RSSILevelState', - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }) # --- # name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.living_room_radiator_study_temp_probe_rssi_level-state] @@ -2813,7 +2813,7 @@ 'device_class': 'signal_strength', 'friendly_name': 'Living Room Radiator Study Temp Probe RSSI level', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }), 'context': , 'entity_id': 'sensor.living_room_radiator_study_temp_probe_rssi_level', @@ -4182,7 +4182,7 @@ 'supported_features': 0, 'translation_key': None, 'unique_id': 'io://1234-5678-1698/9187218#2-core:RSSILevelState', - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }) # --- # name: test_sensor_entities_snapshot[cloud_nexity_rail_din_europe.json][sensor.study_radiator_nursery_temp_probe_rssi_level-state] @@ -4191,7 +4191,7 @@ 'device_class': 'signal_strength', 'friendly_name': 'Study Radiator Nursery Temp Probe RSSI level', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': 'dB', }), 'context': , 'entity_id': 'sensor.study_radiator_nursery_temp_probe_rssi_level', From fb7504e9df05d5df41d84240f699c317a4c9df8f Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 8 May 2026 10:21:39 +0200 Subject: [PATCH 21/27] Fix Z-Wave discovery crash with unknown node firmware version (#170090) --- .../components/zwave_js/discovery.py | 22 ++++++------ tests/components/zwave_js/conftest.py | 12 +++++++ tests/components/zwave_js/test_discovery.py | 36 ++++++++++++++++++- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 36838f53ecf620..7c47296536592e 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -1284,19 +1284,19 @@ def async_discover_single_value( continue # check firmware_version_range - if schema.firmware_version_range is not None and ( - ( + if schema.firmware_version_range is not None: + # skip schema if device firmware version is unknown + if value.node.firmware_version is None: + continue + node_firmware = AwesomeVersion(value.node.firmware_version) + if ( schema.firmware_version_range.min is not None - and schema.firmware_version_range.min_ver - > AwesomeVersion(value.node.firmware_version) - ) - or ( + and schema.firmware_version_range.min_ver > node_firmware + ) or ( schema.firmware_version_range.max is not None - and schema.firmware_version_range.max_ver - < AwesomeVersion(value.node.firmware_version) - ) - ): - continue + and schema.firmware_version_range.max_ver < node_firmware + ): + continue # check device_class_generic # If the value has an endpoint but it is missing on the node diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 084c965e38ad6d..39cd50021d164d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1513,3 +1513,15 @@ def fibaro_fgms001_v2_8_fixture( node = Node(client, fibaro_fgms001_v2_8_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="fibaro_fgms001_unknown_firmware") +def fibaro_fgms001_unknown_firmware_fixture( + client: MagicMock, fibaro_fgms001_v2_8_state: NodeDataType +) -> Node: + """Load FGMS001 node with no reported firmware version.""" + state = copy.deepcopy(fibaro_fgms001_v2_8_state) + state.pop("firmwareVersion", None) + node = Node(client, state) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index f5ec08d569401e..e08b14760182df 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -33,7 +33,7 @@ DynamicCurrentTempClimateDataTemplate, ) from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY, ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -666,6 +666,40 @@ async def test_nabu_casa_zwa2_legacy( ) +@pytest.mark.parametrize("platforms", [[Platform.BINARY_SENSOR, Platform.LIGHT]]) +async def test_fibaro_fgms001_unknown_firmware_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + client: MagicMock, + fibaro_fgms001_unknown_firmware: Node, + integration: MockConfigEntry, +) -> None: + """Test setup completes when an FGMS001 node has no firmware version. + + Regression test for a crash where comparing AwesomeVersion(None) to a + schema's firmware_version_range raised AwesomeVersionCompareException + and aborted setup of the whole config entry. + """ + assert integration.state is ConfigEntryState.LOADED + + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, fibaro_fgms001_unknown_firmware)} + ) + assert device is not None + + entries = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + motion_entries = [ + entry + for entry in entries + if entry.domain == BINARY_SENSOR_DOMAIN + and entry.original_device_class == BinarySensorDeviceClass.MOTION + ] + assert motion_entries == [] + + @pytest.mark.parametrize("platforms", [[Platform.BINARY_SENSOR, Platform.LIGHT]]) async def test_fibaro_fgms001_v2_8_motion_discovery( hass: HomeAssistant, From ba18cded30efad87135da6cb72327802d472470f Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 8 May 2026 12:18:19 +0200 Subject: [PATCH 22/27] Bump ZHA to 1.3.1 (#170095) --- 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 80d044221df21a..83282af03028b2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -23,7 +23,7 @@ "universal_silabs_flasher", "serialx" ], - "requirements": ["zha==1.3.0"], + "requirements": ["zha==1.3.1"], "usb": [ { "description": "*2652*", diff --git a/requirements_all.txt b/requirements_all.txt index f11c663965ca05..d8e64d0a19c0d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3404,7 +3404,7 @@ zeroconf==0.148.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==1.3.0 +zha==1.3.1 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b68b89c64f3f33..f5f5d35ec88054 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2895,7 +2895,7 @@ zeroconf==0.148.0 zeversolar==0.3.2 # homeassistant.components.zha -zha==1.3.0 +zha==1.3.1 # homeassistant.components.zinvolt zinvolt==0.4.3 From 5f98d5ae5273779320a9af75ec54b7580cf45d15 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Fri, 8 May 2026 13:17:47 +0200 Subject: [PATCH 23/27] Bump python-bsblan to 5.2.1 (#170100) --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 4b65dca61b597e..4f36d103a3fa2a 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["bsblan"], "quality_scale": "silver", - "requirements": ["python-bsblan==5.2.0"], + "requirements": ["python-bsblan==5.2.1"], "zeroconf": [ { "name": "bsb-lan*", diff --git a/requirements_all.txt b/requirements_all.txt index d8e64d0a19c0d5..4f20d2b0aeeda4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2566,7 +2566,7 @@ python-awair==0.2.5 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==5.2.0 +python-bsblan==5.2.1 # homeassistant.components.citybikes python-citybikes==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5f5d35ec88054..3dffc28cc138d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2198,7 +2198,7 @@ python-MotionMount==2.3.0 python-awair==0.2.5 # homeassistant.components.bsblan -python-bsblan==5.2.0 +python-bsblan==5.2.1 # homeassistant.components.citybikes python-citybikes==0.3.3 From 4940a0abae6f99432a1a84709675b296f966ff9a Mon Sep 17 00:00:00 2001 From: bkobus-bbx Date: Fri, 8 May 2026 16:21:54 +0200 Subject: [PATCH 24/27] Bump blebox_uniapi to v2.5.3 (#170115) --- homeassistant/components/blebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 885cfb81038d40..eb301bd926c351 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["blebox_uniapi"], - "requirements": ["blebox-uniapi==2.5.2"], + "requirements": ["blebox-uniapi==2.5.3"], "zeroconf": ["_bbxsrv._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f20d2b0aeeda4..76095b03e2804c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,7 +654,7 @@ bleak-retry-connector==4.6.0 bleak==2.1.1 # homeassistant.components.blebox -blebox-uniapi==2.5.2 +blebox-uniapi==2.5.3 # homeassistant.components.blink blinkpy==0.25.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dffc28cc138d0..4a10d2a6a99929 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ bleak-retry-connector==4.6.0 bleak==2.1.1 # homeassistant.components.blebox -blebox-uniapi==2.5.2 +blebox-uniapi==2.5.3 # homeassistant.components.blink blinkpy==0.25.2 From a23131efc8499e32e4759091cb6353cd5ec15945 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 20:42:50 +0200 Subject: [PATCH 25/27] Fix is_closed state for DynamicGate covers in Overkiz (#170130) --- homeassistant/components/overkiz/cover.py | 13 ++- .../setup/cloud_somfy_tahoma_v2_europe.json | 100 ++++++++++++++++++ .../overkiz/snapshots/test_cover.ambr | 53 ++++++++++ tests/components/overkiz/test_cover.py | 11 ++ 4 files changed, 176 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index c9fee171c0e185..3619aface73644 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -158,6 +158,17 @@ class OverkizCoverDescription(CoverEntityDescription): stop_command=OverkizCommand.STOP, is_closed_state=OverkizState.CORE_OPEN_CLOSED_PARTIAL, ), + # Needs override since DiscreteGateWithPedestrianPosition reports + # core:OpenClosedPedestrianState instead of core:OpenClosedState + # uiClass is Gate + OverkizCoverDescription( + key=UIWidget.DISCRETE_GATE_WITH_PEDESTRIAN_POSITION, + device_class=CoverDeviceClass.GATE, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN, + stop_command=OverkizCommand.STOP, + ), # Needs override to support this Generic device (rts:GenericRTSComponent) # uiClass is Generic (not mapped to cover as this is a Generic device class) OverkizCoverDescription( @@ -242,7 +253,7 @@ class OverkizCoverDescription(CoverEntityDescription): device_class=CoverDeviceClass.GATE, open_command=OverkizCommand.OPEN, close_command=OverkizCommand.CLOSE, - is_closed_state=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN, + is_closed_state=OverkizState.CORE_OPEN_CLOSED, stop_command=OverkizCommand.STOP, ), OverkizCoverDescription( diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json index 866eb301aa2eed..d69c75e548ff1c 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_tahoma_v2_europe.json @@ -7672,6 +7672,106 @@ "type": 1, "oid": "f1a2b3c4-d5e6-7890-abcd-ef1234567890", "uiClass": "GarageDoor" + }, + { + "creationTime": 1660551260000, + "lastUpdateTime": 1660551260000, + "label": "OGP Gate", + "deviceURL": "ogp://1234-1234-6233/10410217", + "shortcut": false, + "controllableName": "ogp:Gate", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 0 + }, + { + "commandName": "identify", + "nparams": 0 + }, + { + "commandName": "open", + "nparams": 0 + }, + { + "commandName": "setName", + "nparams": 1 + }, + { + "commandName": "stop", + "nparams": 0 + } + ], + "states": [ + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:AvailabilityState" + }, + { + "type": "DataState", + "qualifiedName": "core:NameState" + }, + { + "type": "DiscreteState", + "values": ["closed", "open"], + "qualifiedName": "core:OpenClosedState" + }, + { + "type": "DiscreteState", + "values": ["available", "unavailable"], + "qualifiedName": "core:StatusState" + } + ], + "dataProperties": [], + "widgetName": "DynamicGate", + "uiProfiles": ["StatefulOpenClose", "OpenClose"], + "uiClass": "Gate", + "qualifiedName": "ogp:Gate", + "type": "ACTUATOR" + }, + "states": [ + { + "name": "core:NameState", + "type": 3, + "value": "OGP Gate" + }, + { + "name": "core:AvailabilityState", + "type": 3, + "value": "available" + }, + { + "name": "core:StatusState", + "type": 3, + "value": "available" + }, + { + "name": "core:OpenClosedState", + "type": 3, + "value": "open" + } + ], + "attributes": [ + { + "name": "core:Technology", + "type": 3, + "value": "io2way" + }, + { + "name": "core:Manufacturer", + "type": 3, + "value": "Somfy" + } + ], + "available": true, + "enabled": true, + "placeOID": "bcbb34ef-2241-43a1-9c5b-523aa0563ec3", + "type": 1, + "widget": "DynamicGate", + "oid": "a8d3e9f1-4b2c-4d5e-8f6a-1234567890ab", + "uiClass": "Gate" } ], "zones": [], diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index d69a4e346b3276..7d0abf5fa1f750 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -1725,6 +1725,59 @@ 'state': 'closed', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_gate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.ogp_gate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'ogp://1234-1234-6233/10410217', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.ogp_gate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gate', + 'friendly_name': 'OGP Gate', + 'is_closed': False, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.ogp_gate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_tahoma_v2_europe.json][cover.partial_garage_door-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 883c5ca7086298..5967303a3342ca 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -120,6 +120,11 @@ "io://1234-1234-6233/7433515", "cover.partial_garage_door", ) +DYNAMIC_GATE = FixtureDevice( + "setup/cloud_somfy_tahoma_v2_europe.json", + "ogp://1234-1234-6233/10410217", + "cover.ogp_gate", +) SNAPSHOT_FIXTURES = [ AWNING, @@ -167,6 +172,7 @@ async def test_cover_entities_snapshot( (GARAGE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), (DYNAMIC_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), + (DYNAMIC_GATE, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), (PARTIAL_GARAGE_DOOR, SERVICE_OPEN_COVER, "open", None, CoverState.OPENING), ( UP_DOWN_BIOCLIMATIC_PERGOLA, @@ -187,6 +193,7 @@ async def test_cover_entities_snapshot( None, CoverState.CLOSING, ), + (DYNAMIC_GATE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), (PARTIAL_GARAGE_DOOR, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), ( UP_DOWN_BIOCLIMATIC_PERGOLA, @@ -207,6 +214,7 @@ async def test_cover_entities_snapshot( (GARAGE, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (DYNAMIC_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (DYNAMIC_GARAGE_DOOR_OGP, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), + (DYNAMIC_GATE, SERVICE_STOP_COVER, "stop", None, CoverState.OPEN), (PARTIAL_GARAGE_DOOR, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), ( UP_DOWN_BIOCLIMATIC_PERGOLA, @@ -244,6 +252,7 @@ async def test_cover_entities_snapshot( "open-garage-door", "open-dynamic-garage-door", "open-dynamic-garage-door-ogp", + "open-dynamic-gate", "open-partial-garage-door", "open-up-down-bioclimatic-pergola", "open-tilt-only-venetian-blind", @@ -252,6 +261,7 @@ async def test_cover_entities_snapshot( "close-garage-door", "close-dynamic-garage-door", "close-dynamic-garage-door-ogp", + "close-dynamic-gate", "close-partial-garage-door", "close-up-down-bioclimatic-pergola", "close-tilt-only-venetian-blind", @@ -260,6 +270,7 @@ async def test_cover_entities_snapshot( "stop-garage-door", "stop-dynamic-garage-door", "stop-dynamic-garage-door-ogp", + "stop-dynamic-gate", "stop-partial-garage-door", "stop-up-down-bioclimatic-pergola", "stop-tilt-only-venetian-blind", From 18ea40c46d833935263bb1df2983cb88479124e1 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 8 May 2026 12:22:23 +0200 Subject: [PATCH 26/27] Fix tilt support for UpDownVenetianBlind (rts:VenetianBlindRTSComponent) in Overkiz (#170047) --- homeassistant/components/overkiz/cover.py | 14 ++++ .../setup/cloud_somfy_connexoon_rts_asia.json | 84 +++++++++++++++++++ .../overkiz/snapshots/test_cover.ambr | 54 ++++++++++++ tests/components/overkiz/test_cover.py | 35 ++++++++ 4 files changed, 187 insertions(+) diff --git a/homeassistant/components/overkiz/cover.py b/homeassistant/components/overkiz/cover.py index 3619aface73644..f8c587218c4f4b 100644 --- a/homeassistant/components/overkiz/cover.py +++ b/homeassistant/components/overkiz/cover.py @@ -132,6 +132,20 @@ class OverkizCoverDescription(CoverEntityDescription): close_tilt_command_args=(15, 1), # position (1-127), speed (1-15) stop_tilt_command=OverkizCommand.STOP, ), + # Needs override to support very specific tilt commands (rts:VenetianBlindRTSComponent) + # uiClass is VenetianBlind + OverkizCoverDescription( + key=UIWidget.UP_DOWN_VENETIAN_BLIND, + device_class=CoverDeviceClass.BLIND, + open_command=OverkizCommand.OPEN, + close_command=OverkizCommand.CLOSE, + stop_command=OverkizCommand.STOP, + open_tilt_command=OverkizCommand.TILT_POSITIVE, + open_tilt_command_args=(15, 1), # position (1-127), speed (1-15) + close_tilt_command=OverkizCommand.TILT_NEGATIVE, + close_tilt_command_args=(15, 1), # position (1-127), speed (1-15) + stop_tilt_command=OverkizCommand.STOP, + ), # Needs override since PositionableGarageDoor reports # core:OpenClosedUnknownState instead of core:OpenClosedState # uiClass is GarageDoor diff --git a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json index 8ea89c840ca9e3..d3559a1344f21d 100644 --- a/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json +++ b/tests/components/overkiz/fixtures/setup/cloud_somfy_connexoon_rts_asia.json @@ -380,6 +380,90 @@ "type": 1, "oid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "uiClass": "VenetianBlind" + }, + { + "creationTime": 1613676700000, + "lastUpdateTime": 1613676700000, + "label": "Office Venetian Blind", + "deviceURL": "rts://1234-1234-6362/16747291", + "shortcut": false, + "controllableName": "rts:VenetianBlindRTSComponent", + "definition": { + "commands": [ + { + "commandName": "close", + "nparams": 1 + }, + { + "commandName": "down", + "nparams": 1 + }, + { + "commandName": "identify", + "nparams": 0 + }, + { + "commandName": "my", + "nparams": 1 + }, + { + "commandName": "open", + "nparams": 1 + }, + { + "commandName": "rest", + "nparams": 1 + }, + { + "commandName": "stop", + "nparams": 1 + }, + { + "commandName": "test", + "nparams": 0 + }, + { + "commandName": "up", + "nparams": 1 + }, + { + "commandName": "moveOf", + "nparams": 2 + }, + { + "commandName": "openConfiguration", + "nparams": 1 + }, + { + "commandName": "tiltNegative", + "nparams": 2 + }, + { + "commandName": "tiltPositive", + "nparams": 2 + } + ], + "states": [], + "dataProperties": [ + { + "value": "0", + "qualifiedName": "core:identifyInterval" + } + ], + "widgetName": "UpDownVenetianBlind", + "uiProfiles": ["OpenCloseBlind", "OpenClose"], + "uiClass": "VenetianBlind", + "qualifiedName": "rts:VenetianBlindRTSComponent", + "type": "ACTUATOR" + }, + "attributes": [], + "available": true, + "enabled": true, + "placeOID": "6133b4a0-f514-4553-b635-d1b7beb7e7b2", + "widget": "UpDownVenetianBlind", + "type": 1, + "oid": "3496a041-cafc-4d5d-ab3b-7947985812dc", + "uiClass": "VenetianBlind" } ], "zones": [], diff --git a/tests/components/overkiz/snapshots/test_cover.ambr b/tests/components/overkiz/snapshots/test_cover.ambr index 7d0abf5fa1f750..c5e9f9a7fe0b9e 100644 --- a/tests/components/overkiz/snapshots/test_cover.ambr +++ b/tests/components/overkiz/snapshots/test_cover.ambr @@ -431,6 +431,60 @@ 'state': 'unknown', }) # --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.office_venetian_blind-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.office_venetian_blind', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'overkiz', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'rts://1234-1234-6362/16747291', + 'unit_of_measurement': None, + }) +# --- +# name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.office_venetian_blind-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'assumed_state': True, + 'device_class': 'blind', + 'friendly_name': 'Office Venetian Blind', + 'is_closed': None, + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.office_venetian_blind', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_cover_entities_snapshot[cloud_somfy_connexoon_rts_asia.json][cover.patio_shutter-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 5967303a3342ca..64bfbb9d69a5d2 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -105,6 +105,11 @@ "rts://1234-1234-6362/16730044", "cover.jaloezie", ) +UP_DOWN_VENETIAN_BLIND = FixtureDevice( + "setup/cloud_somfy_connexoon_rts_asia.json", + "rts://1234-1234-6362/16747291", + "cover.office_venetian_blind", +) DYNAMIC_GARAGE_DOOR = FixtureDevice( "setup/cloud_somfy_tahoma_v2_europe.json", "io://1234-1234-6233/16730050", @@ -182,6 +187,7 @@ async def test_cover_entities_snapshot( CoverState.OPENING, ), (TILT_ONLY_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING), + (UP_DOWN_VENETIAN_BLIND, SERVICE_OPEN_COVER, "open", [0], CoverState.OPENING), (SHUTTER, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), (AWNING, SERVICE_CLOSE_COVER, "undeploy", None, CoverState.CLOSING), (GARAGE, SERVICE_CLOSE_COVER, "close", None, CoverState.CLOSING), @@ -209,6 +215,7 @@ async def test_cover_entities_snapshot( [0], CoverState.CLOSING, ), + (UP_DOWN_VENETIAN_BLIND, SERVICE_CLOSE_COVER, "close", [0], CoverState.CLOSING), (SHUTTER, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (AWNING, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), (GARAGE, SERVICE_STOP_COVER, "stop", None, CoverState.CLOSED), @@ -245,6 +252,28 @@ async def test_cover_entities_snapshot( [0], STATE_UNKNOWN, ), + (UP_DOWN_VENETIAN_BLIND, SERVICE_STOP_COVER, "stop", [0], STATE_UNKNOWN), + ( + UP_DOWN_VENETIAN_BLIND, + SERVICE_OPEN_COVER_TILT, + "tiltPositive", + [15, 1], + CoverState.OPENING, + ), + ( + UP_DOWN_VENETIAN_BLIND, + SERVICE_CLOSE_COVER_TILT, + "tiltNegative", + [15, 1], + CoverState.CLOSING, + ), + ( + UP_DOWN_VENETIAN_BLIND, + SERVICE_STOP_COVER_TILT, + "stop", + [0], + STATE_UNKNOWN, + ), ], ids=[ "open-roller-shutter", @@ -256,6 +285,7 @@ async def test_cover_entities_snapshot( "open-partial-garage-door", "open-up-down-bioclimatic-pergola", "open-tilt-only-venetian-blind", + "open-venetian-blind-rts", "close-roller-shutter", "close-awning", "close-garage-door", @@ -265,6 +295,7 @@ async def test_cover_entities_snapshot( "close-partial-garage-door", "close-up-down-bioclimatic-pergola", "close-tilt-only-venetian-blind", + "close-venetian-blind-rts", "stop-roller-shutter", "stop-awning", "stop-garage-door", @@ -277,6 +308,10 @@ async def test_cover_entities_snapshot( "open-tilt-tilt-only-venetian-blind", "close-tilt-tilt-only-venetian-blind", "stop-tilt-tilt-only-venetian-blind", + "stop-venetian-blind-rts", + "open-tilt-venetian-blind-rts", + "close-tilt-venetian-blind-rts", + "stop-tilt-venetian-blind-rts", ], ) async def test_cover_service_actions( From dd0cdc4fc450e31a1c921361f85b715168f31bc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 May 2026 18:54:08 +0000 Subject: [PATCH 27/27] Bump version to 2026.5.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4ebe4ddf6d0d1d..ddb646d5223ede 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -17,7 +17,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2026 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2) diff --git a/pyproject.toml b/pyproject.toml index e31b984832f24f..f0a068da6754c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2026.5.0" +version = "2026.5.1" license = "Apache-2.0" license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] description = "Open-source home automation platform running on Python 3."