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]:
"""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:
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[
CONF_HOST
].startswith("serial://"):

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import mimetypes
from pathlib import Path
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():
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)

View File

@ -629,14 +629,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
home_connect_client = HomeConnectClient(config_entry_auth)
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
await coordinator.async_config_entry_first_refresh()
await coordinator.async_setup()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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

View File

@ -137,41 +137,6 @@ def setup_home_connect_entry(
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.runtime_data.async_add_special_listener(
partial(

View File

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

View File

@ -2,7 +2,7 @@
from __future__ import annotations
import asyncio
from asyncio import sleep as asyncio_sleep
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass
@ -29,6 +29,7 @@ from aiohomeconnect.model.error import (
HomeConnectApiError,
HomeConnectError,
HomeConnectRequestError,
TooManyRequestsError,
UnauthorizedError,
)
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.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.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
_LOGGER = logging.getLogger(__name__)
@ -154,7 +155,7 @@ class HomeConnectCoordinator(
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."""
retry_time = 10
while True:
@ -269,7 +270,7 @@ class HomeConnectCoordinator(
type(error).__name__,
retry_time,
)
await asyncio.sleep(retry_time)
await asyncio_sleep(retry_time)
retry_time = min(retry_time * 2, 3600)
except HomeConnectApiError as error:
_LOGGER.error("Error while listening for events: %s", error)
@ -278,6 +279,13 @@ class HomeConnectCoordinator(
)
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
def _call_event_listener(self, event_message: EventMessage) -> None:
"""Call listener for event."""
@ -295,6 +303,42 @@ class HomeConnectCoordinator(
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
"""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:
appliances = await self.client.get_home_appliances()
except UnauthorizedError as error:
@ -312,12 +356,38 @@ class HomeConnectCoordinator(
translation_placeholders=get_dict_from_home_connect_error(error),
) from error
return {
appliance.ha_id: await self._get_appliance_data(
appliance, self.data.get(appliance.ha_id)
for appliance in appliances.homeappliances:
self.device_registry.async_get_or_create(
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(
self,
@ -339,6 +409,8 @@ class HomeConnectCoordinator(
await self.client.get_settings(appliance.ha_id)
).settings
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching settings for %s: %s",
@ -353,6 +425,8 @@ class HomeConnectCoordinator(
status.key: status
for status in (await self.client.get_status(appliance.ha_id)).status
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching status for %s: %s",
@ -369,6 +443,8 @@ class HomeConnectCoordinator(
if appliance.type in APPLIANCES_WITH_PROGRAMS:
try:
all_programs = await self.client.get_all_programs(appliance.ha_id)
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching programs for %s: %s",
@ -427,6 +503,8 @@ class HomeConnectCoordinator(
await self.client.get_available_commands(appliance.ha_id)
).commands
}
except TooManyRequestsError:
raise
except HomeConnectError:
commands = set()
@ -461,6 +539,8 @@ class HomeConnectCoordinator(
).options
or []
}
except TooManyRequestsError:
raise
except HomeConnectError as error:
_LOGGER.debug(
"Error fetching options for %s: %s",

View File

@ -1,21 +1,28 @@
"""Home Connect entity base class."""
from abc import abstractmethod
from collections.abc import Callable, Coroutine
import contextlib
from datetime import datetime
import logging
from typing import cast
from typing import Any, Concatenate, cast
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.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.event import async_call_later
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 .utils import get_dict_from_home_connect_error
@ -127,3 +134,34 @@ class HomeConnectOptionEntity(HomeConnectEntity):
def bsh_key(self) -> OptionKey:
"""Return the BSH 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,
)
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
_LOGGER = logging.getLogger(__name__)
@ -189,19 +189,25 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
},
) from err
@constraint_fetcher
async def async_fetch_constraints(self) -> None:
"""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(
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key)
self.appliance.info.ha_id, setting_key=setting_key
)
except HomeConnectError as err:
_LOGGER.error("An error occurred: %s", err)
else:
if data.unit:
self._attr_native_unit_of_measurement = data.unit
self.set_constraints(data)
def set_constraints(self, setting: GetSetting) -> None:
"""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):
return
if constraints.max:
@ -222,10 +228,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
"""When entity is added to hass."""
await super().async_added_to_hass()
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
self._attr_native_unit_of_measurement = data.unit
self.set_constraints(data)
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_step")
):
@ -253,7 +259,6 @@ class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
or candidate_unit != self._attr_native_unit_of_measurement
):
self._attr_native_unit_of_measurement = candidate_unit
self.__dict__.pop("unit_of_measurement", None)
option_constraints = option_definition.constraints
if option_constraints:
if (

View File

@ -1,8 +1,8 @@
"""Provides a select platform for Home Connect."""
from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass
import logging
from typing import Any, cast
from aiohomeconnect.client import Client as HomeConnectClient
@ -47,9 +47,11 @@ from .coordinator import (
HomeConnectConfigEntry,
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
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
@ -413,6 +415,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
"""Select setting class for Home Connect."""
entity_description: HomeConnectSelectEntityDescription
_original_option_keys: set[str | None]
def __init__(
self,
@ -421,6 +424,7 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription,
) -> None:
"""Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key)
super().__init__(
coordinator,
appliance,
@ -458,23 +462,29 @@ class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
async def async_added_to_hass(self) -> None:
"""When entity is 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))
if (
not setting
or not setting.constraints
or not setting.constraints.allowed_values
):
with contextlib.suppress(HomeConnectError):
setting = await self.coordinator.client.get_setting(
self.appliance.info.ha_id,
setting_key=cast(SettingKey, self.bsh_key),
)
setting = await self.coordinator.client.get_setting(
self.appliance.info.ha_id,
setting_key=cast(SettingKey, self.bsh_key),
)
if setting and setting.constraints and setting.constraints.allowed_values:
self._original_option_keys = set(setting.constraints.allowed_values)
self._attr_options = [
self.entity_description.values_translation_key[option]
for option in setting.constraints.allowed_values
if option in self.entity_description.values_translation_key
for option in self._original_option_keys
if option is not None
and option in self.entity_description.values_translation_key
]
@ -491,7 +501,7 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
desc: HomeConnectSelectEntityDescription,
) -> None:
"""Initialize the entity."""
self._original_option_keys = set(desc.values_translation_key.keys())
self._original_option_keys = set(desc.values_translation_key)
super().__init__(
coordinator,
appliance,
@ -524,5 +534,5 @@ class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
self.entity_description.values_translation_key[option]
for option in self._original_option_keys
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."""
import contextlib
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import cast
from aiohomeconnect.model import EventKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -28,7 +27,9 @@ from .const import (
UNIT_MAP,
)
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
from .entity import HomeConnectEntity
from .entity import HomeConnectEntity, constraint_fetcher
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@ -335,16 +336,14 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
else:
await self.fetch_unit()
@constraint_fetcher
async def fetch_unit(self) -> None:
"""Fetch the unit of measurement."""
with contextlib.suppress(HomeConnectError):
data = await self.coordinator.client.get_status_value(
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
)
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(
data.unit, data.unit
)
data = await self.coordinator.client.get_status_value(
self.appliance.info.ha_id, status_key=cast(StatusKey, self.bsh_key)
)
if data.unit:
self._attr_native_unit_of_measurement = UNIT_MAP.get(data.unit, data.unit)
class HomeConnectProgramSensor(HomeConnectSensor):

View File

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

View File

@ -417,11 +417,11 @@
"venting_level": {
"options": {
"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_stage02": "Fan stage 2",
"cooking_hood_enum_type_stage_fan_stage03": "Fan stage 3",
"cooking_hood_enum_type_stage_fan_stage04": "Fan stage 4",
"cooking_hood_enum_type_stage_fan_stage05": "Fan stage 5"
"cooking_hood_enum_type_stage_fan_stage_01": "Fan stage 1",
"cooking_hood_enum_type_stage_fan_stage_02": "Fan stage 2",
"cooking_hood_enum_type_stage_fan_stage_03": "Fan stage 3",
"cooking_hood_enum_type_stage_fan_stage_04": "Fan stage 4",
"cooking_hood_enum_type_stage_fan_stage_05": "Fan stage 5"
}
},
"intensive_level": {
@ -441,14 +441,14 @@
"washer_temperature": {
"options": {
"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_c30": "30ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c40": "40ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c50": "50ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c60": "60ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c70": "70ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c80": "80ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c90": "90ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_20": "20ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_30": "30ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_40": "40ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_50": "50ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_60": "60ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_70": "70ºC clothes",
"laundry_care_washer_enum_type_temperature_g_c_80": "80º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_warm": "Warm",
"laundry_care_washer_enum_type_temperature_ul_hot": "Hot",
@ -458,14 +458,15 @@
"spin_speed": {
"options": {
"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_m600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "1200 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "1400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "1600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_800": "800 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_900": "900 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1000": "1000 rpm",
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 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_low": "Low",
"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%]",
"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_stage01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage01%]",
"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_stage03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage03%]",
"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_stage05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage05%]"
"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_stage_02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_02%]",
"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_stage_04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_04%]",
"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": {
@ -1410,14 +1411,14 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]",
"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_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_c30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c30%]",
"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_c50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c50%]",
"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_c70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c70%]",
"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_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_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_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_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_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_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_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_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_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_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%]",
@ -1428,14 +1429,15 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
"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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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%]",

View File

@ -61,6 +61,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
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
local_client: LaMarzoccoLocalClient | None = 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(
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, local_client),
LaMarzoccoFirmwareUpdateCoordinator(hass, entry, device),
firmware_coordinator,
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
)
# API does not like concurrent requests, so no asyncio.gather here
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()
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)
async def update_listener(

View File

@ -37,5 +37,5 @@
"iot_class": "cloud_polling",
"loggers": ["pylamarzocco"],
"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(
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
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
!= MachineModel.GS3_MP,
),
@ -162,9 +165,12 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_prebrew_time(
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
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
!= MachineModel.GS3_MP,
),
@ -180,8 +186,8 @@ KEY_ENTITIES: tuple[LaMarzoccoKeyNumberEntityDescription, ...] = (
set_value_fn=lambda machine, value, key: machine.set_preinfusion_time(
preinfusion_time=value, key=key
),
native_value_fn=lambda config, key: config.prebrew_configuration[
key
native_value_fn=lambda config, key: config.prebrew_configuration[key][
1
].preinfusion_time,
available_fn=lambda device: len(device.config.prebrew_configuration) > 0
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 = {
"disabled": PrebrewMode.DISABLED,
"prebrew": PrebrewMode.PREBREW,
"prebrew_enabled": PrebrewMode.PREBREW_ENABLED,
"preinfusion": PrebrewMode.PREINFUSION,
}

View File

@ -148,6 +148,7 @@
"state": {
"disabled": "Disabled",
"prebrew": "Prebrew",
"prebrew_enabled": "Prebrew",
"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)
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:
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
listener()

View File

@ -83,7 +83,16 @@ class PlaybackProxyView(HomeAssistantView):
_LOGGER.warning("Reolink playback proxy error: %s", str(err))
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):
_LOGGER.debug(
"Requested Playback Proxy Method %s, Headers: %s",
request.method,
headers,
)
_LOGGER.debug(
"Opening VOD stream from %s: %s",
host.api.camera_name(ch),
@ -93,6 +102,7 @@ class PlaybackProxyView(HomeAssistantView):
try:
reolink_response = await self.session.get(
reolink_url,
headers=headers,
timeout=ClientTimeout(
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}"
_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)
response = web.StreamResponse(
status=200,
reason="OK",
headers={
"Content-Type": "video/mp4",
},
response_headers = dict(reolink_response.headers)
_LOGGER.debug(
"Response Playback Proxy Status %s:%s, Headers: %s",
reolink_response.status,
reolink_response.reason,
response_headers,
)
response_headers["Content-Type"] = "video/mp4"
if reolink_response.content_length is not None:
response.content_length = reolink_response.content_length
response = web.StreamResponse(
status=reolink_response.status,
reason=reolink_response.reason,
headers=response_headers,
)
await response.prepare(request)
@ -141,7 +158,8 @@ class PlaybackProxyView(HomeAssistantView):
"Timeout while reading Reolink playback from %s, writing EOF",
host.api.nvr_name,
)
finally:
reolink_response.release()
reolink_response.release()
await response.write_eof()
return response

View File

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

View File

@ -4,7 +4,7 @@ import logging
from typing import Any
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 (
SmartMeterTexasAPIError,
SmartMeterTexasAuthError,
@ -16,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import aiohttp_client
from homeassistant.util.ssl import get_default_context
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.
"""
client_ssl_context = ClientSSLContext()
ssl_context = await client_ssl_context.get_ssl_context()
ssl_context = get_default_context()
client_session = aiohttp_client.async_get_clientsession(hass)
account = Account(data["username"], data["password"])
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],
)
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
subscription_id = subscription.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]:
"""Determine the supported HVAC modes."""
modes = [HVACMode.OFF]
modes.extend(
state
for mode in self.get_attribute_value(
if (
ac_modes := self.get_attribute_value(
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
)
if (state := AC_MODE_TO_STATE.get(mode)) is not None
if state not in modes
)
) is not None:
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

View File

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

View File

@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"iot_class": "cloud_push",
"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
options_attribute: Attribute | None = None
exists_fn: Callable[[Status], bool] | None = None
use_temperature_unit: bool = False
CAPABILITY_TO_SENSORS: dict[
@ -573,8 +574,9 @@ CAPABILITY_TO_SENSORS: dict[
key=Attribute.OVEN_SETPOINT,
translation_key="oven_setpoint",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
value_fn=lambda value: value if value != 0 else None,
use_temperature_unit=True,
# 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,
) -> None:
"""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._attribute = attribute
self.capability = capability
@ -1033,7 +1038,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
@property
def native_unit_of_measurement(self) -> str | None:
"""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 (
UNITS.get(unit, unit)
if unit

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["snoo"],
"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()
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""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
# Each speaker can return a different polled update_id

View File

@ -165,6 +165,8 @@ async def async_browse_media(
favorites_folder_payload,
speaker.favorites,
media_content_id,
media,
get_browse_image_url,
)
payload = {
@ -443,7 +445,10 @@ def favorites_payload(favorites: SonosFavorites) -> BrowseMedia:
def favorites_folder_payload(
favorites: SonosFavorites, media_content_id: str
favorites: SonosFavorites,
media_content_id: str,
media: SonosMedia,
get_browse_image_url: GetBrowseImageUrlType,
) -> BrowseMedia:
"""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",
can_play=True,
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",
"iot_class": "local_push",
"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://"
else:
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_HOST]}:{user_input[CONF_PORT]}"
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(
key=CHARGER_MAX_ICP_CURRENT_KEY,
translation_key="maximum_icp_current",
max_value_fn=lambda coordinator: cast(
float, coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]
),
max_value_fn=lambda _: 255,
min_value_fn=lambda _: 6,
set_value_fn=lambda coordinator: coordinator.async_set_icp_current,
native_step=1,

View File

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

View File

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

View File

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

10
requirements_all.txt generated
View File

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

View File

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

View File

@ -1,6 +1,6 @@
"""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
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.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(
"google_generative_ai_conversation",

View File

@ -2,13 +2,10 @@
from typing import Any
from aiohomeconnect.model import ArrayOfHomeAppliances, ArrayOfStatus
from aiohomeconnect.model import ArrayOfStatus
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_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
MOCK_STATUS = ArrayOfStatus.from_dict(

View File

@ -11,6 +11,7 @@ from aiohomeconnect.client import Client as HomeConnectClient
from aiohomeconnect.model import (
ArrayOfCommands,
ArrayOfEvents,
ArrayOfHomeAppliances,
ArrayOfOptions,
ArrayOfPrograms,
ArrayOfSettings,
@ -39,15 +40,9 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from . import (
MOCK_APPLIANCES,
MOCK_AVAILABLE_COMMANDS,
MOCK_PROGRAMS,
MOCK_SETTINGS,
MOCK_STATUS,
)
from . import 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_SECRET = "5678"
@ -148,14 +143,6 @@ async def mock_integration_setup(
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(
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
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")
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."""
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]:
"""Mock stream_all_events."""
while True:
for event in await event_queue.get():
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(
side_effect=_get_specific_appliance_side_effect
)
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(
side_effect=_get_set_program_side_effect(
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")
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."""
mock = MagicMock(
autospec=HomeConnectClient,
@ -449,7 +445,8 @@ def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
for event in await event_queue.get():
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.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")
def mock_appliance_ha_id(request: pytest.FixtureRequest) -> str:
"""Fixture to mock Appliance."""
app = "Washer"
def mock_appliance_ha_id(
appliances: list[HomeAppliance], request: pytest.FixtureRequest
) -> str:
"""Fixture to get the ha_id of an appliance."""
appliance_type = "Washer"
if hasattr(request, "param") and request.param:
app = request.param
for appliance in MOCK_APPLIANCES.homeappliances:
if appliance.type == app:
appliance_type = request.param
for appliance in appliances:
if appliance.type == appliance_type:
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": [
{
"name": "FridgeFreezer",
"brand": "SIEMENS",
"vib": "HCS05FRF1",
"connected": true,
"type": "FridgeFreezer",
"enumber": "HCS05FRF1/03",
"haId": "SIEMENS-HCS05FRF1-304F4F9E541D"
},
{
"name": "Dishwasher",
"brand": "SIEMENS",
"vib": "HCS02DWH1",
"connected": true,
"type": "Dishwasher",
"enumber": "HCS02DWH1/03",
"haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1"
},
{
"name": "Oven",
"brand": "BOSCH",
"vib": "HCS01OVN1",
"connected": true,
"type": "Oven",
"enumber": "HCS01OVN1/03",
"haId": "BOSCH-HCS01OVN1-43E0065FE245"
},
{
"name": "Washer",
"brand": "SIEMENS",
"vib": "HCS03WCH1",
"connected": true,
"type": "Washer",
"enumber": "HCS03WCH1/03",
"haId": "SIEMENS-HCS03WCH1-7BC6383CF794"
},
{
"name": "Dryer",
"brand": "BOSCH",
"vib": "HCS04DYR1",
"connected": true,
"type": "Dryer",
"enumber": "HCS04DYR1/03",
"haId": "BOSCH-HCS04DYR1-831694AE3C5A"
},
{
"name": "CoffeeMaker",
"brand": "BOSCH",
"vib": "HCS06COM1",
"connected": true,
"type": "CoffeeMaker",
"enumber": "HCS06COM1/03",
"haId": "BOSCH-HCS06COM1-D70390681C2C"
},
{
"name": "WasherDryer",
"brand": "BOSCH",
"vib": "HCS000001",
"connected": true,
"type": "WasherDryer",
"enumber": "HCS000000/01",
"haId": "BOSCH-HCS000000-D00000000001"
},
{
"name": "Refrigerator",
"brand": "BOSCH",
"vib": "HCS000002",
"connected": true,
"type": "Refrigerator",
"enumber": "HCS000000/02",
"haId": "BOSCH-HCS000000-D00000000002"
},
{
"name": "Freezer",
"brand": "BOSCH",
"vib": "HCS000003",
"connected": true,
"type": "Freezer",
"enumber": "HCS000000/03",
"haId": "BOSCH-HCS000000-D00000000003"
},
{
"name": "Hood",
"brand": "BOSCH",
"vib": "HCS000004",
"connected": true,
"type": "Hood",
"enumber": "HCS000000/04",
"haId": "BOSCH-HCS000000-D00000000004"
},
{
"name": "Hob",
"brand": "BOSCH",
"vib": "HCS000005",
"connected": true,
"type": "Hob",
"enumber": "HCS000000/05",
"haId": "BOSCH-HCS000000-D00000000005"
},
{
"name": "CookProcessor",
"brand": "BOSCH",
"vib": "HCS000006",
"connected": true,
"type": "CookProcessor",
"enumber": "HCS000000/06",
"haId": "BOSCH-HCS000000-D00000000006"
},
{
"name": "DNE",
"brand": "BOSCH",
"vib": "HCS000000",
"connected": true,
"type": "DNE",
"enumber": "HCS000000/00",
"haId": "BOSCH-000000000-000000000000"
}
]
}
"homeappliances": [
{
"name": "FridgeFreezer",
"brand": "SIEMENS",
"vib": "HCS05FRF1",
"connected": true,
"type": "FridgeFreezer",
"enumber": "HCS05FRF1/03",
"haId": "SIEMENS-HCS05FRF1-304F4F9E541D"
},
{
"name": "Dishwasher",
"brand": "SIEMENS",
"vib": "HCS02DWH1",
"connected": true,
"type": "Dishwasher",
"enumber": "HCS02DWH1/03",
"haId": "SIEMENS-HCS02DWH1-6BE58C26DCC1"
},
{
"name": "Oven",
"brand": "BOSCH",
"vib": "HCS01OVN1",
"connected": true,
"type": "Oven",
"enumber": "HCS01OVN1/03",
"haId": "BOSCH-HCS01OVN1-43E0065FE245"
},
{
"name": "Washer",
"brand": "SIEMENS",
"vib": "HCS03WCH1",
"connected": true,
"type": "Washer",
"enumber": "HCS03WCH1/03",
"haId": "SIEMENS-HCS03WCH1-7BC6383CF794"
},
{
"name": "Dryer",
"brand": "BOSCH",
"vib": "HCS04DYR1",
"connected": true,
"type": "Dryer",
"enumber": "HCS04DYR1/03",
"haId": "BOSCH-HCS04DYR1-831694AE3C5A"
},
{
"name": "CoffeeMaker",
"brand": "BOSCH",
"vib": "HCS06COM1",
"connected": true,
"type": "CoffeeMaker",
"enumber": "HCS06COM1/03",
"haId": "BOSCH-HCS06COM1-D70390681C2C"
},
{
"name": "WasherDryer",
"brand": "BOSCH",
"vib": "HCS000001",
"connected": true,
"type": "WasherDryer",
"enumber": "HCS000000/01",
"haId": "BOSCH-HCS000000-D00000000001"
},
{
"name": "Refrigerator",
"brand": "BOSCH",
"vib": "HCS000002",
"connected": true,
"type": "Refrigerator",
"enumber": "HCS000000/02",
"haId": "BOSCH-HCS000000-D00000000002"
},
{
"name": "Freezer",
"brand": "BOSCH",
"vib": "HCS000003",
"connected": true,
"type": "Freezer",
"enumber": "HCS000000/03",
"haId": "BOSCH-HCS000000-D00000000003"
},
{
"name": "Hood",
"brand": "BOSCH",
"vib": "HCS000004",
"connected": true,
"type": "Hood",
"enumber": "HCS000000/04",
"haId": "BOSCH-HCS000000-D00000000004"
},
{
"name": "Hob",
"brand": "BOSCH",
"vib": "HCS000005",
"connected": true,
"type": "Hob",
"enumber": "HCS000000/05",
"haId": "BOSCH-HCS000000-D00000000005"
},
{
"name": "CookProcessor",
"brand": "BOSCH",
"vib": "HCS000006",
"connected": true,
"type": "CookProcessor",
"enumber": "HCS000000/06",
"haId": "BOSCH-HCS000000-D00000000006"
},
{
"name": "DNE",
"brand": "BOSCH",
"vib": "HCS000000",
"connected": true,
"type": "DNE",
"enumber": "HCS000000/00",
"haId": "BOSCH-000000000-000000000000"
}
]
}

View File

@ -1,19 +1,20 @@
"""Test for Home Connect coordinator."""
from collections.abc import Awaitable, Callable
import copy
from datetime import timedelta
from typing import Any
from typing import Any, cast
from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.model import (
ArrayOfEvents,
ArrayOfHomeAppliances,
ArrayOfSettings,
ArrayOfStatus,
Event,
EventKey,
EventMessage,
EventType,
HomeAppliance,
)
from aiohomeconnect.model.error import (
EventStreamInterruptedError,
@ -28,6 +29,7 @@ from homeassistant.components.home_connect.const import (
BSH_DOOR_STATE_OPEN,
BSH_EVENT_PRESENT_STATE_PRESENT,
BSH_POWER_OFF,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntries, ConfigEntryState
from homeassistant.const import EVENT_STATE_REPORTED, Platform
@ -37,12 +39,10 @@ from homeassistant.core import (
HomeAssistant,
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.util import dt as dt_util
from . import MOCK_APPLIANCES
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.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(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
client: MagicMock,
freezer: FrozenDateTimeFactory,
appliance_ha_id: str,
appliance: HomeAppliance,
) -> None:
"""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_2 = "binary_sensor.washer_remote_start"
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.
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.
freezer.tick(timedelta(hours=1))
@ -166,11 +173,13 @@ async def test_coordinator_failure_refresh_and_stream(
# Now make the entity available again.
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.
event_message = EventMessage(
appliance_ha_id,
appliance.ha_id,
EventType.STATUS,
ArrayOfEvents(
[
@ -399,6 +408,9 @@ async def test_event_listener_error(
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(
"exception",
[HomeConnectRequestError(), EventStreamInterruptedError()],
@ -429,11 +441,10 @@ async def test_event_listener_resilience(
after_event_expected_state: str,
exception: HomeConnectError,
hass: HomeAssistant,
appliance: HomeAppliance,
client: MagicMock,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
appliance_ha_id: str,
) -> None:
"""Test that the event listener is resilient to interruptions."""
future = hass.loop.create_future()
@ -467,7 +478,7 @@ async def test_event_listener_resilience(
await client.add_events(
[
EventMessage(
appliance_ha_id,
appliance.ha_id,
EventType.STATUS,
ArrayOfEvents(
[
@ -489,3 +500,44 @@ async def test_event_listener_resilience(
state = hass.states.get(entity_id)
assert 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 http import HTTPStatus
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.model import OptionKey, ProgramKey, SettingKey, StatusKey
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
from aiohomeconnect.model.error import (
HomeConnectError,
TooManyRequestsError,
UnauthorizedError,
)
import aiohttp
import pytest
from syrupy.assertion import SnapshotAssertion
@ -355,6 +359,48 @@ async def test_client_error(
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(
"service_call",
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,

View File

@ -2,7 +2,7 @@
from collections.abc import Awaitable, Callable
import random
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
from aiohomeconnect.model import (
ArrayOfEvents,
@ -22,6 +22,7 @@ from aiohomeconnect.model.error import (
HomeConnectApiError,
HomeConnectError,
SelectedProgramNotSetError,
TooManyRequestsError,
)
from aiohomeconnect.model.program import (
ProgramDefinitionConstraints,
@ -47,7 +48,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
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
@ -340,6 +341,98 @@ async def test_number_entity_functionality(
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(
("entity_id", "setting_key", "mock_attr"),
[

View File

@ -21,6 +21,7 @@ from aiohomeconnect.model.error import (
ActiveProgramNotSetError,
HomeConnectError,
SelectedProgramNotSetError,
TooManyRequestsError,
)
from aiohomeconnect.model.program import (
EnumerateProgram,
@ -50,7 +51,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
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
@ -521,9 +522,18 @@ async def test_select_functionality(
(
"select.hood_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)},
),
(
"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(
@ -566,6 +576,139 @@ async def test_fetch_allowed_values(
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(
("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",
},
),
(
"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(

View File

@ -13,7 +13,7 @@ from aiohomeconnect.model import (
Status,
StatusKey,
)
from aiohomeconnect.model.error import HomeConnectApiError
from aiohomeconnect.model.error import HomeConnectApiError, TooManyRequestsError
from freezegun.api import FrozenDateTimeFactory
import pytest
@ -26,12 +26,13 @@ from homeassistant.components.home_connect.const import (
BSH_EVENT_PRESENT_STATE_PRESENT,
DOMAIN,
)
from homeassistant.components.home_connect.coordinator import HomeConnectError
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
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"
@ -724,3 +725,122 @@ async def test_sensor_unit_fetching(
)
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",
"Group1": [
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseA",
"preWetTime": 0.5,
"preWetHoldTime": 1
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseA",
"preWetTime": 0,
"preWetHoldTime": 4
},
{
"mode": "TypeA",
"groupNumber": "Group1",
"doseType": "DoseB",
"preWetTime": 0.5,
"preWetHoldTime": 1
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseC",
"preWetTime": 3.2999999523162842,
"preWetHoldTime": 3.2999999523162842
"doseType": "DoseB",
"preWetTime": 0,
"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",
"doseType": "DoseD",
"preWetTime": 2,
"preWetHoldTime": 2
},
{
"mode": "TypeB",
"groupNumber": "Group1",
"doseType": "DoseD",
"preWetTime": 0,
"preWetHoldTime": 4
}
]
},

View File

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

View File

@ -27,22 +27,46 @@
}),
'plumbed_in': True,
'prebrew_configuration': dict({
'1': dict({
'off_time': 1,
'on_time': 0.5,
}),
'2': dict({
'off_time': 1,
'on_time': 0.5,
}),
'3': dict({
'off_time': 3.299999952316284,
'on_time': 3.299999952316284,
}),
'4': dict({
'off_time': 2,
'on_time': 2,
}),
'1': list([
dict({
'off_time': 1,
'on_time': 0.5,
}),
dict({
'off_time': 4,
'on_time': 0,
}),
]),
'2': list([
dict({
'off_time': 1,
'on_time': 0.5,
}),
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',
'scale': None,

View File

@ -419,7 +419,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -438,7 +438,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -457,7 +457,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -473,10 +473,10 @@
'last_changed': <ANY>,
'last_reported': <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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -495,7 +495,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -514,7 +514,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -533,7 +533,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -549,10 +549,10 @@
'last_changed': <ANY>,
'last_reported': <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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -587,7 +587,7 @@
'last_changed': <ANY>,
'last_reported': <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]
@ -606,7 +606,7 @@
'last_changed': <ANY>,
'last_reported': <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]
@ -625,7 +625,7 @@
'last_changed': <ANY>,
'last_reported': <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]
@ -644,10 +644,10 @@
'last_changed': <ANY>,
'last_reported': <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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -666,7 +666,7 @@
'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({
'aliases': set({
}),
@ -705,7 +705,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -724,7 +724,7 @@
'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({
'aliases': set({
}),
@ -763,7 +763,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -782,7 +782,7 @@
'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({
'aliases': set({
}),
@ -821,7 +821,7 @@
'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({
'attributes': ReadOnlyDict({
'device_class': 'duration',
@ -840,7 +840,7 @@
'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({
'aliases': set({
}),
@ -953,7 +953,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
'state': '4',
})
# ---
# 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",
return_value=[service_info],
) 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)
discovery.assert_called_once()
init_device.assert_called_once()
_, kwargs = init_device.call_args
assert mock_machine_class.call_count == 2
_, kwargs = mock_machine_class.call_args
assert kwargs["bluetooth_client"] is not None
assert mock_config_entry.data[CONF_NAME] == service_info.name
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
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(
hass: HomeAssistant,
mock_lamarzocco: MagicMock,

View File

@ -46,8 +46,12 @@ def get_mock_session(
mock_response = Mock()
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.iter_chunked = Mock(return_value=content)
mock_response.text = AsyncMock(return_value="test")
mock_session = Mock()
mock_session.get = AsyncMock(return_value=mock_response)
@ -178,16 +182,18 @@ async def test_playback_proxy_timeout(
assert response.status == 200
@pytest.mark.parametrize(("content_type"), [("video/x-flv"), ("text/html")])
async def test_playback_wrong_content(
hass: HomeAssistant,
reolink_connect: MagicMock,
config_entry: MockConfigEntry,
hass_client: ClientSessionGenerator,
content_type: str,
) -> None:
"""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)
mock_session = get_mock_session(content_type="video/x-flv")
mock_session = get_mock_session(content_type=content_type)
with patch(
"homeassistant.components.reolink.views.async_get_clientsession",

View File

@ -110,6 +110,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"da_rvc_normal_000001",
"da_ks_microwave_0101x",
"da_ks_range_0101x",
"da_ks_oven_01061",
"hue_color_temperature_bulb",
"hue_rgbw_color_bulb",
"c2c_shade",
@ -131,6 +132,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"abl_light_b_001",
"tplink_p110",
"ikea_kadrilj",
"aux_ac",
]
)
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
# 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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -1,307 +1,311 @@
# serializer version: 1
# name: test_config_entry_diagnostics[da_ac_rac_000001]
dict({
'_links': dict({
}),
'items': list([
'devices': list([
dict({
'allowed': list([
]),
'components': list([
'_links': dict({
}),
'items': list([
dict({
'capabilities': list([
'allowed': list([
]),
'components': list([
dict({
'id': 'ocf',
'version': 1,
'capabilities': list([
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({
'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,
'capabilities': list([
dict({
'id': 'switch',
'version': 1,
}),
dict({
'id': 'airConditionerMode',
'version': 1,
}),
dict({
'id': 'airConditionerFanMode',
'version': 1,
}),
dict({
'id': 'fanOscillationMode',
'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',
}),
]),
'categories': list([
dict({
'categoryType': 'manufacturer',
'name': 'AirConditioner',
}),
]),
'id': 'main',
'label': 'main',
}),
dict({
'capabilities': list([
dict({
'id': 'switch',
'version': 1,
}),
dict({
'id': 'airConditionerMode',
'version': 1,
}),
dict({
'id': 'airConditionerFanMode',
'version': 1,
}),
dict({
'id': 'fanOscillationMode',
'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',
}),
]),
'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,
})
# ---
# 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]
DeviceRegistryEntrySnapshot({
'area_id': None,
@ -398,6 +431,39 @@
'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]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@ -154,6 +154,58 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -2085,6 +2137,404 @@
'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]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

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

View File

@ -27,6 +27,7 @@
"title": "1984",
"parent_id": "FV:2",
"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>",
"resources": [
{

View File

@ -44,6 +44,31 @@
'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]
dict({
'can_expand': True,

View File

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

View File

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

View File

@ -179,7 +179,16 @@
}),
'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)",
'value': None,
'value': list([
50,
79,
50,
2,
0,
141,
21,
0,
]),
}),
'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)",