This commit is contained in:
Franck Nijhof 2025-03-21 21:21:43 +01:00 committed by GitHub
commit 2f244b2b66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 3092 additions and 842 deletions

View File

@ -101,9 +101,11 @@ def hostname_from_url(url: str) -> str:
def _host_validator(config: dict[str, str]) -> dict[str, str]: def _host_validator(config: dict[str, str]) -> dict[str, str]:
"""Validate that a host is properly configured.""" """Validate that a host is properly configured."""
if config[CONF_HOST].startswith("elks://"): if config[CONF_HOST].startswith(("elks://", "elksv1_2://")):
if CONF_USERNAME not in config or CONF_PASSWORD not in config: if CONF_USERNAME not in config or CONF_PASSWORD not in config:
raise vol.Invalid("Specify username and password for elks://") raise vol.Invalid(
"Specify username and password for elks:// or elksv1_2://"
)
elif not config[CONF_HOST].startswith("elk://") and not config[ elif not config[CONF_HOST].startswith("elk://") and not config[
CONF_HOST CONF_HOST
].startswith("serial://"): ].startswith("serial://"):

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import mimetypes
from pathlib import Path from pathlib import Path
from google import genai # type: ignore[attr-defined] from google import genai # type: ignore[attr-defined]
@ -83,7 +84,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
) )
if not Path(filename).exists(): if not Path(filename).exists():
raise HomeAssistantError(f"`{filename}` does not exist") raise HomeAssistantError(f"`{filename}` does not exist")
prompt_parts.append(client.files.upload(file=filename)) mimetype = mimetypes.guess_type(filename)[0]
with open(filename, "rb") as file:
uploaded_file = client.files.upload(
file=file, config={"mime_type": mimetype}
)
prompt_parts.append(uploaded_file)
await hass.async_add_executor_job(append_files_to_prompt) await hass.async_add_executor_job(append_files_to_prompt)

View File

@ -629,14 +629,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
home_connect_client = HomeConnectClient(config_entry_auth) home_connect_client = HomeConnectClient(config_entry_auth)
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client) coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
await coordinator.async_config_entry_first_refresh() await coordinator.async_setup()
entry.runtime_data = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.runtime_data.start_event_listener() entry.runtime_data.start_event_listener()
entry.async_create_background_task(
hass,
coordinator.async_refresh(),
f"home_connect-initial-full-refresh-{entry.entry_id}",
)
return True return True

View File

@ -137,41 +137,6 @@ def setup_home_connect_entry(
defaultdict(list) defaultdict(list)
) )
entities: list[HomeConnectEntity] = []
for appliance in entry.runtime_data.data.values():
entities_to_add = get_entities_for_appliance(entry, appliance)
if get_option_entities_for_appliance:
entities_to_add.extend(get_option_entities_for_appliance(entry, appliance))
for event_key in (
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
):
changed_options_listener_remove_callback = (
entry.runtime_data.async_add_listener(
partial(
_create_option_entities,
entry,
appliance,
known_entity_unique_ids,
get_option_entities_for_appliance,
async_add_entities,
),
(appliance.info.ha_id, event_key),
)
)
entry.async_on_unload(changed_options_listener_remove_callback)
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
changed_options_listener_remove_callback
)
known_entity_unique_ids.update(
{
cast(str, entity.unique_id): appliance.info.ha_id
for entity in entities_to_add
}
)
entities.extend(entities_to_add)
async_add_entities(entities)
entry.async_on_unload( entry.async_on_unload(
entry.runtime_data.async_add_special_listener( entry.runtime_data.async_add_special_listener(
partial( partial(

View File

@ -10,6 +10,7 @@ from .utils import bsh_key_to_translation_key
DOMAIN = "home_connect" DOMAIN = "home_connect"
API_DEFAULT_RETRY_AFTER = 60
APPLIANCES_WITH_PROGRAMS = ( APPLIANCES_WITH_PROGRAMS = (
"CleaningRobot", "CleaningRobot",
@ -284,6 +285,7 @@ SPIN_SPEED_OPTIONS = {
"LaundryCare.Washer.EnumType.SpinSpeed.Off", "LaundryCare.Washer.EnumType.SpinSpeed.Off",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400", "LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600", "LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM700",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800", "LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM900", "LaundryCare.Washer.EnumType.SpinSpeed.RPM900",
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000", "LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",

View File

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio from asyncio import sleep as asyncio_sleep
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
@ -29,6 +29,7 @@ from aiohomeconnect.model.error import (
HomeConnectApiError, HomeConnectApiError,
HomeConnectError, HomeConnectError,
HomeConnectRequestError, HomeConnectRequestError,
TooManyRequestsError,
UnauthorizedError, UnauthorizedError,
) )
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
@ -36,11 +37,11 @@ from propcache.api import cached_property
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN from .const import API_DEFAULT_RETRY_AFTER, APPLIANCES_WITH_PROGRAMS, DOMAIN
from .utils import get_dict_from_home_connect_error from .utils import get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -154,7 +155,7 @@ class HomeConnectCoordinator(
f"home_connect-events_listener_task-{self.config_entry.entry_id}", f"home_connect-events_listener_task-{self.config_entry.entry_id}",
) )
async def _event_listener(self) -> None: async def _event_listener(self) -> None: # noqa: C901
"""Match event with listener for event type.""" """Match event with listener for event type."""
retry_time = 10 retry_time = 10
while True: while True:
@ -269,7 +270,7 @@ class HomeConnectCoordinator(
type(error).__name__, type(error).__name__,
retry_time, retry_time,
) )
await asyncio.sleep(retry_time) await asyncio_sleep(retry_time)
retry_time = min(retry_time * 2, 3600) retry_time = min(retry_time * 2, 3600)
except HomeConnectApiError as error: except HomeConnectApiError as error:
_LOGGER.error("Error while listening for events: %s", error) _LOGGER.error("Error while listening for events: %s", error)
@ -278,6 +279,13 @@ class HomeConnectCoordinator(
) )
break break
# Trigger to delete the possible depaired device entities
# from known_entities variable at common.py
for listener, context in self._special_listeners.values():
assert isinstance(context, tuple)
if EventKey.BSH_COMMON_APPLIANCE_DEPAIRED in context:
listener()
@callback @callback
def _call_event_listener(self, event_message: EventMessage) -> None: def _call_event_listener(self, event_message: EventMessage) -> None:
"""Call listener for event.""" """Call listener for event."""
@ -295,6 +303,42 @@ class HomeConnectCoordinator(
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]: async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
"""Fetch data from Home Connect.""" """Fetch data from Home Connect."""
await self._async_setup()
for appliance_data in self.data.values():
appliance = appliance_data.info
ha_id = appliance.ha_id
while True:
try:
self.data[ha_id] = await self._get_appliance_data(
appliance, self.data.get(ha_id)
)
except TooManyRequestsError as err:
_LOGGER.debug(
"Rate limit exceeded on initial fetch: %s",
err,
)
await asyncio_sleep(err.retry_after or API_DEFAULT_RETRY_AFTER)
else:
break
for listener, context in self._special_listeners.values():
assert isinstance(context, tuple)
if EventKey.BSH_COMMON_APPLIANCE_PAIRED in context:
listener()
return self.data
async def async_setup(self) -> None:
"""Set up the devices."""
try:
await self._async_setup()
except UpdateFailed as err:
raise ConfigEntryNotReady from err
async def _async_setup(self) -> None:
"""Set up the devices."""
old_appliances = set(self.data.keys())
try: try:
appliances = await self.client.get_home_appliances() appliances = await self.client.get_home_appliances()
except UnauthorizedError as error: except UnauthorizedError as error:
@ -312,12 +356,38 @@ class HomeConnectCoordinator(
translation_placeholders=get_dict_from_home_connect_error(error), translation_placeholders=get_dict_from_home_connect_error(error),
) from error ) from error
return { for appliance in appliances.homeappliances:
appliance.ha_id: await self._get_appliance_data( self.device_registry.async_get_or_create(
appliance, self.data.get(appliance.ha_id) config_entry_id=self.config_entry.entry_id,
identifiers={(DOMAIN, appliance.ha_id)},
manufacturer=appliance.brand,
name=appliance.name,
model=appliance.vib,
) )
for appliance in appliances.homeappliances if appliance.ha_id not in self.data:
} self.data[appliance.ha_id] = HomeConnectApplianceData(
commands=set(),
events={},
info=appliance,
options={},
programs=[],
settings={},
status={},
)
else:
self.data[appliance.ha_id].info.connected = appliance.connected
old_appliances.remove(appliance.ha_id)
for ha_id in old_appliances:
self.data.pop(ha_id, None)
device = self.device_registry.async_get_device(
identifiers={(DOMAIN, ha_id)}
)
if device:
self.device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
async def _get_appliance_data( async def _get_appliance_data(
self, self,
@ -339,6 +409,8 @@ class HomeConnectCoordinator(
await self.client.get_settings(appliance.ha_id) await self.client.get_settings(appliance.ha_id)
).settings ).settings
} }
except TooManyRequestsError:
raise
except HomeConnectError as error: except HomeConnectError as error:
_LOGGER.debug( _LOGGER.debug(
"Error fetching settings for %s: %s", "Error fetching settings for %s: %s",
@ -353,6 +425,8 @@ class HomeConnectCoordinator(
status.key: status status.key: status
for status in (await self.client.get_status(appliance.ha_id)).status for status in (await self.client.get_status(appliance.ha_id)).status
} }
except TooManyRequestsError:
raise
except HomeConnectError as error: except HomeConnectError as error:
_LOGGER.debug( _LOGGER.debug(
"Error fetching status for %s: %s", "Error fetching status for %s: %s",
@ -369,6 +443,8 @@ class HomeConnectCoordinator(
if appliance.type in APPLIANCES_WITH_PROGRAMS: if appliance.type in APPLIANCES_WITH_PROGRAMS:
try: try:
all_programs = await self.client.get_all_programs(appliance.ha_id) all_programs = await self.client.get_all_programs(appliance.ha_id)
except TooManyRequestsError:
raise
except HomeConnectError as error: except HomeConnectError as error:
_LOGGER.debug( _LOGGER.debug(
"Error fetching programs for %s: %s", "Error fetching programs for %s: %s",
@ -427,6 +503,8 @@ class HomeConnectCoordinator(
await self.client.get_available_commands(appliance.ha_id) await self.client.get_available_commands(appliance.ha_id)
).commands ).commands
} }
except TooManyRequestsError:
raise
except HomeConnectError: except HomeConnectError:
commands = set() commands = set()
@ -461,6 +539,8 @@ class HomeConnectCoordinator(
).options ).options
or [] or []
} }
except TooManyRequestsError:
raise
except HomeConnectError as error: except HomeConnectError as error:
_LOGGER.debug( _LOGGER.debug(
"Error fetching options for %s: %s", "Error fetching options for %s: %s",

View File

@ -1,21 +1,28 @@
"""Home Connect entity base class.""" """Home Connect entity base class."""
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Callable, Coroutine
import contextlib import contextlib
from datetime import datetime
import logging import logging
from typing import cast from typing import Any, Concatenate, cast
from aiohomeconnect.model import EventKey, OptionKey from aiohomeconnect.model import EventKey, OptionKey
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError from aiohomeconnect.model.error import (
ActiveProgramNotSetError,
HomeConnectError,
TooManyRequestsError,
)
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import API_DEFAULT_RETRY_AFTER, DOMAIN
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
from .utils import get_dict_from_home_connect_error from .utils import get_dict_from_home_connect_error
@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
def bsh_key(self) -> OptionKey: def bsh_key(self) -> OptionKey:
"""Return the BSH key.""" """Return the BSH key."""
return cast(OptionKey, self.entity_description.key) return cast(OptionKey, self.entity_description.key)
def constraint_fetcher[_EntityT: HomeConnectEntity, **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate the function to catch Home Connect too many requests error and retry later.
If it needs to be called later, it will call async_write_ha_state function
"""
async def handler_to_return(
self: _EntityT, *args: _P.args, **kwargs: _P.kwargs
) -> None:
async def handler(_datetime: datetime | None = None) -> None:
try:
await func(self, *args, **kwargs)
except TooManyRequestsError as err:
if (retry_after := err.retry_after) is None:
retry_after = API_DEFAULT_RETRY_AFTER
async_call_later(self.hass, retry_after, handler)
except HomeConnectError as err:
_LOGGER.error(
"Error fetching constraints for %s: %s", self.entity_id, err
)
else:
if _datetime is not None:
self.async_write_ha_state()
await handler()
return handler_to_return

View File

@ -25,7 +25,7 @@ from .const import (
UNIT_MAP, UNIT_MAP,
) )
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity, HomeConnectOptionEntity from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
from .utils import get_dict_from_home_connect_error from .utils import get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
}, },
) from err ) from err
@constraint_fetcher
async def async_fetch_constraints(self) -> None: async def async_fetch_constraints(self) -> None:
"""Fetch the max and min values and step for the number entity.""" """Fetch the max and min values and step for the number entity."""
try: setting_key = cast(SettingKey, self.bsh_key)
data = self.appliance.settings.get(setting_key)
if not data or not data.unit or not data.constraints:
data = await self.coordinator.client.get_setting( data = await self.coordinator.client.get_setting(
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key) self.appliance.info.ha_id, setting_key=setting_key
) )
except HomeConnectError as err: if data.unit:
_LOGGER.error("An error occurred: %s", err) self._attr_native_unit_of_measurement = data.unit
else:
self.set_constraints(data) self.set_constraints(data)
def set_constraints(self, setting: GetSetting) -> None: def set_constraints(self, setting: GetSetting) -> None:
"""Set constraints for the number entity.""" """Set constraints for the number entity."""
if setting.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
setting.unit, setting.unit
)
if not (constraints := setting.constraints): if not (constraints := setting.constraints):
return return
if constraints.max: if constraints.max:
@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
"""When entity is added to hass.""" """When entity is added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
data = self.appliance.settings[cast(SettingKey, self.bsh_key)] data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
self._attr_native_unit_of_measurement = data.unit
self.set_constraints(data) self.set_constraints(data)
if ( if (
not hasattr(self, "_attr_native_min_value") not hasattr(self, "_attr_native_unit_of_measurement")
or not hasattr(self, "_attr_native_min_value")
or not hasattr(self, "_attr_native_max_value") or not hasattr(self, "_attr_native_max_value")
or not hasattr(self, "_attr_native_step") or not hasattr(self, "_attr_native_step")
): ):
@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
or candidate_unit != self._attr_native_unit_of_measurement or candidate_unit != self._attr_native_unit_of_measurement
): ):
self._attr_native_unit_of_measurement = candidate_unit self._attr_native_unit_of_measurement = candidate_unit
self.__dict__.pop("unit_of_measurement", None)
option_constraints = option_definition.constraints option_constraints = option_definition.constraints
if option_constraints: if option_constraints:
if ( if (

View File

@ -1,8 +1,8 @@
"""Provides a select platform for Home Connect.""" """Provides a select platform for Home Connect."""
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass from dataclasses import dataclass
import logging
from typing import Any, cast from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient from aiohomeconnect.client import Client as HomeConnectClient
@ -47,9 +47,11 @@ from .coordinator import (
HomeConnectConfigEntry, HomeConnectConfigEntry,
HomeConnectCoordinator, HomeConnectCoordinator,
) )
from .entity import HomeConnectEntity, HomeConnectOptionEntity from .entity import HomeConnectEntity, HomeConnectOptionEntity, constraint_fetcher
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = { FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
@ -413,6 +415,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
"""Select setting class for Home Connect.""" """Select setting class for Home Connect."""
entity_description: HomeConnectSelectEntityDescription entity_description: HomeConnectSelectEntityDescription
_original_option_keys: set[str | None]
def __init__( def __init__(
self, self,
@ -421,6 +424,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription, desc: HomeConnectSelectEntityDescription,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key)
super().__init__( super().__init__(
coordinator, coordinator,
appliance, appliance,
@ -458,23 +462,29 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""When entity is added to hass.""" """When entity is added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
await self.async_fetch_options()
@constraint_fetcher
async def async_fetch_options(self) -> None:
"""Fetch options from the API."""
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key)) setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
if ( if (
not setting not setting
or not setting.constraints or not setting.constraints
or not setting.constraints.allowed_values or not setting.constraints.allowed_values
): ):
with contextlib.suppress(HomeConnectError): setting = await self.coordinator.client.get_setting(
setting = await self.coordinator.client.get_setting( self.appliance.info.ha_id,
self.appliance.info.ha_id, setting_key=cast(SettingKey, self.bsh_key),
setting_key=cast(SettingKey, self.bsh_key), )
)
if setting and setting.constraints and setting.constraints.allowed_values: if setting and setting.constraints and setting.constraints.allowed_values:
self._original_option_keys = set(setting.constraints.allowed_values)
self._attr_options = [ self._attr_options = [
self.entity_description.values_translation_key[option] self.entity_description.values_translation_key[option]
for option in setting.constraints.allowed_values for option in self._original_option_keys
if option in self.entity_description.values_translation_key if option is not None
and option in self.entity_description.values_translation_key
] ]
@ -491,7 +501,7 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription, desc: HomeConnectSelectEntityDescription,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key.keys()) self._original_option_keys = set(desc.values_translation_key)
super().__init__( super().__init__(
coordinator, coordinator,
appliance, appliance,
@ -524,5 +534,5 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
self.entity_description.values_translation_key[option] self.entity_description.values_translation_key[option]
for option in self._original_option_keys for option in self._original_option_keys
if option is not None if option is not None
and option in self.entity_description.values_translation_key
] ]
self.__dict__.pop("options", None)

View File

@ -1,12 +1,11 @@
"""Provides a sensor for Home Connect.""" """Provides a sensor for Home Connect."""
import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import logging
from typing import cast from typing import cast
from aiohomeconnect.model import EventKey, StatusKey from aiohomeconnect.model import EventKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -28,7 +27,9 @@ from .const import (
UNIT_MAP, UNIT_MAP,
) )
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity from .entity import HomeConnectEntity, constraint_fetcher
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
else: else:
await self.fetch_unit() await self.fetch_unit()
@constraint_fetcher
async def fetch_unit(self) -> None: async def fetch_unit(self) -> None:
"""Fetch the unit of measurement.""" """Fetch the unit of measurement."""
with contextlib.suppress(HomeConnectError): data = await self.coordinator.client.get_status_value(
data = await self.coordinator.client.get_status_value( self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key) )
) if data.unit:
if data.unit: self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
self._attr_native_unit_of_measurement = UNIT_MAP.get(
data.unit, data.unit
)
class HomeConnectProgramSensor(HomeConnectSensor): class HomeConnectProgramSensor(HomeConnectSensor):

View File

@ -468,11 +468,11 @@ set_program_and_options:
translation_key: venting_level translation_key: venting_level
options: options:
- cooking_hood_enum_type_stage_fan_off - cooking_hood_enum_type_stage_fan_off
- cooking_hood_enum_type_stage_fan_stage01 - cooking_hood_enum_type_stage_fan_stage_01
- cooking_hood_enum_type_stage_fan_stage02 - cooking_hood_enum_type_stage_fan_stage_02
- cooking_hood_enum_type_stage_fan_stage03 - cooking_hood_enum_type_stage_fan_stage_03
- cooking_hood_enum_type_stage_fan_stage04 - cooking_hood_enum_type_stage_fan_stage_04
- cooking_hood_enum_type_stage_fan_stage05 - cooking_hood_enum_type_stage_fan_stage_05
cooking_hood_option_intensive_level: cooking_hood_option_intensive_level:
example: cooking_hood_enum_type_intensive_stage_intensive_stage1 example: cooking_hood_enum_type_intensive_stage_intensive_stage1
required: false required: false
@ -528,7 +528,7 @@ set_program_and_options:
collapsed: true collapsed: true
fields: fields:
laundry_care_washer_option_temperature: laundry_care_washer_option_temperature:
example: laundry_care_washer_enum_type_temperature_g_c40 example: laundry_care_washer_enum_type_temperature_g_c_40
required: false required: false
selector: selector:
select: select:
@ -536,14 +536,14 @@ set_program_and_options:
translation_key: washer_temperature translation_key: washer_temperature
options: options:
- laundry_care_washer_enum_type_temperature_cold - laundry_care_washer_enum_type_temperature_cold
- laundry_care_washer_enum_type_temperature_g_c20 - laundry_care_washer_enum_type_temperature_g_c_20
- laundry_care_washer_enum_type_temperature_g_c30 - laundry_care_washer_enum_type_temperature_g_c_30
- laundry_care_washer_enum_type_temperature_g_c40 - laundry_care_washer_enum_type_temperature_g_c_40
- laundry_care_washer_enum_type_temperature_g_c50 - laundry_care_washer_enum_type_temperature_g_c_50
- laundry_care_washer_enum_type_temperature_g_c60 - laundry_care_washer_enum_type_temperature_g_c_60
- laundry_care_washer_enum_type_temperature_g_c70 - laundry_care_washer_enum_type_temperature_g_c_70
- laundry_care_washer_enum_type_temperature_g_c80 - laundry_care_washer_enum_type_temperature_g_c_80
- laundry_care_washer_enum_type_temperature_g_c90 - laundry_care_washer_enum_type_temperature_g_c_90
- laundry_care_washer_enum_type_temperature_ul_cold - laundry_care_washer_enum_type_temperature_ul_cold
- laundry_care_washer_enum_type_temperature_ul_warm - laundry_care_washer_enum_type_temperature_ul_warm
- laundry_care_washer_enum_type_temperature_ul_hot - laundry_care_washer_enum_type_temperature_ul_hot
@ -557,13 +557,15 @@ set_program_and_options:
translation_key: spin_speed translation_key: spin_speed
options: options:
- laundry_care_washer_enum_type_spin_speed_off - laundry_care_washer_enum_type_spin_speed_off
- laundry_care_washer_enum_type_spin_speed_r_p_m400 - laundry_care_washer_enum_type_spin_speed_r_p_m_400
- laundry_care_washer_enum_type_spin_speed_r_p_m600 - laundry_care_washer_enum_type_spin_speed_r_p_m_600
- laundry_care_washer_enum_type_spin_speed_r_p_m800 - laundry_care_washer_enum_type_spin_speed_r_p_m_700
- laundry_care_washer_enum_type_spin_speed_r_p_m1000 - laundry_care_washer_enum_type_spin_speed_r_p_m_800
- laundry_care_washer_enum_type_spin_speed_r_p_m1200 - laundry_care_washer_enum_type_spin_speed_r_p_m_900
- laundry_care_washer_enum_type_spin_speed_r_p_m1400 - laundry_care_washer_enum_type_spin_speed_r_p_m_1000
- laundry_care_washer_enum_type_spin_speed_r_p_m1600 - laundry_care_washer_enum_type_spin_speed_r_p_m_1200
- laundry_care_washer_enum_type_spin_speed_r_p_m_1400
- laundry_care_washer_enum_type_spin_speed_r_p_m_1600
- laundry_care_washer_enum_type_spin_speed_ul_off - laundry_care_washer_enum_type_spin_speed_ul_off
- laundry_care_washer_enum_type_spin_speed_ul_low - laundry_care_washer_enum_type_spin_speed_ul_low
- laundry_care_washer_enum_type_spin_speed_ul_medium - laundry_care_washer_enum_type_spin_speed_ul_medium

View File

@ -417,11 +417,11 @@
"venting_level": { "venting_level": {
"options": { "options": {
"cooking_hood_enum_type_stage_fan_off": "Fan off", "cooking_hood_enum_type_stage_fan_off": "Fan off",
"cooking_hood_enum_type_stage_fan_stage01": "Fan stage 1", "cooking_hood_enum_type_stage_fan_stage_01": "Fan stage 1",
"cooking_hood_enum_type_stage_fan_stage02": "Fan stage 2", "cooking_hood_enum_type_stage_fan_stage_02": "Fan stage 2",
"cooking_hood_enum_type_stage_fan_stage03": "Fan stage 3", "cooking_hood_enum_type_stage_fan_stage_03": "Fan stage 3",
"cooking_hood_enum_type_stage_fan_stage04": "Fan stage 4", "cooking_hood_enum_type_stage_fan_stage_04": "Fan stage 4",
"cooking_hood_enum_type_stage_fan_stage05": "Fan stage 5" "cooking_hood_enum_type_stage_fan_stage_05": "Fan stage 5"
} }
}, },
"intensive_level": { "intensive_level": {
@ -441,14 +441,14 @@
"washer_temperature": { "washer_temperature": {
"options": { "options": {
"laundry_care_washer_enum_type_temperature_cold": "Cold", "laundry_care_washer_enum_type_temperature_cold": "Cold",
"laundry_care_washer_enum_type_temperature_g_c20": "20ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_20": "20ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c30": "30ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_30": "30ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c40": "40ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_40": "40ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c50": "50ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_50": "50ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c60": "60ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_60": "60ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c70": "70ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_70": "70ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c80": "80ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_80": "80ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c90": "90ºC clothes", "laundry_care_washer_enum_type_temperature_g_c_90": "90ºC clothes",
"laundry_care_washer_enum_type_temperature_ul_cold": "Cold", "laundry_care_washer_enum_type_temperature_ul_cold": "Cold",
"laundry_care_washer_enum_type_temperature_ul_warm": "Warm", "laundry_care_washer_enum_type_temperature_ul_warm": "Warm",
"laundry_care_washer_enum_type_temperature_ul_hot": "Hot", "laundry_care_washer_enum_type_temperature_ul_hot": "Hot",
@ -458,14 +458,15 @@
"spin_speed": { "spin_speed": {
"options": { "options": {
"laundry_care_washer_enum_type_spin_speed_off": "Off", "laundry_care_washer_enum_type_spin_speed_off": "Off",
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "400 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "600 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "900 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "1600 rpm", "laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
"laundry_care_washer_enum_type_spin_speed_ul_off": "Off", "laundry_care_washer_enum_type_spin_speed_ul_off": "Off",
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low", "laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium", "laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
@ -1383,11 +1384,11 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]", "name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
"state": { "state": {
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]", "cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
"cooking_hood_enum_type_stage_fan_stage01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage01%]", "cooking_hood_enum_type_stage_fan_stage_01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_01%]",
"cooking_hood_enum_type_stage_fan_stage02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage02%]", "cooking_hood_enum_type_stage_fan_stage_02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_02%]",
"cooking_hood_enum_type_stage_fan_stage03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage03%]", "cooking_hood_enum_type_stage_fan_stage_03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_03%]",
"cooking_hood_enum_type_stage_fan_stage04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage04%]", "cooking_hood_enum_type_stage_fan_stage_04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_04%]",
"cooking_hood_enum_type_stage_fan_stage05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage05%]" "cooking_hood_enum_type_stage_fan_stage_05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_05%]"
} }
}, },
"intensive_level": { "intensive_level": {
@ -1410,14 +1411,14 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]", "name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]",
"state": { "state": {
"laundry_care_washer_enum_type_temperature_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_cold%]", "laundry_care_washer_enum_type_temperature_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_cold%]",
"laundry_care_washer_enum_type_temperature_g_c20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c20%]", "laundry_care_washer_enum_type_temperature_g_c_20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_20%]",
"laundry_care_washer_enum_type_temperature_g_c30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c30%]", "laundry_care_washer_enum_type_temperature_g_c_30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_30%]",
"laundry_care_washer_enum_type_temperature_g_c40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c40%]", "laundry_care_washer_enum_type_temperature_g_c_40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_40%]",
"laundry_care_washer_enum_type_temperature_g_c50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c50%]", "laundry_care_washer_enum_type_temperature_g_c_50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_50%]",
"laundry_care_washer_enum_type_temperature_g_c60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c60%]", "laundry_care_washer_enum_type_temperature_g_c_60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_60%]",
"laundry_care_washer_enum_type_temperature_g_c70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c70%]", "laundry_care_washer_enum_type_temperature_g_c_70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_70%]",
"laundry_care_washer_enum_type_temperature_g_c80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c80%]", "laundry_care_washer_enum_type_temperature_g_c_80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_80%]",
"laundry_care_washer_enum_type_temperature_g_c90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c90%]", "laundry_care_washer_enum_type_temperature_g_c_90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c_90%]",
"laundry_care_washer_enum_type_temperature_ul_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_cold%]", "laundry_care_washer_enum_type_temperature_ul_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_cold%]",
"laundry_care_washer_enum_type_temperature_ul_warm": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_warm%]", "laundry_care_washer_enum_type_temperature_ul_warm": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_warm%]",
"laundry_care_washer_enum_type_temperature_ul_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_hot%]", "laundry_care_washer_enum_type_temperature_ul_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_hot%]",
@ -1428,14 +1429,15 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]", "name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
"state": { "state": {
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]", "laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_600%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_700%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m900%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_800%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_900": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_900%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1000%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1200%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1600%]", "laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]", "laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]", "laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]", "laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",

View File

@ -61,6 +61,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
client=client, client=client,
) )
# initialize the firmware update coordinator early to check the firmware version
firmware_device = LaMarzoccoMachine(
model=entry.data[CONF_MODEL],
serial_number=entry.unique_id,
name=entry.data[CONF_NAME],
cloud_client=cloud_client,
)
firmware_coordinator = LaMarzoccoFirmwareUpdateCoordinator(
hass, entry, firmware_device
)
await firmware_coordinator.async_config_entry_first_refresh()
gateway_version = version.parse(
firmware_device.firmware[FirmwareType.GATEWAY].current_version
)
if gateway_version >= version.parse("v5.0.9"):
# remove host from config entry, it is not supported anymore
data = {k: v for k, v in entry.data.items() if k != CONF_HOST}
hass.config_entries.async_update_entry(
entry,
data=data,
)
elif gateway_version < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
DOMAIN,
"unsupported_gateway_firmware",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_gateway_firmware",
translation_placeholders={"gateway_version": str(gateway_version)},
)
# initialize local API # initialize local API
local_client: LaMarzoccoLocalClient | None = None local_client: LaMarzoccoLocalClient | None = None
if (host := entry.data.get(CONF_HOST)) is not None: if (host := entry.data.get(CONF_HOST)) is not None:
@ -117,30 +153,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
coordinators = LaMarzoccoRuntimeData( coordinators = LaMarzoccoRuntimeData(
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client), LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
LaMarzoccoFirmwareUpdateCoordinator(hass, entry, device), firmware_coordinator,
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device), LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
) )
# API does not like concurrent requests, so no asyncio.gather here # API does not like concurrent requests, so no asyncio.gather here
await coordinators.config_coordinator.async_config_entry_first_refresh() await coordinators.config_coordinator.async_config_entry_first_refresh()
await coordinators.firmware_coordinator.async_config_entry_first_refresh()
await coordinators.statistics_coordinator.async_config_entry_first_refresh() await coordinators.statistics_coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinators entry.runtime_data = coordinators
gateway_version = device.firmware[FirmwareType.GATEWAY].current_version
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
DOMAIN,
"unsupported_gateway_firmware",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="unsupported_gateway_firmware",
translation_placeholders={"gateway_version": gateway_version},
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener( async def update_listener(

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pylamarzocco"], "loggers": ["pylamarzocco"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": ["pylamarzocco==1.4.7"] "requirements": ["pylamarzocco==1.4.9"]
} }

View File

@ -144,9 +144,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_prebrew_time( set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
prebrew_off_time=value, key=key prebrew_off_time=value, key=key
), ),
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, native_value_fn=lambda config, key: config.prebrew_configuration[key][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0 available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREBREW, and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP, != MachineModel.GS3_MP,
), ),
@ -162,9 +165,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_prebrew_time( set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
prebrew_on_time=value, key=key prebrew_on_time=value, key=key
), ),
native_value_fn=lambda config, key: config.prebrew_configuration[key].off_time, native_value_fn=lambda config, key: config.prebrew_configuration[key][
0
].off_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0 available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREBREW, and device.config.prebrew_mode
in (PrebrewMode.PREBREW, PrebrewMode.PREBREW_ENABLED),
supported_fn=lambda coordinator: coordinator.device.model supported_fn=lambda coordinator: coordinator.device.model
!= MachineModel.GS3_MP, != MachineModel.GS3_MP,
), ),
@ -180,8 +186,8 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_preinfusion_time( set_value_fn=lambda machine, value, key: machine.set_preinfusion_time(
preinfusion_time=value, key=key preinfusion_time=value, key=key
), ),
native_value_fn=lambda config, key: config.prebrew_configuration[ native_value_fn=lambda config, key: config.prebrew_configuration[key][
key 1
].preinfusion_time, ].preinfusion_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0 available_fn=lambda device: len(device.config.prebrew_configuration) > 0
and device.config.prebrew_mode == PrebrewMode.PREINFUSION, and device.config.prebrew_mode == PrebrewMode.PREINFUSION,

View File

@ -38,6 +38,7 @@ STEAM_LEVEL_LM_TO_HA = {value: key for key, value in STEAM_LEVEL_HA_TO_LM.items(
PREBREW_MODE_HA_TO_LM = { PREBREW_MODE_HA_TO_LM = {
"disabled": PrebrewMode.DISABLED, "disabled": PrebrewMode.DISABLED,
"prebrew": PrebrewMode.PREBREW, "prebrew": PrebrewMode.PREBREW,
"prebrew_enabled": PrebrewMode.PREBREW_ENABLED,
"preinfusion": PrebrewMode.PREINFUSION, "preinfusion": PrebrewMode.PREINFUSION,
} }

View File

@ -148,6 +148,7 @@
"state": { "state": {
"disabled": "Disabled", "disabled": "Disabled",
"prebrew": "Prebrew", "prebrew": "Prebrew",
"prebrew_enabled": "Prebrew",
"preinfusion": "Preinfusion" "preinfusion": "Preinfusion"
} }
}, },

View File

@ -97,11 +97,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: OneDriveConfigEntry) -> None:
await hass.config_entries.async_reload(entry.entry_id)
entry.async_on_unload(entry.add_update_listener(update_listener))
def async_notify_backup_listeners() -> None: def async_notify_backup_listeners() -> None:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []): for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
listener() listener()

View File

@ -83,7 +83,16 @@ class PlaybackProxyView(HomeAssistantView):
_LOGGER.warning("Reolink playback proxy error: %s", str(err)) _LOGGER.warning("Reolink playback proxy error: %s", str(err))
return web.Response(body=str(err), status=HTTPStatus.BAD_REQUEST) return web.Response(body=str(err), status=HTTPStatus.BAD_REQUEST)
headers = dict(request.headers)
headers.pop("Host", None)
headers.pop("Referer", None)
if _LOGGER.isEnabledFor(logging.DEBUG): if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"Requested Playback Proxy Method %s, Headers: %s",
request.method,
headers,
)
_LOGGER.debug( _LOGGER.debug(
"Opening VOD stream from %s: %s", "Opening VOD stream from %s: %s",
host.api.camera_name(ch), host.api.camera_name(ch),
@ -93,6 +102,7 @@ class PlaybackProxyView(HomeAssistantView):
try: try:
reolink_response = await self.session.get( reolink_response = await self.session.get(
reolink_url, reolink_url,
headers=headers,
timeout=ClientTimeout( timeout=ClientTimeout(
connect=15, sock_connect=15, sock_read=5, total=None connect=15, sock_connect=15, sock_read=5, total=None
), ),
@ -118,18 +128,25 @@ class PlaybackProxyView(HomeAssistantView):
]: ]:
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}" err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
_LOGGER.error(err_str) _LOGGER.error(err_str)
if reolink_response.content_type == "text/html":
text = await reolink_response.text()
_LOGGER.debug(text)
return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST) return web.Response(body=err_str, status=HTTPStatus.BAD_REQUEST)
response = web.StreamResponse( response_headers = dict(reolink_response.headers)
status=200, _LOGGER.debug(
reason="OK", "Response Playback Proxy Status %s:%s, Headers: %s",
headers={ reolink_response.status,
"Content-Type": "video/mp4", reolink_response.reason,
}, response_headers,
) )
response_headers["Content-Type"] = "video/mp4"
if reolink_response.content_length is not None: response = web.StreamResponse(
response.content_length = reolink_response.content_length status=reolink_response.status,
reason=reolink_response.reason,
headers=response_headers,
)
await response.prepare(request) await response.prepare(request)
@ -141,7 +158,8 @@ class PlaybackProxyView(HomeAssistantView):
"Timeout while reading Reolink playback from %s, writing EOF", "Timeout while reading Reolink playback from %s, writing EOF",
host.api.nvr_name, host.api.nvr_name,
) )
finally:
reolink_response.release()
reolink_response.release()
await response.write_eof() await response.write_eof()
return response return response

View File

@ -3,7 +3,7 @@
import logging import logging
import ssl import ssl
from smart_meter_texas import Account, Client, ClientSSLContext from smart_meter_texas import Account, Client
from smart_meter_texas.exceptions import ( from smart_meter_texas.exceptions import (
SmartMeterTexasAPIError, SmartMeterTexasAPIError,
SmartMeterTexasAuthError, SmartMeterTexasAuthError,
@ -16,6 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import ( from .const import (
DATA_COORDINATOR, DATA_COORDINATOR,
@ -38,8 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
account = Account(username, password) account = Account(username, password)
client_ssl_context = ClientSSLContext() ssl_context = get_default_context()
ssl_context = await client_ssl_context.get_ssl_context()
smart_meter_texas_data = SmartMeterTexasData(hass, entry, account, ssl_context) smart_meter_texas_data = SmartMeterTexasData(hass, entry, account, ssl_context)
try: try:

View File

@ -4,7 +4,7 @@ import logging
from typing import Any from typing import Any
from aiohttp import ClientError from aiohttp import ClientError
from smart_meter_texas import Account, Client, ClientSSLContext from smart_meter_texas import Account, Client
from smart_meter_texas.exceptions import ( from smart_meter_texas.exceptions import (
SmartMeterTexasAPIError, SmartMeterTexasAPIError,
SmartMeterTexasAuthError, SmartMeterTexasAuthError,
@ -16,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.util.ssl import get_default_context
from .const import DOMAIN from .const import DOMAIN
@ -31,8 +32,7 @@ async def validate_input(hass: HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
client_ssl_context = ClientSSLContext() ssl_context = get_default_context()
ssl_context = await client_ssl_context.get_ssl_context()
client_session = aiohttp_client.async_get_clientsession(hass) client_session = aiohttp_client.async_get_clientsession(hass)
account = Account(data["username"], data["password"]) account = Account(data["username"], data["password"])
client = Client(client_session, account, ssl_context) client = Client(client_session, account, ssl_context)

View File

@ -139,7 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID], entry.data[CONF_TOKEN][CONF_INSTALLED_APP_ID],
) )
except SmartThingsSinkError as err: except SmartThingsSinkError as err:
_LOGGER.debug("Couldn't create a new subscription: %s", err) _LOGGER.exception("Couldn't create a new subscription")
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
subscription_id = subscription.subscription_id subscription_id = subscription.subscription_id
_handle_new_subscription_identifier(subscription_id) _handle_new_subscription_identifier(subscription_id)

View File

@ -565,12 +565,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
def _determine_hvac_modes(self) -> list[HVACMode]: def _determine_hvac_modes(self) -> list[HVACMode]:
"""Determine the supported HVAC modes.""" """Determine the supported HVAC modes."""
modes = [HVACMode.OFF] modes = [HVACMode.OFF]
modes.extend( if (
state ac_modes := self.get_attribute_value(
for mode in self.get_attribute_value(
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
) )
if (state := AC_MODE_TO_STATE.get(mode)) is not None ) is not None:
if state not in modes modes.extend(
) state
for mode in ac_modes
if (state := AC_MODE_TO_STATE.get(mode)) is not None
if state not in modes
)
return modes return modes

View File

@ -23,7 +23,7 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
client = entry.runtime_data.client client = entry.runtime_data.client
return await client.get_raw_devices() return {"devices": await client.get_raw_devices()}
async def async_get_device_diagnostics( async def async_get_device_diagnostics(

View File

@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings", "documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"requirements": ["pysmartthings==2.7.2"] "requirements": ["pysmartthings==2.7.4"]
} }

View File

@ -132,6 +132,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
capability_ignore_list: list[set[Capability]] | None = None capability_ignore_list: list[set[Capability]] | None = None
options_attribute: Attribute | None = None options_attribute: Attribute | None = None
exists_fn: Callable[[Status], bool] | None = None exists_fn: Callable[[Status], bool] | None = None
use_temperature_unit: bool = False
CAPABILITY_TO_SENSORS: dict[ CAPABILITY_TO_SENSORS: dict[
@ -573,8 +574,9 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.OVEN_SETPOINT, key=Attribute.OVEN_SETPOINT,
translation_key="oven_setpoint", translation_key="oven_setpoint",
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, use_temperature_unit=True,
value_fn=lambda value: value if value != 0 else None, # Set the value to None if it is 0 F (-17 C)
value_fn=lambda value: None if value in {0, -17} else value,
) )
] ]
}, },
@ -1018,7 +1020,10 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
attribute: Attribute, attribute: Attribute,
) -> None: ) -> None:
"""Init the class.""" """Init the class."""
super().__init__(client, device, {capability}) capabilities_to_subscribe = {capability}
if entity_description.use_temperature_unit:
capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT)
super().__init__(client, device, capabilities_to_subscribe)
self._attr_unique_id = f"{device.device.device_id}{entity_description.unique_id_separator}{entity_description.key}" self._attr_unique_id = f"{device.device.device_id}{entity_description.unique_id_separator}{entity_description.key}"
self._attribute = attribute self._attribute = attribute
self.capability = capability self.capability = capability
@ -1033,7 +1038,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
unit = self._internal_state[self.capability][self._attribute].unit if self.entity_description.use_temperature_unit:
unit = self._internal_state[Capability.TEMPERATURE_MEASUREMENT][
Attribute.TEMPERATURE
].unit
else:
unit = self._internal_state[self.capability][self._attribute].unit
return ( return (
UNITS.get(unit, unit) UNITS.get(unit, unit)
if unit if unit

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["snoo"], "loggers": ["snoo"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["python-snoo==0.6.1"] "requirements": ["python-snoo==0.6.4"]
} }

View File

@ -105,7 +105,7 @@ class SonosFavorites(SonosHouseholdCoordinator):
@soco_error() @soco_error()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool: def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update cache of known favorites and return if cache has changed.""" """Update cache of known favorites and return if cache has changed."""
new_favorites = soco.music_library.get_sonos_favorites() new_favorites = soco.music_library.get_sonos_favorites(full_album_art_uri=True)
# Polled update_id values do not match event_id values # Polled update_id values do not match event_id values
# Each speaker can return a different polled update_id # Each speaker can return a different polled update_id

View File

@ -165,6 +165,8 @@ async def async_browse_media(
favorites_folder_payload, favorites_folder_payload,
speaker.favorites, speaker.favorites,
media_content_id, media_content_id,
media,
get_browse_image_url,
) )
payload = { payload = {
@ -443,7 +445,10 @@ def favorites_payload(favorites: SonosFavorites) -> BrowseMedia:
def favorites_folder_payload( def favorites_folder_payload(
favorites: SonosFavorites, media_content_id: str favorites: SonosFavorites,
media_content_id: str,
media: SonosMedia,
get_browse_image_url: GetBrowseImageUrlType,
) -> BrowseMedia: ) -> BrowseMedia:
"""Create response payload to describe all items of a type of favorite. """Create response payload to describe all items of a type of favorite.
@ -463,7 +468,14 @@ def favorites_folder_payload(
media_content_type="favorite_item_id", media_content_type="favorite_item_id",
can_play=True, can_play=True,
can_expand=False, can_expand=False,
thumbnail=getattr(favorite, "album_art_uri", None), thumbnail=get_thumbnail_url_full(
media=media,
is_internal=True,
media_content_type="favorite_item_id",
media_content_id=favorite.item_id,
get_browse_image_url=get_browse_image_url,
item=favorite,
),
) )
) )

View File

@ -39,5 +39,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbot", "documentation": "https://www.home-assistant.io/integrations/switchbot",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["switchbot"], "loggers": ["switchbot"],
"requirements": ["PySwitchbot==0.56.1"] "requirements": ["PySwitchbot==0.57.1"]
} }

View File

@ -63,7 +63,7 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
self._device = "tls://" self._device = "tls://"
else: else:
self._device = "" self._device = ""
if user_input[CONF_PASSWORD] != "": if CONF_PASSWORD in user_input and user_input[CONF_PASSWORD] != "":
self._device += f"{user_input[CONF_PASSWORD]}@" self._device += f"{user_input[CONF_PASSWORD]}@"
self._device += f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" self._device += f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
self._async_abort_entries_match({CONF_PORT: self._device}) self._async_abort_entries_match({CONF_PORT: self._device})

View File

@ -71,9 +71,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = {
CHARGER_MAX_ICP_CURRENT_KEY: WallboxNumberEntityDescription( CHARGER_MAX_ICP_CURRENT_KEY: WallboxNumberEntityDescription(
key=CHARGER_MAX_ICP_CURRENT_KEY, key=CHARGER_MAX_ICP_CURRENT_KEY,
translation_key="maximum_icp_current", translation_key="maximum_icp_current",
max_value_fn=lambda coordinator: cast( max_value_fn=lambda _: 255,
float, coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]
),
min_value_fn=lambda _: 6, min_value_fn=lambda _: 6,
set_value_fn=lambda coordinator: coordinator.async_set_icp_current, set_value_fn=lambda coordinator: coordinator.async_set_icp_current,
native_step=1, native_step=1,

View File

@ -21,7 +21,7 @@
"zha", "zha",
"universal_silabs_flasher" "universal_silabs_flasher"
], ],
"requirements": ["zha==0.0.52"], "requirements": ["zha==0.0.53"],
"usb": [ "usb": [
{ {
"vid": "10C4", "vid": "10C4",

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "3" PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.3.3" version = "2025.3.4"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"

10
requirements_all.txt generated
View File

@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.56.1 PySwitchbot==0.57.1
# homeassistant.components.switchmate # homeassistant.components.switchmate
PySwitchmate==0.5.1 PySwitchmate==0.5.1
@ -2077,7 +2077,7 @@ pykwb==0.0.8
pylacrosse==0.4 pylacrosse==0.4
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
pylamarzocco==1.4.7 pylamarzocco==1.4.9
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==5.1.0 pylast==5.1.0
@ -2310,7 +2310,7 @@ pysma==0.7.5
pysmappee==0.2.29 pysmappee==0.2.29
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==2.7.2 pysmartthings==2.7.4
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2467,7 +2467,7 @@ python-roborock==2.12.2
python-smarttub==0.0.39 python-smarttub==0.0.39
# homeassistant.components.snoo # homeassistant.components.snoo
python-snoo==0.6.1 python-snoo==0.6.4
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.16.2 python-songpal==0.16.2
@ -3149,7 +3149,7 @@ zeroconf==0.145.1
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.52 zha==0.0.53
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong-hong-hvac==1.0.13 zhong-hong-hvac==1.0.13

View File

@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.56.1 PySwitchbot==0.57.1
# homeassistant.components.syncthru # homeassistant.components.syncthru
PySyncThru==0.8.0 PySyncThru==0.8.0
@ -1691,7 +1691,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.2 pykulersky==0.5.2
# homeassistant.components.lamarzocco # homeassistant.components.lamarzocco
pylamarzocco==1.4.7 pylamarzocco==1.4.9
# homeassistant.components.lastfm # homeassistant.components.lastfm
pylast==5.1.0 pylast==5.1.0
@ -1882,7 +1882,7 @@ pysma==0.7.5
pysmappee==0.2.29 pysmappee==0.2.29
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==2.7.2 pysmartthings==2.7.4
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2000,7 +2000,7 @@ python-roborock==2.12.2
python-smarttub==0.0.39 python-smarttub==0.0.39
# homeassistant.components.snoo # homeassistant.components.snoo
python-snoo==0.6.1 python-snoo==0.6.4
# homeassistant.components.songpal # homeassistant.components.songpal
python-songpal==0.16.2 python-songpal==0.16.2
@ -2538,7 +2538,7 @@ zeroconf==0.145.1
zeversolar==0.3.2 zeversolar==0.3.2
# homeassistant.components.zha # homeassistant.components.zha
zha==0.0.52 zha==0.0.53
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.60.1 zwave-js-server-python==0.60.1

View File

@ -1,6 +1,6 @@
"""Tests for the Google Generative AI Conversation integration.""" """Tests for the Google Generative AI Conversation integration."""
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, mock_open, patch
import pytest import pytest
from requests.exceptions import Timeout from requests.exceptions import Timeout
@ -71,6 +71,8 @@ async def test_generate_content_service_with_image(
), ),
patch("pathlib.Path.exists", return_value=True), patch("pathlib.Path.exists", return_value=True),
patch.object(hass.config, "is_allowed_path", return_value=True), patch.object(hass.config, "is_allowed_path", return_value=True),
patch("builtins.open", mock_open(read_data="this is an image")),
patch("mimetypes.guess_type", return_value=["image/jpeg"]),
): ):
response = await hass.services.async_call( response = await hass.services.async_call(
"google_generative_ai_conversation", "google_generative_ai_conversation",

View File

@ -2,13 +2,10 @@
from typing import Any from typing import Any
from aiohomeconnect.model import ArrayOfHomeAppliances, ArrayOfStatus from aiohomeconnect.model import ArrayOfStatus
from tests.common import load_json_object_fixture from tests.common import load_json_object_fixture
MOCK_APPLIANCES = ArrayOfHomeAppliances.from_dict(
load_json_object_fixture("home_connect/appliances.json")["data"] # type: ignore[arg-type]
)
MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture("home_connect/programs.json") MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture("home_connect/programs.json")
MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json") MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
MOCK_STATUS = ArrayOfStatus.from_dict( MOCK_STATUS = ArrayOfStatus.from_dict(

View File

@ -11,6 +11,7 @@ from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import ( from aiohomeconnect.model import (
ArrayOfCommands, ArrayOfCommands,
ArrayOfEvents, ArrayOfEvents,
ArrayOfHomeAppliances,
ArrayOfOptions, ArrayOfOptions,
ArrayOfPrograms, ArrayOfPrograms,
ArrayOfSettings, ArrayOfSettings,
@ -39,15 +40,9 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import ( from . import MOCK_AVAILABLE_COMMANDS, MOCK_PROGRAMS, MOCK_SETTINGS, MOCK_STATUS
MOCK_APPLIANCES,
MOCK_AVAILABLE_COMMANDS,
MOCK_PROGRAMS,
MOCK_SETTINGS,
MOCK_STATUS,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, load_fixture
CLIENT_ID = "1234" CLIENT_ID = "1234"
CLIENT_SECRET = "5678" CLIENT_SECRET = "5678"
@ -148,14 +143,6 @@ async def mock_integration_setup(
return run return run
def _get_specific_appliance_side_effect(ha_id: str) -> HomeAppliance:
"""Get specific appliance side effect."""
for appliance in copy.deepcopy(MOCK_APPLIANCES).homeappliances:
if appliance.ha_id == ha_id:
return appliance
raise HomeConnectApiError("error.key", "error description")
def _get_set_program_side_effect( def _get_set_program_side_effect(
event_queue: asyncio.Queue[list[EventMessage]], event_key: EventKey event_queue: asyncio.Queue[list[EventMessage]], event_key: EventKey
): ):
@ -271,68 +258,12 @@ def _get_set_program_options_side_effect(
return set_program_options_side_effect return set_program_options_side_effect
async def _get_all_programs_side_effect(ha_id: str) -> ArrayOfPrograms:
"""Get all programs."""
appliance_type = next(
appliance
for appliance in MOCK_APPLIANCES.homeappliances
if appliance.ha_id == ha_id
).type
if appliance_type not in MOCK_PROGRAMS:
raise HomeConnectApiError("error.key", "error description")
return ArrayOfPrograms(
[
EnumerateProgram.from_dict(program)
for program in MOCK_PROGRAMS[appliance_type]["data"]["programs"]
],
Program.from_dict(MOCK_PROGRAMS[appliance_type]["data"]["programs"][0]),
Program.from_dict(MOCK_PROGRAMS[appliance_type]["data"]["programs"][0]),
)
async def _get_settings_side_effect(ha_id: str) -> ArrayOfSettings:
"""Get settings."""
return ArrayOfSettings.from_dict(
MOCK_SETTINGS.get(
next(
appliance
for appliance in MOCK_APPLIANCES.homeappliances
if appliance.ha_id == ha_id
).type,
{},
).get("data", {"settings": []})
)
async def _get_setting_side_effect(ha_id: str, setting_key: SettingKey):
"""Get setting."""
for appliance in MOCK_APPLIANCES.homeappliances:
if appliance.ha_id == ha_id:
settings = MOCK_SETTINGS.get(
next(
appliance
for appliance in MOCK_APPLIANCES.homeappliances
if appliance.ha_id == ha_id
).type,
{},
).get("data", {"settings": []})
for setting_dict in cast(list[dict], settings["settings"]):
if setting_dict["key"] == setting_key:
return GetSetting.from_dict(setting_dict)
raise HomeConnectApiError("error.key", "error description")
async def _get_available_commands_side_effect(ha_id: str) -> ArrayOfCommands:
"""Get available commands."""
for appliance in MOCK_APPLIANCES.homeappliances:
if appliance.ha_id == ha_id and appliance.type in MOCK_AVAILABLE_COMMANDS:
return ArrayOfCommands.from_dict(MOCK_AVAILABLE_COMMANDS[appliance.type])
raise HomeConnectApiError("error.key", "error description")
@pytest.fixture(name="client") @pytest.fixture(name="client")
def mock_client(request: pytest.FixtureRequest) -> MagicMock: def mock_client(
appliances: list[HomeAppliance],
appliance: HomeAppliance | None,
request: pytest.FixtureRequest,
) -> MagicMock:
"""Fixture to mock Client from HomeConnect.""" """Fixture to mock Client from HomeConnect."""
mock = MagicMock( mock = MagicMock(
@ -369,17 +300,78 @@ def mock_client(request: pytest.FixtureRequest) -> MagicMock:
] ]
) )
appliances = [appliance] if appliance else appliances
async def stream_all_events() -> AsyncGenerator[EventMessage]: async def stream_all_events() -> AsyncGenerator[EventMessage]:
"""Mock stream_all_events.""" """Mock stream_all_events."""
while True: while True:
for event in await event_queue.get(): for event in await event_queue.get():
yield event yield event
mock.get_home_appliances = AsyncMock(return_value=copy.deepcopy(MOCK_APPLIANCES)) mock.get_home_appliances = AsyncMock(return_value=ArrayOfHomeAppliances(appliances))
def _get_specific_appliance_side_effect(ha_id: str) -> HomeAppliance:
"""Get specific appliance side effect."""
for appliance_ in appliances:
if appliance_.ha_id == ha_id:
return appliance_
raise HomeConnectApiError("error.key", "error description")
mock.get_specific_appliance = AsyncMock( mock.get_specific_appliance = AsyncMock(
side_effect=_get_specific_appliance_side_effect side_effect=_get_specific_appliance_side_effect
) )
mock.stream_all_events = stream_all_events mock.stream_all_events = stream_all_events
async def _get_all_programs_side_effect(ha_id: str) -> ArrayOfPrograms:
"""Get all programs."""
appliance_type = next(
appliance for appliance in appliances if appliance.ha_id == ha_id
).type
if appliance_type not in MOCK_PROGRAMS:
raise HomeConnectApiError("error.key", "error description")
return ArrayOfPrograms(
[
EnumerateProgram.from_dict(program)
for program in MOCK_PROGRAMS[appliance_type]["data"]["programs"]
],
Program.from_dict(MOCK_PROGRAMS[appliance_type]["data"]["programs"][0]),
Program.from_dict(MOCK_PROGRAMS[appliance_type]["data"]["programs"][0]),
)
async def _get_settings_side_effect(ha_id: str) -> ArrayOfSettings:
"""Get settings."""
return ArrayOfSettings.from_dict(
MOCK_SETTINGS.get(
next(
appliance for appliance in appliances if appliance.ha_id == ha_id
).type,
{},
).get("data", {"settings": []})
)
async def _get_setting_side_effect(ha_id: str, setting_key: SettingKey):
"""Get setting."""
for appliance_ in appliances:
if appliance_.ha_id == ha_id:
settings = MOCK_SETTINGS.get(
appliance_.type,
{},
).get("data", {"settings": []})
for setting_dict in cast(list[dict], settings["settings"]):
if setting_dict["key"] == setting_key:
return GetSetting.from_dict(setting_dict)
raise HomeConnectApiError("error.key", "error description")
async def _get_available_commands_side_effect(ha_id: str) -> ArrayOfCommands:
"""Get available commands."""
for appliance_ in appliances:
if appliance_.ha_id == ha_id and appliance_.type in MOCK_AVAILABLE_COMMANDS:
return ArrayOfCommands.from_dict(
MOCK_AVAILABLE_COMMANDS[appliance_.type]
)
raise HomeConnectApiError("error.key", "error description")
mock.start_program = AsyncMock( mock.start_program = AsyncMock(
side_effect=_get_set_program_side_effect( side_effect=_get_set_program_side_effect(
event_queue, EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM event_queue, EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM
@ -431,7 +423,11 @@ def mock_client(request: pytest.FixtureRequest) -> MagicMock:
@pytest.fixture(name="client_with_exception") @pytest.fixture(name="client_with_exception")
def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock: def mock_client_with_exception(
appliances: list[HomeAppliance],
appliance: HomeAppliance | None,
request: pytest.FixtureRequest,
) -> MagicMock:
"""Fixture to mock Client from HomeConnect that raise exceptions.""" """Fixture to mock Client from HomeConnect that raise exceptions."""
mock = MagicMock( mock = MagicMock(
autospec=HomeConnectClient, autospec=HomeConnectClient,
@ -449,7 +445,8 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
for event in await event_queue.get(): for event in await event_queue.get():
yield event yield event
mock.get_home_appliances = AsyncMock(return_value=copy.deepcopy(MOCK_APPLIANCES)) appliances = [appliance] if appliance else appliances
mock.get_home_appliances = AsyncMock(return_value=ArrayOfHomeAppliances(appliances))
mock.stream_all_events = stream_all_events mock.stream_all_events = stream_all_events
mock.start_program = AsyncMock(side_effect=exception) mock.start_program = AsyncMock(side_effect=exception)
@ -477,12 +474,52 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
@pytest.fixture(name="appliance_ha_id") @pytest.fixture(name="appliance_ha_id")
def mock_appliance_ha_id(request: pytest.FixtureRequest) -> str: def mock_appliance_ha_id(
"""Fixture to mock Appliance.""" appliances: list[HomeAppliance], request: pytest.FixtureRequest
app = "Washer" ) -> str:
"""Fixture to get the ha_id of an appliance."""
appliance_type = "Washer"
if hasattr(request, "param") and request.param: if hasattr(request, "param") and request.param:
app = request.param appliance_type = request.param
for appliance in MOCK_APPLIANCES.homeappliances: for appliance in appliances:
if appliance.type == app: if appliance.type == appliance_type:
return appliance.ha_id return appliance.ha_id
raise ValueError(f"Appliance {app} not found") raise ValueError(f"Appliance {appliance_type} not found")
@pytest.fixture(name="appliances")
def mock_appliances(
appliances_data: str, request: pytest.FixtureRequest
) -> list[HomeAppliance]:
"""Fixture to mock the returned appliances."""
appliances = ArrayOfHomeAppliances.from_json(appliances_data).homeappliances
appliance_types = {appliance.type for appliance in appliances}
if hasattr(request, "param") and request.param:
appliance_types = request.param
return [appliance for appliance in appliances if appliance.type in appliance_types]
@pytest.fixture(name="appliance")
def mock_appliance(
appliances_data: str, request: pytest.FixtureRequest
) -> HomeAppliance | None:
"""Fixture to mock a single specific appliance to return."""
appliance_type = None
if hasattr(request, "param") and request.param:
appliance_type = request.param
return next(
(
appliance
for appliance in ArrayOfHomeAppliances.from_json(
appliances_data
).homeappliances
if appliance.type == appliance_type
),
None,
)
@pytest.fixture(name="appliances_data")
def appliances_data_fixture() -> str:
"""Fixture to return a the string for an array of appliances."""
return load_fixture("appliances.json", integration=DOMAIN)

View File

@ -1,123 +1,121 @@
{ {
"data": { "homeappliances": [
"homeappliances": [ {
{ "name": "FridgeFreezer",
"name": "FridgeFreezer", "brand": "SIEMENS",
"brand": "SIEMENS", "vib": "HCS05FRF1",
"vib": "HCS05FRF1", "connected": true,
"connected": true, "type": "FridgeFreezer",
"type": "FridgeFreezer", "enumber": "HCS05FRF1/03",
"enumber": "HCS05FRF1/03", "haId": "SIEMENS-HCS05FRF1-304F4F9E541D"
"haId": "SIEMENS-HCS05FRF1-304F4F9E541D" },
}, {
{ "name": "Dishwasher",
"name": "Dishwasher", "brand": "SIEMENS",
"brand": "SIEMENS", "vib": "HCS02DWH1",
"vib": "HCS02DWH1", "connected": true,
"connected": true, "type": "Dishwasher",
"type": "Dishwasher", "enumber": "HCS02DWH1/03",
"enumber": "HCS02DWH1/03", "haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1"
"haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1" },
}, {
{ "name": "Oven",
"name": "Oven", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS01OVN1",
"vib": "HCS01OVN1", "connected": true,
"connected": true, "type": "Oven",
"type": "Oven", "enumber": "HCS01OVN1/03",
"enumber": "HCS01OVN1/03", "haId": "BOSCH-HCS01OVN1-43E0065FE245"
"haId": "BOSCH-HCS01OVN1-43E0065FE245" },
}, {
{ "name": "Washer",
"name": "Washer", "brand": "SIEMENS",
"brand": "SIEMENS", "vib": "HCS03WCH1",
"vib": "HCS03WCH1", "connected": true,
"connected": true, "type": "Washer",
"type": "Washer", "enumber": "HCS03WCH1/03",
"enumber": "HCS03WCH1/03", "haId": "SIEMENS-HCS03WCH1-7BC6383CF794"
"haId": "SIEMENS-HCS03WCH1-7BC6383CF794" },
}, {
{ "name": "Dryer",
"name": "Dryer", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS04DYR1",
"vib": "HCS04DYR1", "connected": true,
"connected": true, "type": "Dryer",
"type": "Dryer", "enumber": "HCS04DYR1/03",
"enumber": "HCS04DYR1/03", "haId": "BOSCH-HCS04DYR1-831694AE3C5A"
"haId": "BOSCH-HCS04DYR1-831694AE3C5A" },
}, {
{ "name": "CoffeeMaker",
"name": "CoffeeMaker", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS06COM1",
"vib": "HCS06COM1", "connected": true,
"connected": true, "type": "CoffeeMaker",
"type": "CoffeeMaker", "enumber": "HCS06COM1/03",
"enumber": "HCS06COM1/03", "haId": "BOSCH-HCS06COM1-D70390681C2C"
"haId": "BOSCH-HCS06COM1-D70390681C2C" },
}, {
{ "name": "WasherDryer",
"name": "WasherDryer", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000001",
"vib": "HCS000001", "connected": true,
"connected": true, "type": "WasherDryer",
"type": "WasherDryer", "enumber": "HCS000000/01",
"enumber": "HCS000000/01", "haId": "BOSCH-HCS000000-D00000000001"
"haId": "BOSCH-HCS000000-D00000000001" },
}, {
{ "name": "Refrigerator",
"name": "Refrigerator", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000002",
"vib": "HCS000002", "connected": true,
"connected": true, "type": "Refrigerator",
"type": "Refrigerator", "enumber": "HCS000000/02",
"enumber": "HCS000000/02", "haId": "BOSCH-HCS000000-D00000000002"
"haId": "BOSCH-HCS000000-D00000000002" },
}, {
{ "name": "Freezer",
"name": "Freezer", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000003",
"vib": "HCS000003", "connected": true,
"connected": true, "type": "Freezer",
"type": "Freezer", "enumber": "HCS000000/03",
"enumber": "HCS000000/03", "haId": "BOSCH-HCS000000-D00000000003"
"haId": "BOSCH-HCS000000-D00000000003" },
}, {
{ "name": "Hood",
"name": "Hood", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000004",
"vib": "HCS000004", "connected": true,
"connected": true, "type": "Hood",
"type": "Hood", "enumber": "HCS000000/04",
"enumber": "HCS000000/04", "haId": "BOSCH-HCS000000-D00000000004"
"haId": "BOSCH-HCS000000-D00000000004" },
}, {
{ "name": "Hob",
"name": "Hob", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000005",
"vib": "HCS000005", "connected": true,
"connected": true, "type": "Hob",
"type": "Hob", "enumber": "HCS000000/05",
"enumber": "HCS000000/05", "haId": "BOSCH-HCS000000-D00000000005"
"haId": "BOSCH-HCS000000-D00000000005" },
}, {
{ "name": "CookProcessor",
"name": "CookProcessor", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000006",
"vib": "HCS000006", "connected": true,
"connected": true, "type": "CookProcessor",
"type": "CookProcessor", "enumber": "HCS000000/06",
"enumber": "HCS000000/06", "haId": "BOSCH-HCS000000-D00000000006"
"haId": "BOSCH-HCS000000-D00000000006" },
}, {
{ "name": "DNE",
"name": "DNE", "brand": "BOSCH",
"brand": "BOSCH", "vib": "HCS000000",
"vib": "HCS000000", "connected": true,
"connected": true, "type": "DNE",
"type": "DNE", "enumber": "HCS000000/00",
"enumber": "HCS000000/00", "haId": "BOSCH-000000000-000000000000"
"haId": "BOSCH-000000000-000000000000" }
} ]
]
}
} }

View File

@ -1,19 +1,20 @@
"""Test for Home Connect coordinator.""" """Test for Home Connect coordinator."""
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import copy
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.model import ( from aiohomeconnect.model import (
ArrayOfEvents, ArrayOfEvents,
ArrayOfHomeAppliances,
ArrayOfSettings, ArrayOfSettings,
ArrayOfStatus, ArrayOfStatus,
Event, Event,
EventKey, EventKey,
EventMessage, EventMessage,
EventType, EventType,
HomeAppliance,
) )
from aiohomeconnect.model.error import ( from aiohomeconnect.model.error import (
EventStreamInterruptedError, EventStreamInterruptedError,
@ -28,6 +29,7 @@ from homeassistant.components.home_connect.const import (
BSH_DOOR_STATE_OPEN, BSH_DOOR_STATE_OPEN,
BSH_EVENT_PRESENT_STATE_PRESENT, BSH_EVENT_PRESENT_STATE_PRESENT,
BSH_POWER_OFF, BSH_POWER_OFF,
DOMAIN,
) )
from homeassistant.config_entries import ConfigEntries, ConfigEntryState from homeassistant.config_entries import ConfigEntries, ConfigEntryState
from homeassistant.const import EVENT_STATE_REPORTED, Platform from homeassistant.const import EVENT_STATE_REPORTED, Platform
@ -37,12 +39,10 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
callback, callback,
) )
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import MOCK_APPLIANCES
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -81,16 +81,21 @@ async def test_coordinator_update_failing_get_appliances(
@pytest.mark.usefixtures("setup_credentials") @pytest.mark.usefixtures("setup_credentials")
@pytest.mark.parametrize("platforms", [("binary_sensor",)]) @pytest.mark.parametrize("platforms", [("binary_sensor",)])
@pytest.mark.parametrize("appliance_ha_id", ["Washer"], indirect=True) @pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
async def test_coordinator_failure_refresh_and_stream( async def test_coordinator_failure_refresh_and_stream(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]], integration_setup: Callable[[MagicMock], Awaitable[bool]],
client: MagicMock, client: MagicMock,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
appliance_ha_id: str, appliance: HomeAppliance,
) -> None: ) -> None:
"""Test entity available state via coordinator refresh and event stream.""" """Test entity available state via coordinator refresh and event stream."""
appliance_data = (
cast(str, appliance.to_json())
.replace("ha_id", "haId")
.replace("e_number", "enumber")
)
entity_id_1 = "binary_sensor.washer_remote_control" entity_id_1 = "binary_sensor.washer_remote_control"
entity_id_2 = "binary_sensor.washer_remote_start" entity_id_2 = "binary_sensor.washer_remote_start"
await async_setup_component(hass, "homeassistant", {}) await async_setup_component(hass, "homeassistant", {})
@ -121,7 +126,9 @@ async def test_coordinator_failure_refresh_and_stream(
# Test that the entity becomes available again after a successful update. # Test that the entity becomes available again after a successful update.
client.get_home_appliances.side_effect = None client.get_home_appliances.side_effect = None
client.get_home_appliances.return_value = copy.deepcopy(MOCK_APPLIANCES) client.get_home_appliances.return_value = ArrayOfHomeAppliances(
[HomeAppliance.from_json(appliance_data)]
)
# Move time forward to pass the debounce time. # Move time forward to pass the debounce time.
freezer.tick(timedelta(hours=1)) freezer.tick(timedelta(hours=1))
@ -166,11 +173,13 @@ async def test_coordinator_failure_refresh_and_stream(
# Now make the entity available again. # Now make the entity available again.
client.get_home_appliances.side_effect = None client.get_home_appliances.side_effect = None
client.get_home_appliances.return_value = copy.deepcopy(MOCK_APPLIANCES) client.get_home_appliances.return_value = ArrayOfHomeAppliances(
[HomeAppliance.from_json(appliance_data)]
)
# One event should make all entities for this appliance available again. # One event should make all entities for this appliance available again.
event_message = EventMessage( event_message = EventMessage(
appliance_ha_id, appliance.ha_id,
EventType.STATUS, EventType.STATUS,
ArrayOfEvents( ArrayOfEvents(
[ [
@ -399,6 +408,9 @@ async def test_event_listener_error(
assert not config_entry._background_tasks assert not config_entry._background_tasks
@pytest.mark.usefixtures("setup_credentials")
@pytest.mark.parametrize("platforms", [("sensor",)])
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"exception", "exception",
[HomeConnectRequestError(), EventStreamInterruptedError()], [HomeConnectRequestError(), EventStreamInterruptedError()],
@ -429,11 +441,10 @@ async def test_event_listener_resilience(
after_event_expected_state: str, after_event_expected_state: str,
exception: HomeConnectError, exception: HomeConnectError,
hass: HomeAssistant, hass: HomeAssistant,
appliance: HomeAppliance,
client: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]], integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None: ) -> None:
"""Test that the event listener is resilient to interruptions.""" """Test that the event listener is resilient to interruptions."""
future = hass.loop.create_future() future = hass.loop.create_future()
@ -467,7 +478,7 @@ async def test_event_listener_resilience(
await client.add_events( await client.add_events(
[ [
EventMessage( EventMessage(
appliance_ha_id, appliance.ha_id,
EventType.STATUS, EventType.STATUS,
ArrayOfEvents( ArrayOfEvents(
[ [
@ -489,3 +500,44 @@ async def test_event_listener_resilience(
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state assert state
assert state.state == after_event_expected_state assert state.state == after_event_expected_state
async def test_devices_updated_on_refresh(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test handling of devices added or deleted while event stream is down."""
appliances: list[HomeAppliance] = (
client.get_home_appliances.return_value.homeappliances
)
assert len(appliances) >= 3
client.get_home_appliances = AsyncMock(
return_value=ArrayOfHomeAppliances(appliances[:2]),
)
await async_setup_component(hass, "homeassistant", {})
assert config_entry.state == ConfigEntryState.NOT_LOADED
await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
for appliance in appliances[:2]:
assert device_registry.async_get_device({(DOMAIN, appliance.ha_id)})
assert not device_registry.async_get_device({(DOMAIN, appliances[2].ha_id)})
client.get_home_appliances = AsyncMock(
return_value=ArrayOfHomeAppliances(appliances[1:3]),
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{"entity_id": "switch.dishwasher_power"},
blocking=True,
)
assert not device_registry.async_get_device({(DOMAIN, appliances[0].ha_id)})
for appliance in appliances[2:3]:
assert device_registry.async_get_device({(DOMAIN, appliance.ha_id)})

View File

@ -3,11 +3,15 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.const import OAUTH2_TOKEN from aiohomeconnect.const import OAUTH2_TOKEN
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError from aiohomeconnect.model.error import (
HomeConnectError,
TooManyRequestsError,
UnauthorizedError,
)
import aiohttp import aiohttp
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -355,6 +359,48 @@ async def test_client_error(
assert client_with_exception.get_home_appliances.call_count == 1 assert client_with_exception.get_home_appliances.call_count == 1
@pytest.mark.parametrize(
"raising_exception_method",
[
"get_settings",
"get_status",
"get_all_programs",
"get_available_commands",
"get_available_program",
],
)
async def test_client_rate_limit_error(
raising_exception_method: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test client errors during setup integration."""
retry_after = 42
original_mock = getattr(client, raising_exception_method)
mock = AsyncMock()
async def side_effect(*args, **kwargs):
if mock.call_count <= 1:
raise TooManyRequestsError("error.key", retry_after=retry_after)
return await original_mock(*args, **kwargs)
mock.side_effect = side_effect
setattr(client, raising_exception_method, mock)
assert config_entry.state == ConfigEntryState.NOT_LOADED
with patch(
"homeassistant.components.home_connect.coordinator.asyncio_sleep",
) as asyncio_sleep_mock:
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
assert mock.call_count >= 2
asyncio_sleep_mock.assert_called_once_with(retry_after)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service_call", "service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS, SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,

View File

@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import random import random
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.model import ( from aiohomeconnect.model import (
ArrayOfEvents, ArrayOfEvents,
@ -22,6 +22,7 @@ from aiohomeconnect.model.error import (
HomeConnectApiError, HomeConnectApiError,
HomeConnectError, HomeConnectError,
SelectedProgramNotSetError, SelectedProgramNotSetError,
TooManyRequestsError,
) )
from aiohomeconnect.model.program import ( from aiohomeconnect.model.program import (
ProgramDefinitionConstraints, ProgramDefinitionConstraints,
@ -47,7 +48,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture @pytest.fixture
@ -340,6 +341,98 @@ async def test_number_entity_functionality(
assert hass.states.is_state(entity_id, str(float(value))) assert hass.states.is_state(entity_id, str(float(value)))
@pytest.mark.parametrize("appliance_ha_id", ["FridgeFreezer"], indirect=True)
@pytest.mark.parametrize("retry_after", [0, None])
@pytest.mark.parametrize(
(
"entity_id",
"setting_key",
"type",
"min_value",
"max_value",
"step_size",
"unit_of_measurement",
),
[
(
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
"Double",
7,
15,
5,
"°C",
),
],
)
@patch("homeassistant.components.home_connect.entity.API_DEFAULT_RETRY_AFTER", new=0)
async def test_fetch_constraints_after_rate_limit_error(
retry_after: int | None,
appliance_ha_id: str,
entity_id: str,
setting_key: SettingKey,
type: str,
min_value: int,
max_value: int,
step_size: int,
unit_of_measurement: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test that, if a API rate limit error is raised, the constraints are fetched later."""
def get_settings_side_effect(ha_id: str):
if ha_id != appliance_ha_id:
return ArrayOfSettings([])
return ArrayOfSettings(
[
GetSetting(
key=setting_key,
raw_key=setting_key.value,
value=random.randint(min_value, max_value),
)
]
)
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
client.get_setting = AsyncMock(
side_effect=[
TooManyRequestsError("error.key", retry_after=retry_after),
GetSetting(
key=setting_key,
raw_key=setting_key.value,
value=random.randint(min_value, max_value),
unit=unit_of_measurement,
type=type,
constraints=SettingConstraints(
min=min_value,
max=max_value,
step_size=step_size,
),
),
]
)
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert client.get_setting.call_count == 2
entity_state = hass.states.get(entity_id)
assert entity_state
attributes = entity_state.attributes
assert attributes["min"] == min_value
assert attributes["max"] == max_value
assert attributes["step"] == step_size
assert attributes["unit_of_measurement"] == unit_of_measurement
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_id", "setting_key", "mock_attr"), ("entity_id", "setting_key", "mock_attr"),
[ [

View File

@ -21,6 +21,7 @@ from aiohomeconnect.model.error import (
ActiveProgramNotSetError, ActiveProgramNotSetError,
HomeConnectError, HomeConnectError,
SelectedProgramNotSetError, SelectedProgramNotSetError,
TooManyRequestsError,
) )
from aiohomeconnect.model.program import ( from aiohomeconnect.model.program import (
EnumerateProgram, EnumerateProgram,
@ -50,7 +51,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture @pytest.fixture
@ -521,9 +522,18 @@ async def test_select_functionality(
( (
"select.hood_ambient_light_color", "select.hood_ambient_light_color",
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR, SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
[f"BSH.Common.EnumType.AmbientLightColor.Color{i}" for i in range(50)], [f"BSH.Common.EnumType.AmbientLightColor.Color{i}" for i in range(1, 50)],
{str(i) for i in range(1, 50)}, {str(i) for i in range(1, 50)},
), ),
(
"select.hood_ambient_light_color",
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
[
"A.Non.Documented.Option",
"BSH.Common.EnumType.AmbientLightColor.Color42",
],
{"42"},
),
], ],
) )
async def test_fetch_allowed_values( async def test_fetch_allowed_values(
@ -566,6 +576,139 @@ async def test_fetch_allowed_values(
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
@pytest.mark.parametrize("appliance_ha_id", ["Hood"], indirect=True)
@pytest.mark.parametrize(
(
"entity_id",
"setting_key",
"allowed_values",
"expected_options",
),
[
(
"select.hood_ambient_light_color",
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
[f"BSH.Common.EnumType.AmbientLightColor.Color{i}" for i in range(50)],
{str(i) for i in range(1, 50)},
),
],
)
async def test_fetch_allowed_values_after_rate_limit_error(
appliance_ha_id: str,
entity_id: str,
setting_key: SettingKey,
allowed_values: list[str | None],
expected_options: set[str],
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test fetch allowed values."""
def get_settings_side_effect(ha_id: str):
if ha_id != appliance_ha_id:
return ArrayOfSettings([])
return ArrayOfSettings(
[
GetSetting(
key=setting_key,
raw_key=setting_key.value,
value="", # Not important
)
]
)
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
client.get_setting = AsyncMock(
side_effect=[
TooManyRequestsError("error.key", retry_after=0),
GetSetting(
key=setting_key,
raw_key=setting_key.value,
value="", # Not important
constraints=SettingConstraints(
allowed_values=allowed_values,
),
),
]
)
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert client.get_setting.call_count == 2
entity_state = hass.states.get(entity_id)
assert entity_state
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
@pytest.mark.parametrize("appliance_ha_id", ["Hood"], indirect=True)
@pytest.mark.parametrize(
(
"entity_id",
"setting_key",
"exception",
"expected_options",
),
[
(
"select.hood_ambient_light_color",
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
HomeConnectError(),
{
"b_s_h_common_enum_type_ambient_light_color_custom_color",
*{str(i) for i in range(1, 100)},
},
),
],
)
async def test_default_values_after_fetch_allowed_values_error(
appliance_ha_id: str,
entity_id: str,
setting_key: SettingKey,
exception: Exception,
expected_options: set[str],
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test fetch allowed values."""
def get_settings_side_effect(ha_id: str):
if ha_id != appliance_ha_id:
return ArrayOfSettings([])
return ArrayOfSettings(
[
GetSetting(
key=setting_key,
raw_key=setting_key.value,
value="", # Not important
)
]
)
client.get_settings = AsyncMock(side_effect=get_settings_side_effect)
client.get_setting = AsyncMock(side_effect=exception)
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state is ConfigEntryState.LOADED
assert client.get_setting.call_count == 1
entity_state = hass.states.get(entity_id)
assert entity_state
assert set(entity_state.attributes[ATTR_OPTIONS]) == expected_options
@pytest.mark.parametrize( @pytest.mark.parametrize(
("entity_id", "setting_key", "allowed_value", "value_to_set", "mock_attr"), ("entity_id", "setting_key", "allowed_value", "value_to_set", "mock_attr"),
[ [
@ -679,6 +822,17 @@ async def test_select_entity_error(
"laundry_care_washer_enum_type_temperature_ul_extra_hot", "laundry_care_washer_enum_type_temperature_ul_extra_hot",
}, },
), ),
(
"select.washer_temperature",
OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE,
[
"A.Non.Documented.Option",
"LaundryCare.Washer.EnumType.Temperature.UlWarm",
],
{
"laundry_care_washer_enum_type_temperature_ul_warm",
},
),
], ],
) )
async def test_options_functionality( async def test_options_functionality(

View File

@ -13,7 +13,7 @@ from aiohomeconnect.model import (
Status, Status,
StatusKey, StatusKey,
) )
from aiohomeconnect.model.error import HomeConnectApiError from aiohomeconnect.model.error import HomeConnectApiError, TooManyRequestsError
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -26,12 +26,13 @@ from homeassistant.components.home_connect.const import (
BSH_EVENT_PRESENT_STATE_PRESENT, BSH_EVENT_PRESENT_STATE_PRESENT,
DOMAIN, DOMAIN,
) )
from homeassistant.components.home_connect.coordinator import HomeConnectError
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry, async_fire_time_changed
TEST_HC_APP = "Dishwasher" TEST_HC_APP = "Dishwasher"
@ -724,3 +725,122 @@ async def test_sensor_unit_fetching(
) )
assert client.get_status_value.call_count == get_status_value_call_count assert client.get_status_value.call_count == get_status_value_call_count
@pytest.mark.parametrize(
(
"appliance_ha_id",
"entity_id",
"status_key",
),
[
(
"Oven",
"sensor.oven_current_oven_cavity_temperature",
StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE,
),
],
indirect=["appliance_ha_id"],
)
async def test_sensor_unit_fetching_error(
appliance_ha_id: str,
entity_id: str,
status_key: StatusKey,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test that the sensor entities are capable of fetching units."""
async def get_status_mock(ha_id: str) -> ArrayOfStatus:
if ha_id != appliance_ha_id:
return ArrayOfStatus([])
return ArrayOfStatus(
[
Status(
key=status_key,
raw_key=status_key.value,
value=0,
)
]
)
client.get_status = AsyncMock(side_effect=get_status_mock)
client.get_status_value = AsyncMock(side_effect=HomeConnectError())
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
assert hass.states.get(entity_id)
@pytest.mark.parametrize(
(
"appliance_ha_id",
"entity_id",
"status_key",
"unit",
),
[
(
"Oven",
"sensor.oven_current_oven_cavity_temperature",
StatusKey.COOKING_OVEN_CURRENT_CAVITY_TEMPERATURE,
"°C",
),
],
indirect=["appliance_ha_id"],
)
async def test_sensor_unit_fetching_after_rate_limit_error(
appliance_ha_id: str,
entity_id: str,
status_key: StatusKey,
unit: str,
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
) -> None:
"""Test that the sensor entities are capable of fetching units."""
async def get_status_mock(ha_id: str) -> ArrayOfStatus:
if ha_id != appliance_ha_id:
return ArrayOfStatus([])
return ArrayOfStatus(
[
Status(
key=status_key,
raw_key=status_key.value,
value=0,
)
]
)
client.get_status = AsyncMock(side_effect=get_status_mock)
client.get_status_value = AsyncMock(
side_effect=[
TooManyRequestsError("error.key", retry_after=0),
Status(
key=status_key,
raw_key=status_key.value,
value=0,
unit=unit,
),
]
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.LOADED
assert client.get_status_value.call_count == 2
entity_state = hass.states.get(entity_id)
assert entity_state
assert entity_state.attributes["unit_of_measurement"] == unit

View File

@ -101,28 +101,60 @@
"mode": "TypeB", "mode": "TypeB",
"Group1": [ "Group1": [
{ {
"mode": "TypeA",
"groupNumber": "Group1", "groupNumber": "Group1",
"doseType": "DoseA", "doseType": "DoseA",
"preWetTime": 0.5, "preWetTime": 0.5,
"preWetHoldTime": 1 "preWetHoldTime": 1
}, },
{ {
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseA",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1", "groupNumber": "Group1",
"doseType": "DoseB", "doseType": "DoseB",
"preWetTime": 0.5, "preWetTime": 0.5,
"preWetHoldTime": 1 "preWetHoldTime": 1
}, },
{ {
"mode": "TypeB",
"groupNumber": "Group1", "groupNumber": "Group1",
"doseType": "DoseC", "doseType": "DoseB",
"preWetTime": 3.2999999523162842, "preWetTime": 0,
"preWetHoldTime": 3.2999999523162842 "preWetHoldTime": 4
}, },
{ {
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseC",
"preWetTime": 3.3,
"preWetHoldTime": 3.3
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseC",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1", "groupNumber": "Group1",
"doseType": "DoseD", "doseType": "DoseD",
"preWetTime": 2, "preWetTime": 2,
"preWetHoldTime": 2 "preWetHoldTime": 2
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseD",
"preWetTime": 0,
"preWetHoldTime": 4
} }
] ]
}, },

View File

@ -82,10 +82,18 @@
"mode": "TypeB", "mode": "TypeB",
"Group1": [ "Group1": [
{ {
"mode": "TypeA",
"groupNumber": "Group1", "groupNumber": "Group1",
"doseType": "DoseA", "doseType": "Continuous",
"preWetTime": 2, "preWetTime": 2,
"preWetHoldTime": 3 "preWetHoldTime": 3
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "Continuous",
"preWetTime": 0,
"preWetHoldTime": 3
} }
] ]
}, },

View File

@ -27,22 +27,46 @@
}), }),
'plumbed_in': True, 'plumbed_in': True,
'prebrew_configuration': dict({ 'prebrew_configuration': dict({
'1': dict({ '1': list([
'off_time': 1, dict({
'on_time': 0.5, 'off_time': 1,
}), 'on_time': 0.5,
'2': dict({ }),
'off_time': 1, dict({
'on_time': 0.5, 'off_time': 4,
}), 'on_time': 0,
'3': dict({ }),
'off_time': 3.299999952316284, ]),
'on_time': 3.299999952316284, '2': list([
}), dict({
'4': dict({ 'off_time': 1,
'off_time': 2, 'on_time': 0.5,
'on_time': 2, }),
}), dict({
'off_time': 4,
'on_time': 0,
}),
]),
'3': list([
dict({
'off_time': 3.3,
'on_time': 3.3,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
'4': list([
dict({
'off_time': 2,
'on_time': 2,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
}), }),
'prebrew_mode': 'TypeB', 'prebrew_mode': 'TypeB',
'scale': None, 'scale': None,

View File

@ -419,7 +419,7 @@
'state': '121', 'state': '121',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_1-state] # name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-TypeA-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_1-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -438,7 +438,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_2-state] # name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-TypeA-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_2-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -457,7 +457,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_3-state] # name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-TypeA-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_3-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -473,10 +473,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '3.29999995231628', 'state': '3.3',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-Enabled-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_4-state] # name: test_pre_brew_infusion_key_numbers[prebrew_off_time-6-TypeA-set_prebrew_time-kwargs0-GS3 AV][GS012345_prebrew_off_time_key_4-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -495,7 +495,7 @@
'state': '2', 'state': '2',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_1-state] # name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-TypeA-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_1-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -514,7 +514,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_2-state] # name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-TypeA-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_2-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -533,7 +533,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_3-state] # name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-TypeA-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_3-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -549,10 +549,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '3.29999995231628', 'state': '3.3',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-Enabled-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_4-state] # name: test_pre_brew_infusion_key_numbers[prebrew_on_time-6-TypeA-set_prebrew_time-kwargs1-GS3 AV][GS012345_prebrew_on_time_key_4-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -587,7 +587,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '1', 'state': '4',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_2-state] # name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_2-state]
@ -606,7 +606,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '1', 'state': '4',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_3-state] # name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_3-state]
@ -625,7 +625,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '3.29999995231628', 'state': '4',
}) })
# --- # ---
# name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_4-state] # name: test_pre_brew_infusion_key_numbers[preinfusion_time-7-TypeB-set_preinfusion_time-kwargs2-GS3 AV][GS012345_preinfusion_time_key_4-state]
@ -644,10 +644,10 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '2', 'state': '4',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini] # name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-TypeA-6-kwargs0-Linea Mini]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -666,7 +666,7 @@
'state': '3', 'state': '3',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Linea Mini].1 # name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-TypeA-6-kwargs0-Linea Mini].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -705,7 +705,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra] # name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-TypeA-6-kwargs0-Micra]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -724,7 +724,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-Enabled-6-kwargs0-Micra].1 # name: test_pre_brew_infusion_numbers[prebrew_off_time-set_prebrew_time-TypeA-6-kwargs0-Micra].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -763,7 +763,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini] # name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-TypeA-6-kwargs1-Linea Mini]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -782,7 +782,7 @@
'state': '3', 'state': '3',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Linea Mini].1 # name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-TypeA-6-kwargs1-Linea Mini].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -821,7 +821,7 @@
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>, 'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra] # name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-TypeA-6-kwargs1-Micra]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'device_class': 'duration', 'device_class': 'duration',
@ -840,7 +840,7 @@
'state': '1', 'state': '1',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-Enabled-6-kwargs1-Micra].1 # name: test_pre_brew_infusion_numbers[prebrew_on_time-set_prebrew_time-TypeA-6-kwargs1-Micra].1
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@ -953,7 +953,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': '1', 'state': '4',
}) })
# --- # ---
# name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra].1 # name: test_pre_brew_infusion_numbers[preinfusion_time-set_preinfusion_time-TypeB-7-kwargs2-Micra].1

View File

@ -170,12 +170,18 @@ async def test_bluetooth_is_set_from_discovery(
"homeassistant.components.lamarzocco.async_discovered_service_info", "homeassistant.components.lamarzocco.async_discovered_service_info",
return_value=[service_info], return_value=[service_info],
) as discovery, ) as discovery,
patch("homeassistant.components.lamarzocco.LaMarzoccoMachine") as init_device, patch(
"homeassistant.components.lamarzocco.LaMarzoccoMachine"
) as mock_machine_class,
): ):
mock_machine = MagicMock()
mock_machine.get_firmware = AsyncMock()
mock_machine.firmware = mock_lamarzocco.firmware
mock_machine_class.return_value = mock_machine
await async_init_integration(hass, mock_config_entry) await async_init_integration(hass, mock_config_entry)
discovery.assert_called_once() discovery.assert_called_once()
init_device.assert_called_once() assert mock_machine_class.call_count == 2
_, kwargs = init_device.call_args _, kwargs = mock_machine_class.call_args
assert kwargs["bluetooth_client"] is not None assert kwargs["bluetooth_client"] is not None
assert mock_config_entry.data[CONF_NAME] == service_info.name assert mock_config_entry.data[CONF_NAME] == service_info.name
assert mock_config_entry.data[CONF_MAC] == service_info.address assert mock_config_entry.data[CONF_MAC] == service_info.address
@ -223,6 +229,19 @@ async def test_gateway_version_issue(
assert (issue is not None) == issue_exists assert (issue is not None) == issue_exists
async def test_conf_host_removed_for_new_gateway(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_lamarzocco: MagicMock,
) -> None:
"""Make sure we get the issue for certain gateway firmware versions."""
mock_lamarzocco.firmware[FirmwareType.GATEWAY].current_version = "v5.0.9"
await async_init_integration(hass, mock_config_entry)
assert CONF_HOST not in mock_config_entry.data
async def test_device( async def test_device(
hass: HomeAssistant, hass: HomeAssistant,
mock_lamarzocco: MagicMock, mock_lamarzocco: MagicMock,

View File

@ -46,8 +46,12 @@ def get_mock_session(
mock_response = Mock() mock_response = Mock()
mock_response.content_length = content_length mock_response.content_length = content_length
mock_response.headers = {}
mock_response.status = 200
mock_response.reason = "OK"
mock_response.content_type = content_type mock_response.content_type = content_type
mock_response.content.iter_chunked = Mock(return_value=content) mock_response.content.iter_chunked = Mock(return_value=content)
mock_response.text = AsyncMock(return_value="test")
mock_session = Mock() mock_session = Mock()
mock_session.get = AsyncMock(return_value=mock_response) mock_session.get = AsyncMock(return_value=mock_response)
@ -178,16 +182,18 @@ async def test_playback_proxy_timeout(
assert response.status == 200 assert response.status == 200
@pytest.mark.parametrize(("content_type"), [("video/x-flv"), ("text/html")])
async def test_playback_wrong_content( async def test_playback_wrong_content(
hass: HomeAssistant, hass: HomeAssistant,
reolink_connect: MagicMock, reolink_connect: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
content_type: str,
) -> None: ) -> None:
"""Test playback proxy URL with a wrong content type in the response.""" """Test playback proxy URL with a wrong content type in the response."""
reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL) reolink_connect.get_vod_source.return_value = (TEST_MIME_TYPE_MP4, TEST_URL)
mock_session = get_mock_session(content_type="video/x-flv") mock_session = get_mock_session(content_type=content_type)
with patch( with patch(
"homeassistant.components.reolink.views.async_get_clientsession", "homeassistant.components.reolink.views.async_get_clientsession",

View File

@ -110,6 +110,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"da_rvc_normal_000001", "da_rvc_normal_000001",
"da_ks_microwave_0101x", "da_ks_microwave_0101x",
"da_ks_range_0101x", "da_ks_range_0101x",
"da_ks_oven_01061",
"hue_color_temperature_bulb", "hue_color_temperature_bulb",
"hue_rgbw_color_bulb", "hue_rgbw_color_bulb",
"c2c_shade", "c2c_shade",
@ -131,6 +132,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"abl_light_b_001", "abl_light_b_001",
"tplink_p110", "tplink_p110",
"ikea_kadrilj", "ikea_kadrilj",
"aux_ac",
] ]
) )
def device_fixture( def device_fixture(

View File

@ -0,0 +1,69 @@
{
"components": {
"main": {
"partyvoice23922.vtempset": {
"vtemp": {
"value": 20,
"unit": "C",
"timestamp": "2024-12-05T20:03:33.161Z"
}
},
"airConditionerFanMode": {
"fanMode": {
"value": "auto",
"timestamp": "2024-12-05T20:03:32.930Z"
},
"supportedAcFanModes": {
"value": null
},
"availableAcFanModes": {
"value": null
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 20.0,
"unit": "C",
"timestamp": "2024-12-05T20:03:33.066Z"
}
},
"airConditionerMode": {
"availableAcModes": {
"value": null
},
"supportedAcModes": {
"value": null
},
"airConditionerMode": {
"value": "cool",
"timestamp": "2024-12-05T20:03:32.845Z"
}
},
"fanSpeed": {
"fanSpeed": {
"value": 0,
"timestamp": "2024-12-05T20:03:33.334Z"
}
},
"thermostatCoolingSetpoint": {
"coolingSetpointRange": {
"value": null
},
"coolingSetpoint": {
"value": 20.0,
"unit": "C",
"timestamp": "2024-12-05T20:03:33.243Z"
}
},
"switch": {
"switch": {
"value": "off",
"timestamp": "2024-12-05T20:03:32.662Z"
}
}
}
}
}

View File

@ -0,0 +1,566 @@
{
"components": {
"main": {
"ovenSetpoint": {
"ovenSetpointRange": {
"value": null
},
"ovenSetpoint": {
"value": 220,
"timestamp": "2025-03-15T12:06:07.818Z"
}
},
"refresh": {},
"samsungce.doorState": {
"doorState": {
"value": "closed",
"timestamp": "2025-03-15T09:25:35.157Z"
}
},
"samsungce.microwavePower": {
"supportedPowerLevels": {
"value": null
},
"powerLevel": {
"value": "0W",
"timestamp": "2025-03-15T12:06:07.803Z"
}
},
"samsungce.waterReservoir": {
"slotState": {
"value": null
}
},
"samsungce.kitchenDeviceDefaults": {
"defaultOperationTime": {
"value": null
},
"defaultOvenMode": {
"value": "Convection",
"timestamp": "2025-03-15T12:06:07.758Z"
},
"defaultOvenSetpoint": {
"value": null
}
},
"execute": {
"data": {
"value": null
}
},
"samsungce.deviceIdentification": {
"micomAssayCode": {
"value": null
},
"modelName": {
"value": null
},
"serialNumber": {
"value": null
},
"serialNumberExtra": {
"value": null
},
"modelClassificationCode": {
"value": null
},
"description": {
"value": null
},
"releaseYear": {
"value": null
},
"binaryId": {
"value": "TP1X_DA-KS-OVEN-01061",
"timestamp": "2025-03-13T20:35:02.073Z"
}
},
"samsungce.ovenDrainageRequirement": {
"drainageRequirement": {
"value": null
}
},
"ocf": {
"st": {
"value": null
},
"mndt": {
"value": null
},
"mnfv": {
"value": "AKS-WW-TP1X-21-OVEN_40211229",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnhw": {
"value": "Realtek",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"di": {
"value": "9447959a-0dfa-6b27-d40d-650da525c53f",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnsl": {
"value": "http://www.samsung.com",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"n": {
"value": "[oven] Samsung",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnmo": {
"value": "TP1X_DA-KS-OVEN-01061|40457041|50030018001611000A00000000000000",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"vid": {
"value": "DA-KS-OVEN-01061",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnpv": {
"value": "DAWIT 3.0",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"mnos": {
"value": "TizenRT 3.1",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"pi": {
"value": "9447959a-0dfa-6b27-d40d-650da525c53f",
"timestamp": "2025-01-08T17:29:14.260Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2025-01-08T17:29:14.260Z"
}
},
"remoteControlStatus": {
"remoteControlEnabled": {
"value": "true",
"timestamp": "2025-03-15T09:47:55.406Z"
}
},
"samsungce.kitchenDeviceIdentification": {
"regionCode": {
"value": "EU",
"timestamp": "2025-03-15T12:06:07.758Z"
},
"modelCode": {
"value": "NQ7000B-/EU7",
"timestamp": "2025-03-15T12:06:07.758Z"
},
"fuel": {
"value": null
},
"type": {
"value": "oven",
"timestamp": "2025-01-08T17:29:12.924Z"
},
"representativeComponent": {
"value": null
}
},
"samsungce.kitchenModeSpecification": {
"specification": {
"value": {
"single": [
{
"mode": "NoOperation",
"supportedOperations": [],
"supportedOptions": {}
},
{
"mode": "Autocook",
"supportedOperations": [],
"supportedOptions": {}
},
{
"mode": "Convection",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 40,
"max": 230,
"default": 160,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "FanConventional",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 40,
"max": 230,
"default": 180,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "LargeGrill",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 150,
"max": 230,
"default": 220,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "FanGrill",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 40,
"max": 230,
"default": 180,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "MicroWaveGrill",
"supportedOperations": ["set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 40,
"max": 200,
"default": 200,
"resolution": 5
}
},
"operationTime": {
"min": "00:00:10",
"max": "01:30:00",
"default": "00:00:30",
"resolution": "00:00:10"
},
"powerLevel": {
"default": "300W",
"supportedValues": ["100W", "180W", "300W", "450W", "600W"]
}
}
},
{
"mode": "MicroWaveConvection",
"supportedOperations": ["set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 40,
"max": 200,
"default": 180,
"resolution": 5
}
},
"operationTime": {
"min": "00:00:10",
"max": "01:30:00",
"default": "00:00:30",
"resolution": "00:00:10"
},
"powerLevel": {
"default": "300W",
"supportedValues": ["100W", "180W", "300W", "450W", "600W"]
}
}
},
{
"mode": "AirFryer",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 150,
"max": 230,
"default": 220,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "MicroWave",
"supportedOperations": ["set"],
"supportedOptions": {
"operationTime": {
"min": "00:00:10",
"max": "01:30:00",
"default": "00:00:30",
"resolution": "00:00:10"
},
"powerLevel": {
"default": "800W",
"supportedValues": [
"100W",
"180W",
"300W",
"450W",
"600W",
"700W",
"800W"
]
}
}
},
{
"mode": "Deodorization",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"operationTime": {
"min": "00:00:10",
"max": "00:15:00",
"default": "00:05:00",
"resolution": "00:00:10"
}
}
},
{
"mode": "KeepWarm",
"supportedOperations": ["start", "set"],
"supportedOptions": {
"temperature": {
"C": {
"min": 60,
"max": 100,
"default": 60,
"resolution": 5
}
},
"operationTime": {
"min": "00:01:00",
"max": "10:00:00",
"default": "01:00:00",
"resolution": "00:01:00"
}
}
},
{
"mode": "SteamClean",
"supportedOperations": ["set"],
"supportedOptions": {}
}
]
},
"timestamp": "2025-01-08T17:29:14.757Z"
}
},
"custom.disabledCapabilities": {
"disabledCapabilities": {
"value": [
"samsungce.waterReservoir",
"samsungce.ovenDrainageRequirement"
],
"timestamp": "2025-03-15T12:06:07.758Z"
}
},
"samsungce.definedRecipe": {
"definedRecipe": {
"value": {
"cavityId": "0",
"recipeType": "0",
"categoryId": 0,
"itemId": 0,
"servingSize": 0,
"browingLevel": 0,
"option": 0
},
"timestamp": "2025-03-15T12:06:07.803Z"
}
},
"samsungce.driverVersion": {
"versionNumber": {
"value": 22100101,
"timestamp": "2025-01-08T17:29:12.924Z"
}
},
"samsungce.softwareUpdate": {
"targetModule": {
"value": null
},
"otnDUID": {
"value": "43CB2ZD4VUEGW",
"timestamp": "2025-03-13T20:35:02.073Z"
},
"lastUpdatedDate": {
"value": null
},
"availableModules": {
"value": [],
"timestamp": "2025-03-13T20:35:02.073Z"
},
"newVersionAvailable": {
"value": false,
"timestamp": "2025-03-13T20:35:02.073Z"
},
"operatingState": {
"value": null
},
"progress": {
"value": null
}
},
"temperatureMeasurement": {
"temperatureRange": {
"value": null
},
"temperature": {
"value": 30,
"unit": "C",
"timestamp": "2025-03-15T12:06:32.918Z"
}
},
"samsungce.ovenOperatingState": {
"completionTime": {
"value": "2025-03-15T12:06:09.550Z",
"timestamp": "2025-03-15T12:06:09.554Z"
},
"operatingState": {
"value": "running",
"timestamp": "2025-03-15T12:06:07.866Z"
},
"progress": {
"value": 0,
"timestamp": "2025-03-15T12:06:07.866Z"
},
"ovenJobState": {
"value": "preheat",
"timestamp": "2025-03-15T12:06:07.803Z"
},
"operationTime": {
"value": "00:00:00",
"timestamp": "2025-03-15T12:06:07.866Z"
}
},
"ovenMode": {
"supportedOvenModes": {
"value": ["Others", "Bake", "Broil", "ConvectionBroil", "warming"],
"timestamp": "2025-01-08T17:29:14.757Z"
},
"ovenMode": {
"value": "Bake",
"timestamp": "2025-03-15T12:06:07.758Z"
}
},
"ovenOperatingState": {
"completionTime": {
"value": "2025-03-15T12:06:09.550Z",
"timestamp": "2025-03-15T12:06:09.554Z"
},
"machineState": {
"value": "running",
"timestamp": "2025-03-15T12:06:07.866Z"
},
"progress": {
"value": 0,
"unit": "%",
"timestamp": "2025-03-15T12:06:07.866Z"
},
"supportedMachineStates": {
"value": null
},
"ovenJobState": {
"value": "preheat",
"timestamp": "2025-03-15T12:06:07.803Z"
},
"operationTime": {
"value": 0,
"timestamp": "2025-03-15T12:06:07.866Z"
}
},
"samsungce.ovenMode": {
"supportedOvenModes": {
"value": [
"NoOperation",
"Autocook",
"Convection",
"FanConventional",
"LargeGrill",
"FanGrill",
"MicroWaveGrill",
"MicroWaveConvection",
"AirFryer",
"MicroWave",
"Deodorization",
"KeepWarm",
"SteamClean"
],
"timestamp": "2025-01-08T17:29:14.757Z"
},
"ovenMode": {
"value": "Convection",
"timestamp": "2025-03-15T12:06:07.758Z"
}
},
"samsungce.lamp": {
"brightnessLevel": {
"value": "high",
"timestamp": "2025-03-15T12:06:07.956Z"
},
"supportedBrightnessLevel": {
"value": ["off", "high"],
"timestamp": "2025-03-15T12:06:07.758Z"
}
},
"samsungce.kidsLock": {
"lockState": {
"value": "unlocked",
"timestamp": "2025-03-13T20:35:02.170Z"
}
}
}
}
}

View File

@ -0,0 +1,81 @@
{
"items": [
{
"deviceId": "bf53a150-f8a4-45d1-aac4-86252475d551",
"name": "vedgeaircon.v1",
"label": "AUX A/C on-off",
"manufacturerName": "SmartThingsCommunity",
"presentationId": "ab252042-5669-3c2c-8b1b-d606bbcc9e04",
"deviceManufacturerCode": "SmartThings Community",
"locationId": "5db1e3d8-ea26-44b4-8ed0-1ba9c841fd57",
"ownerId": "5404aa57-6a68-4fe2-83ff-168ef769d1c7",
"roomId": "564cdd9a-fa9f-4187-902f-95656ef22989",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "switch",
"version": 1
},
{
"id": "airConditionerMode",
"version": 1
},
{
"id": "thermostatCoolingSetpoint",
"version": 1
},
{
"id": "airConditionerFanMode",
"version": 1
},
{
"id": "fanSpeed",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "partyvoice23922.vtempset",
"version": 1
}
],
"categories": [
{
"name": "AirConditioner",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2024-06-19T20:18:45.407Z",
"parentDeviceId": "e699599d-30f8-4cf0-8de7-6dbdba6a665f",
"profile": {
"id": "87f0ac35-e024-3c0a-8153-78ca27a6fe0c"
},
"lan": {
"networkId": "vEdge_A/C_1718828324.999",
"driverId": "0fd9a9a4-8863-4a83-97a7-5a288ff0f5a6",
"executingLocally": true,
"hubId": "e699599d-30f8-4cf0-8de7-6dbdba6a665f",
"provisioningState": "TYPED"
},
"type": "LAN",
"restrictionTier": 0,
"allowed": null,
"indoorMap": {
"coordinates": [130.0, 36.0, 378.0],
"rotation": [270.0, 0.0, 0.0],
"visible": true,
"data": null
},
"executionContext": "LOCAL",
"relationships": []
}
],
"_links": {}
}

View File

@ -0,0 +1,153 @@
{
"items": [
{
"deviceId": "9447959a-0dfa-6b27-d40d-650da525c53f",
"name": "[oven] Samsung",
"label": "Oven",
"manufacturerName": "Samsung Electronics",
"presentationId": "DA-KS-OVEN-01061",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "a81dc8da-5a3f-43b6-8c8a-1309f37eeeb9",
"ownerId": "97ee2149-9de0-3287-8245-24d6fd1609aa",
"roomId": "eb2167dd-8b8d-4131-b59e-5dd391b2e151",
"deviceTypeName": "Samsung OCF Oven",
"components": [
{
"id": "main",
"label": "main",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "remoteControlStatus",
"version": 1
},
{
"id": "ovenSetpoint",
"version": 1
},
{
"id": "ovenMode",
"version": 1
},
{
"id": "ovenOperatingState",
"version": 1
},
{
"id": "temperatureMeasurement",
"version": 1
},
{
"id": "samsungce.deviceIdentification",
"version": 1
},
{
"id": "samsungce.doorState",
"version": 1
},
{
"id": "samsungce.definedRecipe",
"version": 1
},
{
"id": "samsungce.kitchenDeviceIdentification",
"version": 1
},
{
"id": "samsungce.kitchenDeviceDefaults",
"version": 1
},
{
"id": "samsungce.driverVersion",
"version": 1
},
{
"id": "samsungce.ovenMode",
"version": 1
},
{
"id": "samsungce.ovenOperatingState",
"version": 1
},
{
"id": "samsungce.microwavePower",
"version": 1
},
{
"id": "samsungce.lamp",
"version": 1
},
{
"id": "samsungce.kitchenModeSpecification",
"version": 1
},
{
"id": "samsungce.kidsLock",
"version": 1
},
{
"id": "samsungce.softwareUpdate",
"version": 1
},
{
"id": "samsungce.waterReservoir",
"version": 1
},
{
"id": "samsungce.ovenDrainageRequirement",
"version": 1
},
{
"id": "custom.disabledCapabilities",
"version": 1
}
],
"categories": [
{
"name": "Oven",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2025-01-08T17:29:12.549Z",
"profile": {
"id": "eb34598f-f96a-3420-a90a-71693052eaa3"
},
"ocf": {
"ocfDeviceType": "oic.d.oven",
"name": "[oven] Samsung",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "TP1X_DA-KS-OVEN-01061|40457041|50030018001611000A00000000000000",
"platformVersion": "DAWIT 3.0",
"platformOS": "TizenRT 3.1",
"hwVersion": "Realtek",
"firmwareVersion": "AKS-WW-TP1X-21-OVEN_40211229",
"vendorId": "DA-KS-OVEN-01061",
"vendorResourceClientServerVersion": "Realtek Release 3.1.211122",
"lastSignupTime": "2025-01-08T17:29:08.536664213Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -1,4 +1,68 @@
# serializer version: 1 # serializer version: 1
# name: test_all_entities[aux_ac][climate.aux_a_c_on_off-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'fan_modes': None,
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.aux_a_c_on_off',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 393>,
'translation_key': None,
'unique_id': 'bf53a150-f8a4-45d1-aac4-86252475d551',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[aux_ac][climate.aux_a_c_on_off-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': 20.0,
'fan_mode': 'auto',
'fan_modes': None,
'friendly_name': 'AUX A/C on-off',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
]),
'max_temp': 35,
'min_temp': 7,
'supported_features': <ClimateEntityFeature: 393>,
'temperature': 20.0,
}),
'context': <ANY>,
'entity_id': 'climate.aux_a_c_on_off',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[bosch_radiator_thermostat_ii][climate.radiator_thermostat_ii_m_wohnzimmer-entry] # name: test_all_entities[bosch_radiator_thermostat_ii][climate.radiator_thermostat_ii_m_wohnzimmer-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -1,307 +1,311 @@
# serializer version: 1 # serializer version: 1
# name: test_config_entry_diagnostics[da_ac_rac_000001] # name: test_config_entry_diagnostics[da_ac_rac_000001]
dict({ dict({
'_links': dict({ 'devices': list([
}),
'items': list([
dict({ dict({
'allowed': list([ '_links': dict({
]), }),
'components': list([ 'items': list([
dict({ dict({
'capabilities': list([ 'allowed': list([
]),
'components': list([
dict({ dict({
'id': 'ocf', 'capabilities': list([
'version': 1, dict({
'id': 'ocf',
'version': 1,
}),
dict({
'id': 'switch',
'version': 1,
}),
dict({
'id': 'airConditionerMode',
'version': 1,
}),
dict({
'id': 'airConditionerFanMode',
'version': 1,
}),
dict({
'id': 'fanOscillationMode',
'version': 1,
}),
dict({
'id': 'airQualitySensor',
'version': 1,
}),
dict({
'id': 'temperatureMeasurement',
'version': 1,
}),
dict({
'id': 'thermostatCoolingSetpoint',
'version': 1,
}),
dict({
'id': 'relativeHumidityMeasurement',
'version': 1,
}),
dict({
'id': 'dustSensor',
'version': 1,
}),
dict({
'id': 'veryFineDustSensor',
'version': 1,
}),
dict({
'id': 'audioVolume',
'version': 1,
}),
dict({
'id': 'remoteControlStatus',
'version': 1,
}),
dict({
'id': 'powerConsumptionReport',
'version': 1,
}),
dict({
'id': 'demandResponseLoadControl',
'version': 1,
}),
dict({
'id': 'refresh',
'version': 1,
}),
dict({
'id': 'execute',
'version': 1,
}),
dict({
'id': 'custom.spiMode',
'version': 1,
}),
dict({
'id': 'custom.thermostatSetpointControl',
'version': 1,
}),
dict({
'id': 'custom.airConditionerOptionalMode',
'version': 1,
}),
dict({
'id': 'custom.airConditionerTropicalNightMode',
'version': 1,
}),
dict({
'id': 'custom.autoCleaningMode',
'version': 1,
}),
dict({
'id': 'custom.deviceReportStateConfiguration',
'version': 1,
}),
dict({
'id': 'custom.energyType',
'version': 1,
}),
dict({
'id': 'custom.dustFilter',
'version': 1,
}),
dict({
'id': 'custom.airConditionerOdorController',
'version': 1,
}),
dict({
'id': 'custom.deodorFilter',
'version': 1,
}),
dict({
'id': 'custom.disabledComponents',
'version': 1,
}),
dict({
'id': 'custom.disabledCapabilities',
'version': 1,
}),
dict({
'id': 'samsungce.deviceIdentification',
'version': 1,
}),
dict({
'id': 'samsungce.dongleSoftwareInstallation',
'version': 1,
}),
dict({
'id': 'samsungce.softwareUpdate',
'version': 1,
}),
dict({
'id': 'samsungce.selfCheck',
'version': 1,
}),
dict({
'id': 'samsungce.driverVersion',
'version': 1,
}),
]),
'categories': list([
dict({
'categoryType': 'manufacturer',
'name': 'AirConditioner',
}),
]),
'id': 'main',
'label': 'main',
}), }),
dict({ dict({
'id': 'switch', 'capabilities': list([
'version': 1, dict({
}), 'id': 'switch',
dict({ 'version': 1,
'id': 'airConditionerMode', }),
'version': 1, dict({
}), 'id': 'airConditionerMode',
dict({ 'version': 1,
'id': 'airConditionerFanMode', }),
'version': 1, dict({
}), 'id': 'airConditionerFanMode',
dict({ 'version': 1,
'id': 'fanOscillationMode', }),
'version': 1, dict({
}), 'id': 'fanOscillationMode',
dict({ 'version': 1,
'id': 'airQualitySensor', }),
'version': 1, dict({
}), 'id': 'temperatureMeasurement',
dict({ 'version': 1,
'id': 'temperatureMeasurement', }),
'version': 1, dict({
}), 'id': 'thermostatCoolingSetpoint',
dict({ 'version': 1,
'id': 'thermostatCoolingSetpoint', }),
'version': 1, dict({
}), 'id': 'relativeHumidityMeasurement',
dict({ 'version': 1,
'id': 'relativeHumidityMeasurement', }),
'version': 1, dict({
}), 'id': 'airQualitySensor',
dict({ 'version': 1,
'id': 'dustSensor', }),
'version': 1, dict({
}), 'id': 'dustSensor',
dict({ 'version': 1,
'id': 'veryFineDustSensor', }),
'version': 1, dict({
}), 'id': 'veryFineDustSensor',
dict({ 'version': 1,
'id': 'audioVolume', }),
'version': 1, dict({
}), 'id': 'odorSensor',
dict({ 'version': 1,
'id': 'remoteControlStatus', }),
'version': 1, dict({
}), 'id': 'remoteControlStatus',
dict({ 'version': 1,
'id': 'powerConsumptionReport', }),
'version': 1, dict({
}), 'id': 'audioVolume',
dict({ 'version': 1,
'id': 'demandResponseLoadControl', }),
'version': 1, dict({
}), 'id': 'custom.thermostatSetpointControl',
dict({ 'version': 1,
'id': 'refresh', }),
'version': 1, dict({
}), 'id': 'custom.autoCleaningMode',
dict({ 'version': 1,
'id': 'execute', }),
'version': 1, dict({
}), 'id': 'custom.airConditionerTropicalNightMode',
dict({ 'version': 1,
'id': 'custom.spiMode', }),
'version': 1, dict({
}), 'id': 'custom.disabledCapabilities',
dict({ 'version': 1,
'id': 'custom.thermostatSetpointControl', }),
'version': 1, dict({
}), 'id': 'ocf',
dict({ 'version': 1,
'id': 'custom.airConditionerOptionalMode', }),
'version': 1, dict({
}), 'id': 'powerConsumptionReport',
dict({ 'version': 1,
'id': 'custom.airConditionerTropicalNightMode', }),
'version': 1, dict({
}), 'id': 'demandResponseLoadControl',
dict({ 'version': 1,
'id': 'custom.autoCleaningMode', }),
'version': 1, dict({
}), 'id': 'custom.spiMode',
dict({ 'version': 1,
'id': 'custom.deviceReportStateConfiguration', }),
'version': 1, dict({
}), 'id': 'custom.airConditionerOptionalMode',
dict({ 'version': 1,
'id': 'custom.energyType', }),
'version': 1, dict({
}), 'id': 'custom.deviceReportStateConfiguration',
dict({ 'version': 1,
'id': 'custom.dustFilter', }),
'version': 1, dict({
}), 'id': 'custom.energyType',
dict({ 'version': 1,
'id': 'custom.airConditionerOdorController', }),
'version': 1, dict({
}), 'id': 'custom.dustFilter',
dict({ 'version': 1,
'id': 'custom.deodorFilter', }),
'version': 1, dict({
}), 'id': 'custom.airConditionerOdorController',
dict({ 'version': 1,
'id': 'custom.disabledComponents', }),
'version': 1, dict({
}), 'id': 'custom.deodorFilter',
dict({ 'version': 1,
'id': 'custom.disabledCapabilities', }),
'version': 1, ]),
}), 'categories': list([
dict({ dict({
'id': 'samsungce.deviceIdentification', 'categoryType': 'manufacturer',
'version': 1, 'name': 'Other',
}), }),
dict({ ]),
'id': 'samsungce.dongleSoftwareInstallation', 'id': '1',
'version': 1, 'label': '1',
}),
dict({
'id': 'samsungce.softwareUpdate',
'version': 1,
}),
dict({
'id': 'samsungce.selfCheck',
'version': 1,
}),
dict({
'id': 'samsungce.driverVersion',
'version': 1,
}), }),
]), ]),
'categories': list([ 'createTime': '2021-04-06T16:43:34.753Z',
dict({ 'deviceId': '96a5ef74-5832-a84b-f1f7-ca799957065d',
'categoryType': 'manufacturer', 'deviceManufacturerCode': 'Samsung Electronics',
'name': 'AirConditioner', 'deviceTypeName': 'Samsung OCF Air Conditioner',
}), 'executionContext': 'CLOUD',
]), 'label': 'AC Office Granit',
'id': 'main', 'locationId': '58d3fd7c-c512-4da3-b500-ef269382756c',
'label': 'main', 'manufacturerName': 'Samsung Electronics',
}), 'name': '[room a/c] Samsung',
dict({ 'ocf': dict({
'capabilities': list([ 'additionalAuthCodeRequired': False,
dict({ 'lastSignupTime': '2025-01-08T02:32:04.631093137Z',
'id': 'switch', 'manufacturerName': 'Samsung Electronics',
'version': 1, 'ocfDeviceType': 'x.com.st.d.sensor.light',
}), 'transferCandidate': False,
dict({ 'vendorId': 'VD-Sensor.Light-2023',
'id': 'airConditionerMode', }),
'version': 1, 'ownerId': 'f9a28d7c-1ed5-d9e9-a81c-18971ec081db',
}), 'presentationId': 'DA-AC-RAC-000001',
dict({ 'profile': dict({
'id': 'airConditionerFanMode', 'id': '60fbc713-8da5-315d-b31a-6d6dcde4be7b',
'version': 1, }),
}), 'restrictionTier': 0,
dict({ 'roomId': '85a79db4-9cf2-4f09-a5b2-cd70a5c0cef0',
'id': 'fanOscillationMode', 'type': 'OCF',
'version': 1,
}),
dict({
'id': 'temperatureMeasurement',
'version': 1,
}),
dict({
'id': 'thermostatCoolingSetpoint',
'version': 1,
}),
dict({
'id': 'relativeHumidityMeasurement',
'version': 1,
}),
dict({
'id': 'airQualitySensor',
'version': 1,
}),
dict({
'id': 'dustSensor',
'version': 1,
}),
dict({
'id': 'veryFineDustSensor',
'version': 1,
}),
dict({
'id': 'odorSensor',
'version': 1,
}),
dict({
'id': 'remoteControlStatus',
'version': 1,
}),
dict({
'id': 'audioVolume',
'version': 1,
}),
dict({
'id': 'custom.thermostatSetpointControl',
'version': 1,
}),
dict({
'id': 'custom.autoCleaningMode',
'version': 1,
}),
dict({
'id': 'custom.airConditionerTropicalNightMode',
'version': 1,
}),
dict({
'id': 'custom.disabledCapabilities',
'version': 1,
}),
dict({
'id': 'ocf',
'version': 1,
}),
dict({
'id': 'powerConsumptionReport',
'version': 1,
}),
dict({
'id': 'demandResponseLoadControl',
'version': 1,
}),
dict({
'id': 'custom.spiMode',
'version': 1,
}),
dict({
'id': 'custom.airConditionerOptionalMode',
'version': 1,
}),
dict({
'id': 'custom.deviceReportStateConfiguration',
'version': 1,
}),
dict({
'id': 'custom.energyType',
'version': 1,
}),
dict({
'id': 'custom.dustFilter',
'version': 1,
}),
dict({
'id': 'custom.airConditionerOdorController',
'version': 1,
}),
dict({
'id': 'custom.deodorFilter',
'version': 1,
}),
]),
'categories': list([
dict({
'categoryType': 'manufacturer',
'name': 'Other',
}),
]),
'id': '1',
'label': '1',
}), }),
]), ]),
'createTime': '2021-04-06T16:43:34.753Z',
'deviceId': '96a5ef74-5832-a84b-f1f7-ca799957065d',
'deviceManufacturerCode': 'Samsung Electronics',
'deviceTypeName': 'Samsung OCF Air Conditioner',
'executionContext': 'CLOUD',
'label': 'AC Office Granit',
'locationId': '58d3fd7c-c512-4da3-b500-ef269382756c',
'manufacturerName': 'Samsung Electronics',
'name': '[room a/c] Samsung',
'ocf': dict({
'additionalAuthCodeRequired': False,
'lastSignupTime': '2025-01-08T02:32:04.631093137Z',
'manufacturerName': 'Samsung Electronics',
'ocfDeviceType': 'x.com.st.d.sensor.light',
'transferCandidate': False,
'vendorId': 'VD-Sensor.Light-2023',
}),
'ownerId': 'f9a28d7c-1ed5-d9e9-a81c-18971ec081db',
'presentationId': 'DA-AC-RAC-000001',
'profile': dict({
'id': '60fbc713-8da5-315d-b31a-6d6dcde4be7b',
}),
'restrictionTier': 0,
'roomId': '85a79db4-9cf2-4f09-a5b2-cd70a5c0cef0',
'type': 'OCF',
}), }),
]), ]),
}) })

View File

@ -68,6 +68,39 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_devices[aux_ac]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'bf53a150-f8a4-45d1-aac4-86252475d551',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': None,
'model': None,
'model_id': None,
'name': 'AUX A/C on-off',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_devices[base_electric_meter] # name: test_devices[base_electric_meter]
DeviceRegistryEntrySnapshot({ DeviceRegistryEntrySnapshot({
'area_id': None, 'area_id': None,
@ -398,6 +431,39 @@
'via_device_id': None, 'via_device_id': None,
}) })
# --- # ---
# name: test_devices[da_ks_oven_01061]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': 'Realtek',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'9447959a-0dfa-6b27-d40d-650da525c53f',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'TP1X_DA-KS-OVEN-01061',
'model_id': None,
'name': 'Oven',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': 'AKS-WW-TP1X-21-OVEN_40211229',
'via_device_id': None,
})
# ---
# name: test_devices[da_ks_range_0101x] # name: test_devices[da_ks_range_0101x]
DeviceRegistryEntrySnapshot({ DeviceRegistryEntrySnapshot({
'area_id': None, 'area_id': None,

View File

@ -154,6 +154,58 @@
'state': 'unknown', 'state': 'unknown',
}) })
# --- # ---
# name: test_all_entities[aux_ac][sensor.aux_a_c_on_off_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.aux_a_c_on_off_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'bf53a150-f8a4-45d1-aac4-86252475d551.temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[aux_ac][sensor.aux_a_c_on_off_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'AUX A/C on-off Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.aux_a_c_on_off_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '20.0',
})
# ---
# name: test_all_entities[base_electric_meter][sensor.aeon_energy_monitor_energy-entry] # name: test_all_entities[base_electric_meter][sensor.aeon_energy_monitor_energy-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@ -2085,6 +2137,404 @@
'state': '-17', 'state': '-17',
}) })
# --- # ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_completion_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.oven_completion_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Completion time',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'completion_time',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.completionTime',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_completion_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'Oven Completion time',
}),
'context': <ANY>,
'entity_id': 'sensor.oven_completion_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-03-15T12:06:09+00:00',
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_job_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'cleaning',
'cooking',
'cooling',
'draining',
'preheat',
'ready',
'rinsing',
'finished',
'scheduled_start',
'warming',
'defrosting',
'sensing',
'searing',
'fast_preheat',
'scheduled_end',
'stone_heating',
'time_hold_preheat',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.oven_job_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Job state',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'oven_job_state',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.ovenJobState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_job_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Oven Job state',
'options': list([
'cleaning',
'cooking',
'cooling',
'draining',
'preheat',
'ready',
'rinsing',
'finished',
'scheduled_start',
'warming',
'defrosting',
'sensing',
'searing',
'fast_preheat',
'scheduled_end',
'stone_heating',
'time_hold_preheat',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.oven_job_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'preheat',
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_machine_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'ready',
'running',
'paused',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.oven_machine_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Machine state',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'oven_machine_state',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.machineState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_machine_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Oven Machine state',
'options': list([
'ready',
'running',
'paused',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.oven_machine_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'running',
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_oven_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'conventional',
'bake',
'bottom_heat',
'convection_bake',
'convection_roast',
'broil',
'convection_broil',
'steam_cook',
'steam_bake',
'steam_roast',
'steam_bottom_heat_plus_convection',
'microwave',
'microwave_plus_grill',
'microwave_plus_convection',
'microwave_plus_hot_blast',
'microwave_plus_hot_blast_2',
'slim_middle',
'slim_strong',
'slow_cook',
'proof',
'dehydrate',
'others',
'strong_steam',
'descale',
'rinse',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.oven_oven_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Oven mode',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'oven_mode',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.ovenMode',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_oven_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Oven Oven mode',
'options': list([
'conventional',
'bake',
'bottom_heat',
'convection_bake',
'convection_roast',
'broil',
'convection_broil',
'steam_cook',
'steam_bake',
'steam_roast',
'steam_bottom_heat_plus_convection',
'microwave',
'microwave_plus_grill',
'microwave_plus_convection',
'microwave_plus_hot_blast',
'microwave_plus_hot_blast_2',
'slim_middle',
'slim_strong',
'slow_cook',
'proof',
'dehydrate',
'others',
'strong_steam',
'descale',
'rinse',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.oven_oven_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'bake',
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_set_point-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.oven_set_point',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Set point',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'oven_setpoint',
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.ovenSetpoint',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_set_point-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Oven Set point',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.oven_set_point',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '220',
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.oven_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '9447959a-0dfa-6b27-d40d-650da525c53f.temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[da_ks_oven_01061][sensor.oven_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Oven Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.oven_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '30',
})
# ---
# name: test_all_entities[da_ks_range_0101x][sensor.vulcan_completion_time-entry] # name: test_all_entities[da_ks_range_0101x][sensor.vulcan_completion_time-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({

View File

@ -30,9 +30,9 @@ async def test_config_entry_diagnostics(
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test generating diagnostics for a device entry.""" """Test generating diagnostics for a device entry."""
mock_smartthings.get_raw_devices.return_value = load_json_object_fixture( mock_smartthings.get_raw_devices.return_value = [
"devices/da_ac_rac_000001.json", DOMAIN load_json_object_fixture("devices/da_ac_rac_000001.json", DOMAIN)
) ]
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
assert ( assert (
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)

View File

@ -27,6 +27,7 @@
"title": "1984", "title": "1984",
"parent_id": "FV:2", "parent_id": "FV:2",
"item_id": "FV:2/8", "item_id": "FV:2/8",
"album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.2%2fmusic%2fiTunes%2520Music%2fAerosmith%2f1984&v=742",
"resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"A:ALBUMARTIST/Aerosmith/1984\" parentID=\"A:ALBUMARTIST/Aerosmith\" restricted=\"true\"><dc:title>1984</dc:title><upnp:class>object.container.album.musicAlbum</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>", "resource_meta_data": "<DIDL-Lite xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:upnp=\"urn:schemas-upnp-org:metadata-1-0/upnp/\" xmlns:r=\"urn:schemas-rinconnetworks-com:metadata-1-0/\" xmlns=\"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/\"><item id=\"A:ALBUMARTIST/Aerosmith/1984\" parentID=\"A:ALBUMARTIST/Aerosmith\" restricted=\"true\"><dc:title>1984</dc:title><upnp:class>object.container.album.musicAlbum</upnp:class><desc id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>",
"resources": [ "resources": [
{ {

View File

@ -44,6 +44,31 @@
'title': 'Favorites', 'title': 'Favorites',
}) })
# --- # ---
# name: test_browse_media_favorites[object.container.album.musicAlbum-favorites_folder]
dict({
'can_expand': True,
'can_play': False,
'children': list([
dict({
'can_expand': False,
'can_play': True,
'children_media_class': None,
'media_class': 'album',
'media_content_id': 'FV:2/8',
'media_content_type': 'favorite_item_id',
'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.2/music/iTunes%20Music/Aerosmith/1984&v=742',
'title': '1984',
}),
]),
'children_media_class': 'album',
'media_class': 'directory',
'media_content_id': '',
'media_content_type': 'favorites',
'not_shown': 0,
'thumbnail': None,
'title': 'Albums',
})
# ---
# name: test_browse_media_favorites[object.item.audioItem.audioBook-favorites_folder] # name: test_browse_media_favorites[object.item.audioItem.audioBook-favorites_folder]
dict({ dict({
'can_expand': True, 'can_expand': True,

View File

@ -190,6 +190,10 @@ async def test_browse_media_library_albums(
"object.item.audioItem.audioBook", "object.item.audioItem.audioBook",
"favorites_folder", "favorites_folder",
), ),
(
"object.container.album.musicAlbum",
"favorites_folder",
),
], ],
) )
async def test_browse_media_favorites( async def test_browse_media_favorites(

View File

@ -59,43 +59,30 @@ def mock_controller_connection_failed():
@pytest.mark.usefixtures("controller") @pytest.mark.usefixtures("controller")
async def test_user_network_succes(hass: HomeAssistant) -> None: @pytest.mark.parametrize(
"""Test user network config.""" ("inputParams", "expected"),
# inttial menu show [
result = await hass.config_entries.flow.async_init( (
DOMAIN, context={"source": SOURCE_USER} {
) CONF_TLS: True,
assert result CONF_PASSWORD: "password",
assert result.get("flow_id") },
assert result.get("type") is FlowResultType.MENU "tls://password@velbus:6000",
assert result.get("step_id") == "user" ),
assert result.get("menu_options") == ["network", "usbselect"] (
# select the network option {
result = await hass.config_entries.flow.async_configure( CONF_TLS: True,
result.get("flow_id"), CONF_PASSWORD: "",
{"next_step_id": "network"}, },
) "tls://velbus:6000",
assert result.get("type") is FlowResultType.FORM ),
# fill in the network form ({CONF_TLS: True}, "tls://velbus:6000"),
result = await hass.config_entries.flow.async_configure( ({CONF_TLS: False}, "velbus:6000"),
result.get("flow_id"), ],
{ )
CONF_TLS: False, async def test_user_network_succes(
CONF_HOST: "velbus", hass: HomeAssistant, inputParams: str, expected: str
CONF_PORT: 6000, ) -> None:
CONF_PASSWORD: "",
},
)
assert result
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == "Velbus Network"
data = result.get("data")
assert data
assert data[CONF_PORT] == "velbus:6000"
@pytest.mark.usefixtures("controller")
async def test_user_network_succes_tls(hass: HomeAssistant) -> None:
"""Test user network config.""" """Test user network config."""
# inttial menu show # inttial menu show
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -116,10 +103,9 @@ async def test_user_network_succes_tls(hass: HomeAssistant) -> None:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result.get("flow_id"), result.get("flow_id"),
{ {
CONF_TLS: True,
CONF_HOST: "velbus", CONF_HOST: "velbus",
CONF_PORT: 6000, CONF_PORT: 6000,
CONF_PASSWORD: "password", **inputParams,
}, },
) )
assert result assert result
@ -127,7 +113,7 @@ async def test_user_network_succes_tls(hass: HomeAssistant) -> None:
assert result.get("title") == "Velbus Network" assert result.get("title") == "Velbus Network"
data = result.get("data") data = result.get("data")
assert data assert data
assert data[CONF_PORT] == "tls://password@velbus:6000" assert data[CONF_PORT] == expected
@pytest.mark.usefixtures("controller") @pytest.mark.usefixtures("controller")

View File

@ -179,7 +179,16 @@
}), }),
'0x0010': dict({ '0x0010': dict({
'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=<class 'zigpy.types.named.EUI64'>, zcl_type=<DataTypeId.EUI64: 240>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=True, is_manufacturer_specific=False)", 'attribute': "ZCLAttributeDef(id=0x0010, name='cie_addr', type=<class 'zigpy.types.named.EUI64'>, zcl_type=<DataTypeId.EUI64: 240>, access=<ZCLAttributeAccess.Read|Write: 3>, mandatory=True, is_manufacturer_specific=False)",
'value': None, 'value': list([
50,
79,
50,
2,
0,
141,
21,
0,
]),
}), }),
'0x0011': dict({ '0x0011': dict({
'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=<class 'zigpy.types.basic.uint8_t'>, zcl_type=<DataTypeId.uint8: 32>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)", 'attribute': "ZCLAttributeDef(id=0x0011, name='zone_id', type=<class 'zigpy.types.basic.uint8_t'>, zcl_type=<DataTypeId.uint8: 32>, access=<ZCLAttributeAccess.Read: 1>, mandatory=True, is_manufacturer_specific=False)",