Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/media_player/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
"service": "mdi:shuffle"
},
"toggle": {
"service": "mdi:play-pause"
"service": "mdi:power"
},
"turn_off": {
"service": "mdi:power"
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/roborock/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"default": "mdi:brush"
}
},
"image": {
"map": {
"default": "mdi:floor-plan"
}
},
"number": {
"volume": {
"default": "mdi:volume-source"
Expand Down
79 changes: 65 additions & 14 deletions homeassistant/components/roborock/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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():
Expand Down Expand Up @@ -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
111 changes: 99 additions & 12 deletions homeassistant/components/roborock/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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__)

Expand Down Expand Up @@ -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,
Expand All @@ -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():
Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions homeassistant/components/roborock/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@
"name": "Start"
}
},
"image": {
"map": {
"name": "Map"
}
},
"number": {
"volume": {
"name": "Volume"
Expand Down
35 changes: 35 additions & 0 deletions homeassistant/components/stiebel_eltron/diagnostics.py
Original file line number Diff line number Diff line change
@@ -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(),
},
}
2 changes: 1 addition & 1 deletion homeassistant/components/stiebel_eltron/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ rules:

# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/zone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CONF_LONGITUDE,
CONF_NAME,
CONF_RADIUS,
DEFAULT_RADIUS,
EVENT_CORE_CONFIG_UPDATE,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions homeassistant/core_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading