From 270c728ae470fbc125016f01b6f749d79ad29e95 Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Tue, 7 Jul 2026 23:31:08 +0200 Subject: [PATCH 1/6] Change toggle service icon from play-pause to power (#175901) --- homeassistant/components/media_player/icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/icons.json b/homeassistant/components/media_player/icons.json index 789c2c064f5fd..fad4025e6f7fc 100644 --- a/homeassistant/components/media_player/icons.json +++ b/homeassistant/components/media_player/icons.json @@ -113,7 +113,7 @@ "service": "mdi:shuffle" }, "toggle": { - "service": "mdi:play-pause" + "service": "mdi:power" }, "turn_off": { "service": "mdi:power" From 94c37d9d6228d71ffece0f00dc3bd0b58fd8c3aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Jul 2026 23:31:52 +0200 Subject: [PATCH 2/6] Move DEFAULT_RADIUS to const.py to drop zone inline imports in core_config (#175896) Co-authored-by: Claude --- homeassistant/components/zone/__init__.py | 2 +- homeassistant/const.py | 3 +++ homeassistant/core_config.py | 6 +----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 18f7e6c34ad5e..b0a14960fca7e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -20,6 +20,7 @@ CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, + DEFAULT_RADIUS, EVENT_CORE_CONFIG_UPDATE, SERVICE_RELOAD, STATE_UNAVAILABLE, @@ -49,7 +50,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PASSIVE = False -DEFAULT_RADIUS = 100 ENTITY_ID_FORMAT = "zone.{}" ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) diff --git a/homeassistant/const.py b/homeassistant/const.py index 326742e062e1d..587f83d70f334 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -48,6 +48,9 @@ # If no name is specified DEVICE_DEFAULT_NAME: Final = "Unnamed Device" +# Default radius of the Home Zone (in meters) +DEFAULT_RADIUS: Final = 100 + # Max characters for data stored in the recorder (changes to these limits would require # a database migration) MAX_LENGTH_EVENT_EVENT_TYPE: Final = 64 diff --git a/homeassistant/core_config.py b/homeassistant/core_config.py index de958b97d417b..82413efcc7216 100644 --- a/homeassistant/core_config.py +++ b/homeassistant/core_config.py @@ -49,6 +49,7 @@ CONF_UNIT_SYSTEM, CONF_URL, CONF_USERNAME, + DEFAULT_RADIUS, EVENT_CORE_CONFIG_UPDATE, KEY_DATA_LOGGING_DISABLED_REASON, LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, @@ -538,8 +539,6 @@ class Config: def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" - from .components.zone import DEFAULT_RADIUS # noqa: PLC0415 - self.hass = hass self.latitude: float = 0 @@ -858,9 +857,6 @@ async def _async_migrate_func( old_data: dict[str, Any], ) -> dict[str, Any]: """Migrate to the new version.""" - - from .components.zone import DEFAULT_RADIUS # noqa: PLC0415 - data = old_data if old_major_version == 1 and old_minor_version < 2: # In 1.2, we remove support for "imperial", replaced by "us_customary" From 12b69b9b18f9fd9276c0cfaebd01a4fc9c7ea202 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 7 Jul 2026 23:49:03 +0200 Subject: [PATCH 3/6] Add diagnostics support for stiebel_eltron integration (#175905) Co-authored-by: Claude Fable 5 --- .../components/stiebel_eltron/diagnostics.py | 35 +++++++++++++++++++ .../stiebel_eltron/quality_scale.yaml | 2 +- tests/components/stiebel_eltron/conftest.py | 7 +++- .../snapshots/test_diagnostics.ambr | 22 ++++++++++++ .../stiebel_eltron/test_diagnostics.py | 26 ++++++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/stiebel_eltron/diagnostics.py create mode 100644 tests/components/stiebel_eltron/snapshots/test_diagnostics.ambr create mode 100644 tests/components/stiebel_eltron/test_diagnostics.py diff --git a/homeassistant/components/stiebel_eltron/diagnostics.py b/homeassistant/components/stiebel_eltron/diagnostics.py new file mode 100644 index 0000000000000..483968dca5c60 --- /dev/null +++ b/homeassistant/components/stiebel_eltron/diagnostics.py @@ -0,0 +1,35 @@ +"""Diagnostics support for STIEBEL ELTRON.""" + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from . import StiebelEltronConfigEntry + +TO_REDACT = {CONF_HOST} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: StiebelEltronConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator = entry.runtime_data + + return { + "entry_data": async_redact_data(entry.data, TO_REDACT), + "model": coordinator.device_info["model"], + "modbus": { + "is_connected": coordinator.api_client.is_connected, + }, + "data": { + "current_temp": coordinator.api_client.get_current_temp(), + "target_temp": coordinator.api_client.get_target_temp(), + "current_humidity": coordinator.api_client.get_current_humidity(), + "operating_mode": coordinator.api_client.get_operation().name, + "heating_status": coordinator.api_client.get_heating_status(), + "cooling_status": coordinator.api_client.get_cooling_status(), + "filter_alarm": coordinator.api_client.get_filter_alarm_status(), + }, + } diff --git a/homeassistant/components/stiebel_eltron/quality_scale.yaml b/homeassistant/components/stiebel_eltron/quality_scale.yaml index fb718177cb621..83242da57564f 100644 --- a/homeassistant/components/stiebel_eltron/quality_scale.yaml +++ b/homeassistant/components/stiebel_eltron/quality_scale.yaml @@ -47,7 +47,7 @@ rules: # Gold devices: done - diagnostics: todo + diagnostics: done discovery-update-info: todo discovery: todo docs-data-update: todo diff --git a/tests/components/stiebel_eltron/conftest.py b/tests/components/stiebel_eltron/conftest.py index 5c43aab971963..489377acd783c 100644 --- a/tests/components/stiebel_eltron/conftest.py +++ b/tests/components/stiebel_eltron/conftest.py @@ -44,9 +44,14 @@ def mock_lwz_api() -> Generator[MagicMock]: api_client.get_current_temp = MagicMock(return_value=21.0) api_client.get_current_humidity = MagicMock(return_value=45.0) api_client.get_operation = MagicMock(return_value=OperatingMode.AUTOMATIC) + api_client.get_heating_status = MagicMock(return_value=True) + api_client.get_cooling_status = MagicMock(return_value=False) api_client.get_filter_alarm_status = MagicMock(return_value=False) - api_client.connect = AsyncMock() + def _connect() -> None: + api_client.is_connected = True + + api_client.connect = AsyncMock(side_effect=_connect) api_client.close = AsyncMock() api_client.async_update = AsyncMock() api_client.set_operation = AsyncMock() diff --git a/tests/components/stiebel_eltron/snapshots/test_diagnostics.ambr b/tests/components/stiebel_eltron/snapshots/test_diagnostics.ambr new file mode 100644 index 0000000000000..004cc7fa84350 --- /dev/null +++ b/tests/components/stiebel_eltron/snapshots/test_diagnostics.ambr @@ -0,0 +1,22 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'cooling_status': False, + 'current_humidity': 45.0, + 'current_temp': 21.0, + 'filter_alarm': False, + 'heating_status': True, + 'operating_mode': 'AUTOMATIC', + 'target_temp': 22.5, + }), + 'entry_data': dict({ + 'host': '**REDACTED**', + 'port': 502, + }), + 'modbus': dict({ + 'is_connected': True, + }), + 'model': 'LWZ', + }) +# --- diff --git a/tests/components/stiebel_eltron/test_diagnostics.py b/tests/components/stiebel_eltron/test_diagnostics.py new file mode 100644 index 0000000000000..bc1c820fb1d0e --- /dev/null +++ b/tests/components/stiebel_eltron/test_diagnostics.py @@ -0,0 +1,26 @@ +"""Tests for STIEBEL ELTRON diagnostics.""" + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test config entry diagnostics.""" + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) + == snapshot + ) From 676f18c0960dbbaa0f0bac70f7b29d0014239be1 Mon Sep 17 00:00:00 2001 From: Vincent Courcelle <2070309+tubededentifrice@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:44:36 +0400 Subject: [PATCH 4/6] Add Roborock Q10 map image entity (#173883) --- homeassistant/components/roborock/icons.json | 5 ++ homeassistant/components/roborock/image.py | 79 +++++++++++++++---- .../components/roborock/strings.json | 5 ++ tests/components/roborock/conftest.py | 1 + tests/components/roborock/test_image.py | 56 ++++++++++++- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/roborock/icons.json b/homeassistant/components/roborock/icons.json index 76ba0b47a925d..bc018d23789b8 100644 --- a/homeassistant/components/roborock/icons.json +++ b/homeassistant/components/roborock/icons.json @@ -40,6 +40,11 @@ "default": "mdi:brush" } }, + "image": { + "map": { + "default": "mdi:floor-plan" + } + }, "number": { "volume": { "default": "mdi:volume-source" diff --git a/homeassistant/components/roborock/image.py b/homeassistant/components/roborock/image.py index 6c9e2327e4cdf..913eab5c8b1c6 100644 --- a/homeassistant/components/roborock/image.py +++ b/homeassistant/components/roborock/image.py @@ -14,13 +14,15 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import dt as dt_util from .coordinator import ( + RoborockB01Q10UpdateCoordinator, RoborockConfigEntry, RoborockCoordinatorType, RoborockDataUpdateCoordinator, ) -from .entity import RoborockCoordinatedEntityV1 +from .entity import RoborockCoordinatedEntityB01Q10, RoborockCoordinatedEntityV1 _LOGGER = logging.getLogger(__name__) @@ -40,20 +42,22 @@ def async_add_coordinator_entities( coordinator: RoborockCoordinatorType, ) -> None: """Add entities for a specific coordinator.""" - if not isinstance(coordinator, RoborockDataUpdateCoordinator): - return - entities = [ - RoborockMap( - config_entry, - coordinator, - coordinator.properties_api.home, - map_info.map_flag, - map_info.name, + entities: list[ImageEntity] = [] + if isinstance(coordinator, RoborockDataUpdateCoordinator): + entities.extend( + RoborockMap( + config_entry, + coordinator, + coordinator.properties_api.home, + map_info.map_flag, + map_info.name, + ) + for map_info in ( + coordinator.properties_api.home.home_map_info or {} + ).values() ) - for map_info in ( - coordinator.properties_api.home.home_map_info or {} - ).values() - ] + elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator): + entities.append(RoborockMapQ10(coordinator)) async_add_entities(entities) for coordinator in coordinators.values(): @@ -134,3 +138,50 @@ async def async_image(self) -> bytes | None: if (map_content := self._map_content) is None: raise HomeAssistantError("Map flag not found in coordinator maps") return map_content.image_content + + +class RoborockMapQ10(RoborockCoordinatedEntityB01Q10, ImageEntity): + """A class to let you visualize the current map of a Q10 device. + + The Q10 pushes its current map over MQTT rather than serving it on + request, and the multi-map list is not reachable on this channel, so the + device exposes a single push-driven map entity. + """ + + _attr_content_type = "image/png" + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_translation_key = "map" + + def __init__(self, coordinator: RoborockB01Q10UpdateCoordinator) -> None: + """Initialize a Roborock Q10 map.""" + RoborockCoordinatedEntityB01Q10.__init__( + self, f"map_{coordinator.duid_slug}", coordinator + ) + ImageEntity.__init__(self, coordinator.hass) + self._map_trait = coordinator.api.map + self._cached_map: bytes | None = None + + @override + async def async_added_to_hass(self) -> None: + """Register a trait listener for push-based map updates.""" + await super().async_added_to_hass() + self.async_on_remove( + self._map_trait.add_update_listener(self._handle_map_update) + ) + # Pick up a map that was pushed before the entity was added. + self._handle_map_update() + + @callback + def _handle_map_update(self) -> None: + """Cache the newly pushed map if its content changed.""" + image_content = self._map_trait.image_content + if image_content is None or image_content == self._cached_map: + return + self._cached_map = image_content + self._attr_image_last_updated = dt_util.utcnow() + self.async_write_ha_state() + + @override + async def async_image(self) -> bytes | None: + """Get the cached image.""" + return self._cached_map diff --git a/homeassistant/components/roborock/strings.json b/homeassistant/components/roborock/strings.json index 7f4b524dc0880..3d496b9738bcc 100644 --- a/homeassistant/components/roborock/strings.json +++ b/homeassistant/components/roborock/strings.json @@ -125,6 +125,11 @@ "name": "Start" } }, + "image": { + "map": { + "name": "Map" + } + }, "number": { "volume": { "name": "Volume" diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 0a1d4aeb3ec08..3f73ade734c45 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -239,6 +239,7 @@ def create_b01_q10_trait() -> Mock: q10_trait.button_light.disable = AsyncMock() q10_trait.map = Mock() + q10_trait.map.image_content = b"\x89PNG-q10" q10_trait.map.rooms = [ Q10Room(id=9, raw_name="rr_bedroom", pixel_value=36, pixel_count=100), Q10Room(id=10, raw_name="rr_living_room", pixel_value=40, pixel_count=200), diff --git a/tests/components/roborock/test_image.py b/tests/components/roborock/test_image.py index 7ddd50de97ab1..6168c04d7b208 100644 --- a/tests/components/roborock/test_image.py +++ b/tests/components/roborock/test_image.py @@ -6,6 +6,7 @@ import logging from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from roborock import MultiMapsList, RoborockException from roborock.data import RoborockStateCode @@ -45,7 +46,7 @@ async def test_floorplan_image( fake_devices: list[FakeDevice], ) -> None: """Test floor plan map image is correctly set up.""" - assert len(hass.states.async_all("image")) == 4 + assert len(hass.states.async_all("image")) == 5 assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None # Load the image on demand @@ -131,7 +132,7 @@ async def test_map_status_change( fake_vacuum: FakeDevice, ) -> None: """Test floor plan map image is correctly updated on status change.""" - assert len(hass.states.async_all("image")) == 4 + assert len(hass.states.async_all("image")) == 5 assert hass.states.get("image.roborock_s7_maxv_upstairs") is not None client = await hass_client() @@ -181,6 +182,7 @@ async def test_map_status_change( "image.roborock_s7_2_upstairs", "image.roborock_s7_maxv_downstairs", "image.roborock_s7_maxv_upstairs", + "image.roborock_q10_s5_map", }, ), ( @@ -191,6 +193,7 @@ async def test_map_status_change( # Expect default names based on map flags "image.roborock_s7_maxv_map_0", "image.roborock_s7_maxv_map_1", + "image.roborock_q10_s5_map", }, ), ], @@ -222,3 +225,52 @@ async def test_image_entity_naming( assert { state.entity_id for state in hass.states.async_all("image") } == expected_entity_ids + + +async def test_q10_map_image( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, + fake_q10_vacuum: FakeDevice, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the push-driven Q10 map image.""" + entity_id = "image.roborock_q10_s5_map" + assert hass.states.get(entity_id) is not None + + # The map pushed before startup is served + client = await hass_client() + resp = await client.get(f"/api/image_proxy/{entity_id}") + assert resp.status == HTTPStatus.OK + assert await resp.read() == b"\x89PNG-q10" + + assert fake_q10_vacuum.b01_q10_properties is not None + map_trait = fake_q10_vacuum.b01_q10_properties.map + + def push_update() -> None: + for call in map_trait.add_update_listener.call_args_list: + call.args[0]() + + # A push that does not change the map content must not update the entity + state = hass.states.get(entity_id) + assert state is not None + last_updated = state.state + freezer.tick(timedelta(seconds=30)) + push_update() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert state.state == last_updated + + # The device pushes an updated map + freezer.tick(timedelta(seconds=30)) + map_trait.image_content = b"\x89PNG-q10-new" + push_update() + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state is not None + assert state.state != last_updated + resp = await client.get(f"/api/image_proxy/{entity_id}") + assert resp.status == HTTPStatus.OK + assert await resp.read() == b"\x89PNG-q10-new" From 53f073677c7b87ab48bf5836d489c9e4d1262b26 Mon Sep 17 00:00:00 2001 From: Vincent Courcelle <2070309+tubededentifrice@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:48:00 +0400 Subject: [PATCH 5/6] Add Roborock Q10 volume number entity (#175732) --- homeassistant/components/roborock/number.py | 111 +++++++++++++++++--- tests/components/roborock/conftest.py | 9 ++ tests/components/roborock/test_number.py | 68 ++++++++++++ 3 files changed, 176 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/roborock/number.py b/homeassistant/components/roborock/number.py index 67dff3ae6b7d2..e05884d945b5d 100644 --- a/homeassistant/components/roborock/number.py +++ b/homeassistant/components/roborock/number.py @@ -5,6 +5,8 @@ import logging from typing import Any, override +from roborock.devices.traits.b01 import Q10PropertiesApi +from roborock.devices.traits.b01.q10 import SoundVolumeTrait from roborock.devices.traits.v1 import PropertiesApi from roborock.exceptions import RoborockException @@ -17,11 +19,12 @@ from .const import DOMAIN from .coordinator import ( + RoborockB01Q10UpdateCoordinator, RoborockConfigEntry, RoborockCoordinatorType, RoborockDataUpdateCoordinator, ) -from .entity import RoborockEntityV1 +from .entity import RoborockCoordinatedEntityB01Q10, RoborockEntityV1 _LOGGER = logging.getLogger(__name__) @@ -59,6 +62,37 @@ class RoborockNumberDescription(NumberEntityDescription): ] +@dataclass(frozen=True, kw_only=True) +class RoborockNumberDescriptionQ10(NumberEntityDescription): + """Class to describe a Roborock Q10 number entity.""" + + trait: Callable[[Q10PropertiesApi], SoundVolumeTrait | None] + """Function to get the trait backing the entity, if supported.""" + + get_value: Callable[[SoundVolumeTrait], float | None] + """Function to get the value from the trait.""" + + set_value: Callable[[SoundVolumeTrait, float], Coroutine[Any, Any, None]] + """Function to set the value on the trait.""" + + +Q10_NUMBER_DESCRIPTIONS: list[RoborockNumberDescriptionQ10] = [ + RoborockNumberDescriptionQ10( + key="volume", + translation_key="volume", + native_min_value=0, + native_max_value=100, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.CONFIG, + trait=lambda api: api.volume, + get_value=lambda trait: ( + float(trait.volume) if trait.volume is not None else None + ), + set_value=lambda trait, value: trait.set_volume(int(value)), + ) +] + + async def async_setup_entry( hass: HomeAssistant, config_entry: RoborockConfigEntry, @@ -72,18 +106,29 @@ def async_add_coordinator_entities( coordinator: RoborockCoordinatorType, ) -> None: """Add entities for a specific coordinator.""" - if not isinstance(coordinator, RoborockDataUpdateCoordinator): - return - entities = [ - RoborockNumberEntity( - f"{description.key}_{coordinator.duid_slug}", - coordinator=coordinator, - entity_description=description, - trait=trait, + entities: list[NumberEntity] = [] + if isinstance(coordinator, RoborockDataUpdateCoordinator): + entities.extend( + RoborockNumberEntity( + f"{description.key}_{coordinator.duid_slug}", + coordinator=coordinator, + entity_description=description, + trait=trait, + ) + for description in NUMBER_DESCRIPTIONS + if (trait := description.trait(coordinator.properties_api)) is not None + ) + elif isinstance(coordinator, RoborockB01Q10UpdateCoordinator): + entities.extend( + RoborockNumberEntityQ10( + f"{description.key}_{coordinator.duid_slug}", + coordinator=coordinator, + entity_description=description, + trait=q10_trait, + ) + for description in Q10_NUMBER_DESCRIPTIONS + if (q10_trait := description.trait(coordinator.api)) is not None ) - for description in NUMBER_DESCRIPTIONS - if (trait := description.trait(coordinator.properties_api)) is not None - ] async_add_entities(entities) for coordinator in coordinators.values(): @@ -133,3 +178,45 @@ async def async_set_native_value(self, value: float) -> None: translation_domain=DOMAIN, translation_key="update_options_failed", ) from err + + +class RoborockNumberEntityQ10(RoborockCoordinatedEntityB01Q10, NumberEntity): + """A class to set a numeric setting on a Roborock Q10 device.""" + + entity_description: RoborockNumberDescriptionQ10 + coordinator: RoborockB01Q10UpdateCoordinator + + def __init__( + self, + unique_id: str, + coordinator: RoborockB01Q10UpdateCoordinator, + entity_description: RoborockNumberDescriptionQ10, + trait: SoundVolumeTrait, + ) -> None: + """Create a number entity.""" + self.entity_description = entity_description + self._trait = trait + super().__init__(unique_id, coordinator) + + @override + async def async_added_to_hass(self) -> None: + """Register a trait listener for push-based state updates.""" + await super().async_added_to_hass() + self.async_on_remove(self._trait.add_update_listener(self.async_write_ha_state)) + + @property + @override + def native_value(self) -> float | None: + """Get native value.""" + return self.entity_description.get_value(self._trait) + + @override + async def async_set_native_value(self, value: float) -> None: + """Set number value.""" + try: + await self.entity_description.set_value(self._trait, value) + except RoborockException as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="update_options_failed", + ) from err diff --git a/tests/components/roborock/conftest.py b/tests/components/roborock/conftest.py index 3f73ade734c45..43e29665ac295 100644 --- a/tests/components/roborock/conftest.py +++ b/tests/components/roborock/conftest.py @@ -238,6 +238,15 @@ def create_b01_q10_trait() -> Mock: q10_trait.button_light.enable = AsyncMock() q10_trait.button_light.disable = AsyncMock() + q10_trait.volume = AsyncMock() + q10_trait.volume.volume = 50 + volume_notify = attach_update_listeners(q10_trait.volume) + + async def _set_volume(volume: int) -> None: + q10_trait.volume.volume = volume + volume_notify() + + q10_trait.volume.set_volume = AsyncMock(side_effect=_set_volume) q10_trait.map = Mock() q10_trait.map.image_content = b"\x89PNG-q10" q10_trait.map.rooms = [ diff --git a/tests/components/roborock/test_number.py b/tests/components/roborock/test_number.py index c5ce70d5b0712..8a269a3ef040c 100644 --- a/tests/components/roborock/test_number.py +++ b/tests/components/roborock/test_number.py @@ -50,6 +50,74 @@ async def test_update_sound_volume( assert state.state == "3.0" +async def test_q10_update_sound_volume( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + fake_q10_vacuum: FakeDevice, +) -> None: + """Test changing the volume of a Q10 device.""" + entity_id = "number.roborock_q10_s5_volume" + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "50.0" + + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 30.0}, + blocking=True, + target={"entity_id": entity_id}, + ) + + assert fake_q10_vacuum.b01_q10_properties is not None + fake_q10_vacuum.b01_q10_properties.volume.set_volume.assert_awaited_once_with(30) + + # The trait listener pushes the new value into the entity state + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "30.0" + + +async def test_q10_volume_unknown_value( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + fake_q10_vacuum: FakeDevice, +) -> None: + """Test the Q10 entity reports unknown when the trait value is None.""" + assert fake_q10_vacuum.b01_q10_properties is not None + fake_q10_vacuum.b01_q10_properties.volume.volume = None + + await async_update_entity(hass, "number.roborock_q10_s5_volume") + + state = hass.states.get("number.roborock_q10_s5_volume") + assert state is not None + assert state.state == STATE_UNKNOWN + + +async def test_q10_volume_update_failed( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + fake_q10_vacuum: FakeDevice, +) -> None: + """Test a failure while changing the volume of a Q10 device.""" + assert fake_q10_vacuum.b01_q10_properties is not None + fake_q10_vacuum.b01_q10_properties.volume.set_volume.side_effect = RoborockTimeout + + assert hass.states.get("number.roborock_q10_s5_volume") is not None + + with pytest.raises(HomeAssistantError, match="Failed to update Roborock options"): + await hass.services.async_call( + "number", + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: 30.0}, + blocking=True, + target={"entity_id": "number.roborock_q10_s5_volume"}, + ) + + fake_q10_vacuum.b01_q10_properties.volume.set_volume.assert_awaited_once_with(30) + + async def test_volume_unknown_value( hass: HomeAssistant, setup_entry: MockConfigEntry, From 306963a0ebafddd8cddd45b976220a11b56cc4e9 Mon Sep 17 00:00:00 2001 From: Sarabveer Singh <4297171+sarabveer@users.noreply.github.com> Date: Tue, 7 Jul 2026 19:01:58 -0400 Subject: [PATCH 6/6] Bump tesla-wall-connector to 1.2.0 (#175912) --- homeassistant/components/tesla_wall_connector/manifest.json | 2 +- requirements_all.txt | 2 +- script/hassfest/requirements.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla_wall_connector/manifest.json b/homeassistant/components/tesla_wall_connector/manifest.json index d008d99f1c16a..10d32279cde06 100644 --- a/homeassistant/components/tesla_wall_connector/manifest.json +++ b/homeassistant/components/tesla_wall_connector/manifest.json @@ -21,5 +21,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["tesla_wall_connector"], - "requirements": ["tesla-wall-connector==1.1.0"] + "requirements": ["tesla-wall-connector==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 109220ef27a79..3a4e584a6f6e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3159,7 +3159,7 @@ tesla-fleet-api==1.5.4 tesla-powerwall==0.5.3 # homeassistant.components.tesla_wall_connector -tesla-wall-connector==1.1.0 +tesla-wall-connector==1.2.0 # homeassistant.components.teslemetry teslemetry-stream==0.9.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 5ce6510ebd7ba..7aba0b5e27dfd 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -217,7 +217,6 @@ "surepetcare": {"surepy": {"async-timeout"}}, "tailwind": {"gotailwind": {"backoff"}}, "technove": {"python-technove": {"backoff"}}, - "tesla_wall_connector": {"tesla-wall-connector": {"backoff"}}, "tibber": {"gql": {"backoff"}}, "toon": {"toonapi": {"backoff"}}, "travisci": {