Migrate from homeconnect dependency to aiohomeconnect (#136116)
* Migrate from homeconnect dependency to aiohomeconnect * Reload the integration if there is an API error on event stream * fix typos at coordinator tests * Setup config entry at coordinator tests * fix ruff * Bump aiohomeconnect to version 0.11.4 * Fix set program options * Use context based updates at coordinator * Improved how `context_callbacks` cache is invalidated * fix * fixes and improvements at coordinator Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove stale Entity inheritance * Small improvement for light subscriptions * Remove non-needed function It had its purpose before some refactoring before the firs commit, no is no needed as is only used at HomeConnectEntity constructor * Static methods and variables at conftest * Refresh the data after an event stream interruption * Cleaned debug logs * Fetch programs at coordinator * Improvements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Simplify obtaining power settings from coordinator data Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove unnecessary statement * use `is UNDEFINED` instead of `isinstance` * Request power setting only when it is strictly necessary * Bump aiohomeconnect to 0.12.1 * use raw keys for diagnostics * Use keyword arguments where needed * Remove unnecessary statements Co-authored-by: Martin Hjelmare <marhje52@gmail.com> --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/136949/head
parent
4e3e1e91b7
commit
b637129208
|
@ -2,17 +2,16 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, cast
|
||||
|
||||
from requests import HTTPError
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import CommandKey, Option, OptionKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_DEVICE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
|
@ -21,16 +20,13 @@ from homeassistant.helpers import (
|
|||
)
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from . import api
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import (
|
||||
ATTR_KEY,
|
||||
ATTR_PROGRAM,
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
BSH_PAUSE,
|
||||
BSH_RESUME,
|
||||
DOMAIN,
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
|
||||
SERVICE_OPTION_ACTIVE,
|
||||
|
@ -44,15 +40,11 @@ from .const import (
|
|||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
|
||||
type HomeConnectConfigEntry = ConfigEntry[api.ConfigEntryAuth]
|
||||
from .coordinator import HomeConnectConfigEntry, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RE_CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])|(?=\d)(?<=\D)")
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
SERVICE_SETTING_SCHEMA = vol.Schema(
|
||||
|
@ -99,17 +91,24 @@ PLATFORMS = [
|
|||
]
|
||||
|
||||
|
||||
def _get_appliance(
|
||||
hass: HomeAssistant,
|
||||
device_id: str | None = None,
|
||||
device_entry: dr.DeviceEntry | None = None,
|
||||
entry: HomeConnectConfigEntry | None = None,
|
||||
) -> api.HomeConnectAppliance:
|
||||
"""Return a Home Connect appliance instance given a device id or a device entry."""
|
||||
if device_id is not None and device_entry is None:
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
assert device_entry, "Either a device id or a device entry must be provided"
|
||||
async def _get_client_and_ha_id(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> tuple[HomeConnectClient, str]:
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
if device_entry is None:
|
||||
raise ServiceValidationError("Device entry not found for device id")
|
||||
entry: HomeConnectConfigEntry | None = None
|
||||
for entry_id in device_entry.config_entries:
|
||||
_entry = hass.config_entries.async_get_entry(entry_id)
|
||||
assert _entry
|
||||
if _entry.domain == DOMAIN:
|
||||
entry = cast(HomeConnectConfigEntry, _entry)
|
||||
break
|
||||
if entry is None:
|
||||
raise ServiceValidationError(
|
||||
"Home Connect config entry not found for that device id"
|
||||
)
|
||||
|
||||
ha_id = next(
|
||||
(
|
||||
|
@ -119,158 +118,148 @@ def _get_appliance(
|
|||
),
|
||||
None,
|
||||
)
|
||||
assert ha_id
|
||||
|
||||
def find_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
) -> api.HomeConnectAppliance | None:
|
||||
for device in entry.runtime_data.devices:
|
||||
appliance = device.appliance
|
||||
if appliance.haId == ha_id:
|
||||
return appliance
|
||||
return None
|
||||
|
||||
if entry is None:
|
||||
for entry_id in device_entry.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
assert entry
|
||||
if entry.domain == DOMAIN:
|
||||
entry = cast(HomeConnectConfigEntry, entry)
|
||||
if (appliance := find_appliance(entry)) is not None:
|
||||
return appliance
|
||||
elif (appliance := find_appliance(entry)) is not None:
|
||||
return appliance
|
||||
raise ValueError(f"Appliance for device id {device_entry.id} not found")
|
||||
|
||||
|
||||
def _get_appliance_or_raise_service_validation_error(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> api.HomeConnectAppliance:
|
||||
"""Return a Home Connect appliance instance or raise a service validation error."""
|
||||
try:
|
||||
return _get_appliance(hass, device_id)
|
||||
except (ValueError, AssertionError) as err:
|
||||
if ha_id is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="appliance_not_found",
|
||||
translation_placeholders={
|
||||
"device_id": device_id,
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
async def _run_appliance_service[*_Ts](
|
||||
hass: HomeAssistant,
|
||||
appliance: api.HomeConnectAppliance,
|
||||
method: str,
|
||||
*args: *_Ts,
|
||||
error_translation_key: str,
|
||||
error_translation_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
try:
|
||||
await hass.async_add_executor_job(getattr(appliance, method), *args)
|
||||
except api.HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=error_translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
**error_translation_placeholders,
|
||||
},
|
||||
) from err
|
||||
)
|
||||
return entry.runtime_data.client, ha_id
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Home Connect component."""
|
||||
|
||||
async def _async_service_program(call, method):
|
||||
async def _async_service_program(call: ServiceCall, start: bool):
|
||||
"""Execute calls to services taking a program."""
|
||||
program = call.data[ATTR_PROGRAM]
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
|
||||
options = []
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
option_key = call.data.get(ATTR_KEY)
|
||||
if option_key is not None:
|
||||
option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]}
|
||||
|
||||
option_unit = call.data.get(ATTR_UNIT)
|
||||
if option_unit is not None:
|
||||
option[ATTR_UNIT] = option_unit
|
||||
|
||||
options.append(option)
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
_get_appliance_or_raise_service_validation_error(hass, device_id),
|
||||
method,
|
||||
program,
|
||||
options,
|
||||
error_translation_key=method,
|
||||
error_translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program,
|
||||
},
|
||||
options = (
|
||||
[
|
||||
Option(
|
||||
OptionKey(option_key),
|
||||
call.data[ATTR_VALUE],
|
||||
unit=call.data.get(ATTR_UNIT),
|
||||
)
|
||||
]
|
||||
if option_key is not None
|
||||
else None
|
||||
)
|
||||
|
||||
async def _async_service_command(call, command):
|
||||
"""Execute calls to services executing a command."""
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
try:
|
||||
if start:
|
||||
await client.start_program(ha_id, program_key=program, options=options)
|
||||
else:
|
||||
await client.set_selected_program(
|
||||
ha_id, program_key=program, options=options
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="start_program" if start else "select_program",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program,
|
||||
},
|
||||
) from err
|
||||
|
||||
appliance = _get_appliance_or_raise_service_validation_error(hass, device_id)
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
appliance,
|
||||
"execute_command",
|
||||
command,
|
||||
error_translation_key="execute_command",
|
||||
error_translation_placeholders={"command": command},
|
||||
)
|
||||
|
||||
async def _async_service_key_value(call, method):
|
||||
"""Execute calls to services taking a key and value."""
|
||||
key = call.data[ATTR_KEY]
|
||||
async def _async_service_set_program_options(call: ServiceCall, active: bool):
|
||||
"""Execute calls to services taking a program."""
|
||||
option_key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
unit = call.data.get(ATTR_UNIT)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
await _run_appliance_service(
|
||||
hass,
|
||||
_get_appliance_or_raise_service_validation_error(hass, device_id),
|
||||
method,
|
||||
*((key, value) if unit is None else (key, value, unit)),
|
||||
error_translation_key=method,
|
||||
error_translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
)
|
||||
try:
|
||||
if active:
|
||||
await client.set_active_program_option(
|
||||
ha_id,
|
||||
option_key=OptionKey(option_key),
|
||||
value=value,
|
||||
unit=unit,
|
||||
)
|
||||
else:
|
||||
await client.set_selected_program_option(
|
||||
ha_id,
|
||||
option_key=OptionKey(option_key),
|
||||
value=value,
|
||||
unit=unit,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_options_active_program"
|
||||
if active
|
||||
else "set_options_selected_program",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: option_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_option_active(call):
|
||||
async def _async_service_command(call: ServiceCall, command_key: CommandKey):
|
||||
"""Execute calls to services executing a command."""
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
try:
|
||||
await client.put_command(ha_id, command_key=command_key, value=True)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="execute_command",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"command": command_key.value,
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_option_active(call: ServiceCall):
|
||||
"""Service for setting an option for an active program."""
|
||||
await _async_service_key_value(call, "set_options_active_program")
|
||||
await _async_service_set_program_options(call, True)
|
||||
|
||||
async def async_service_option_selected(call):
|
||||
async def async_service_option_selected(call: ServiceCall):
|
||||
"""Service for setting an option for a selected program."""
|
||||
await _async_service_key_value(call, "set_options_selected_program")
|
||||
await _async_service_set_program_options(call, False)
|
||||
|
||||
async def async_service_setting(call):
|
||||
async def async_service_setting(call: ServiceCall):
|
||||
"""Service for changing a setting."""
|
||||
await _async_service_key_value(call, "set_setting")
|
||||
key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
async def async_service_pause_program(call):
|
||||
try:
|
||||
await client.set_setting(ha_id, setting_key=key, value=value)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_setting",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: str(value),
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_pause_program(call: ServiceCall):
|
||||
"""Service for pausing a program."""
|
||||
await _async_service_command(call, BSH_PAUSE)
|
||||
await _async_service_command(call, CommandKey.BSH_COMMON_PAUSE_PROGRAM)
|
||||
|
||||
async def async_service_resume_program(call):
|
||||
async def async_service_resume_program(call: ServiceCall):
|
||||
"""Service for resuming a paused program."""
|
||||
await _async_service_command(call, BSH_RESUME)
|
||||
await _async_service_command(call, CommandKey.BSH_COMMON_RESUME_PROGRAM)
|
||||
|
||||
async def async_service_select_program(call):
|
||||
async def async_service_select_program(call: ServiceCall):
|
||||
"""Service for selecting a program."""
|
||||
await _async_service_program(call, "select_program")
|
||||
await _async_service_program(call, False)
|
||||
|
||||
async def async_service_start_program(call):
|
||||
async def async_service_start_program(call: ServiceCall):
|
||||
"""Service for starting a program."""
|
||||
await _async_service_program(call, "start_program")
|
||||
await _async_service_program(call, True)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
|
@ -323,12 +312,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry)
|
|||
)
|
||||
)
|
||||
|
||||
entry.runtime_data = api.ConfigEntryAuth(hass, entry, implementation)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
await update_all_devices(hass, entry)
|
||||
config_entry_auth = AsyncConfigEntryAuth(hass, session)
|
||||
|
||||
home_connect_client = HomeConnectClient(config_entry_auth)
|
||||
|
||||
coordinator = HomeConnectCoordinator(hass, entry, home_connect_client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.runtime_data.start_event_listener()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -339,21 +337,6 @@ async def async_unload_entry(
|
|||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
async def update_all_devices(
|
||||
hass: HomeAssistant, entry: HomeConnectConfigEntry
|
||||
) -> None:
|
||||
"""Update all the devices."""
|
||||
hc_api = entry.runtime_data
|
||||
|
||||
try:
|
||||
await hass.async_add_executor_job(hc_api.get_devices)
|
||||
for device in hc_api.devices:
|
||||
await hass.async_add_executor_job(device.initialize)
|
||||
except HTTPError as err:
|
||||
_LOGGER.warning("Cannot update devices: %s", err.response.status_code)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, entry: HomeConnectConfigEntry
|
||||
) -> bool:
|
||||
|
@ -382,25 +365,3 @@ async def async_migrate_entry(
|
|||
|
||||
_LOGGER.debug("Migration to version %s successful", entry.version)
|
||||
return True
|
||||
|
||||
|
||||
def get_dict_from_home_connect_error(err: api.HomeConnectError) -> dict[str, Any]:
|
||||
"""Return a dict from a Home Connect error."""
|
||||
return {
|
||||
"description": cast(dict[str, Any], err.args[0]).get("description", "?")
|
||||
if len(err.args) > 0 and isinstance(err.args[0], dict)
|
||||
else err.args[0]
|
||||
if len(err.args) > 0 and isinstance(err.args[0], str)
|
||||
else "?",
|
||||
}
|
||||
|
||||
|
||||
def bsh_key_to_translation_key(bsh_key: str) -> str:
|
||||
"""Convert a BSH key to a translation key format.
|
||||
|
||||
This function takes a BSH key, such as `Dishcare.Dishwasher.Program.Eco50`,
|
||||
and converts it to a translation key format, such as `dishcare_dishwasher_bsh_key_eco50`.
|
||||
"""
|
||||
return "_".join(
|
||||
RE_CAMEL_CASE.sub("_", split) for split in bsh_key.split(".")
|
||||
).lower()
|
||||
|
|
|
@ -1,85 +1,28 @@
|
|||
"""API for Home Connect bound to HASS OAuth."""
|
||||
|
||||
from asyncio import run_coroutine_threadsafe
|
||||
import logging
|
||||
from aiohomeconnect.client import AbstractAuth
|
||||
from aiohomeconnect.const import API_ENDPOINT
|
||||
|
||||
import homeconnect
|
||||
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import ATTR_KEY, ATTR_VALUE, BSH_ACTIVE_PROGRAM, SIGNAL_UPDATE_ENTITIES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
|
||||
class ConfigEntryAuth(homeconnect.HomeConnectAPI):
|
||||
class AsyncConfigEntryAuth(AbstractAuth):
|
||||
"""Provide Home Connect authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation,
|
||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize Home Connect Auth."""
|
||||
self.hass = hass
|
||||
self.config_entry = config_entry
|
||||
self.session = config_entry_oauth2_flow.OAuth2Session(
|
||||
hass, config_entry, implementation
|
||||
)
|
||||
super().__init__(self.session.token)
|
||||
self.devices: list[HomeConnectDevice] = []
|
||||
super().__init__(get_async_client(hass), host=API_ENDPOINT)
|
||||
self.session = oauth_session
|
||||
|
||||
def refresh_tokens(self) -> dict:
|
||||
"""Refresh and return new Home Connect tokens using Home Assistant OAuth2 session."""
|
||||
run_coroutine_threadsafe(
|
||||
self.session.async_ensure_token_valid(), self.hass.loop
|
||||
).result()
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
await self.session.async_ensure_token_valid()
|
||||
|
||||
return self.session.token
|
||||
|
||||
def get_devices(self) -> list[HomeConnectAppliance]:
|
||||
"""Get a dictionary of devices."""
|
||||
appl: list[HomeConnectAppliance] = self.get_appliances()
|
||||
self.devices = [HomeConnectDevice(self.hass, app) for app in appl]
|
||||
return self.devices
|
||||
|
||||
|
||||
class HomeConnectDevice:
|
||||
"""Generic Home Connect device."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, appliance: HomeConnectAppliance) -> None:
|
||||
"""Initialize the device class."""
|
||||
self.hass = hass
|
||||
self.appliance = appliance
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""Fetch the info needed to initialize the device."""
|
||||
try:
|
||||
self.appliance.get_status()
|
||||
except (HomeConnectError, ValueError):
|
||||
_LOGGER.debug("Unable to fetch appliance status. Probably offline")
|
||||
try:
|
||||
self.appliance.get_settings()
|
||||
except (HomeConnectError, ValueError):
|
||||
_LOGGER.debug("Unable to fetch settings. Probably offline")
|
||||
try:
|
||||
program_active = self.appliance.get_programs_active()
|
||||
except (HomeConnectError, ValueError):
|
||||
_LOGGER.debug("Unable to fetch active programs. Probably offline")
|
||||
program_active = None
|
||||
if program_active and ATTR_KEY in program_active:
|
||||
self.appliance.status[BSH_ACTIVE_PROGRAM] = {
|
||||
ATTR_VALUE: program_active[ATTR_KEY]
|
||||
}
|
||||
self.appliance.listen_events(callback=self.event_callback)
|
||||
|
||||
def event_callback(self, appliance: HomeConnectAppliance) -> None:
|
||||
"""Handle event."""
|
||||
_LOGGER.debug("Update triggered on %s", appliance.name)
|
||||
_LOGGER.debug(self.appliance.status)
|
||||
dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId)
|
||||
return self.session.token["access_token"]
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
"""Application credentials platform for Home Connect."""
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Provides a binary sensor for Home Connect."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import StatusKey
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.binary_sensor import (
|
||||
|
@ -19,26 +21,21 @@ from homeassistant.helpers.issue_registry import (
|
|||
async_delete_issue,
|
||||
)
|
||||
|
||||
from . import HomeConnectConfigEntry
|
||||
from .api import HomeConnectDevice
|
||||
from .const import (
|
||||
ATTR_VALUE,
|
||||
BSH_DOOR_STATE,
|
||||
BSH_DOOR_STATE_CLOSED,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
BSH_REMOTE_CONTROL_ACTIVATION_STATE,
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
DOMAIN,
|
||||
REFRIGERATION_STATUS_DOOR_CHILLER,
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED,
|
||||
REFRIGERATION_STATUS_DOOR_FREEZER,
|
||||
REFRIGERATION_STATUS_DOOR_OPEN,
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REFRIGERATION_DOOR_BOOLEAN_MAP = {
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED: False,
|
||||
REFRIGERATION_STATUS_DOOR_OPEN: True,
|
||||
|
@ -54,19 +51,19 @@ class HomeConnectBinarySensorEntityDescription(BinarySensorEntityDescription):
|
|||
|
||||
BINARY_SENSORS = (
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=BSH_REMOTE_CONTROL_ACTIVATION_STATE,
|
||||
key=StatusKey.BSH_COMMON_REMOTE_CONTROL_ACTIVE,
|
||||
translation_key="remote_control",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
key=StatusKey.BSH_COMMON_REMOTE_CONTROL_START_ALLOWED,
|
||||
translation_key="remote_start",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="BSH.Common.Status.LocalControlActive",
|
||||
key=StatusKey.BSH_COMMON_LOCAL_CONTROL_ACTIVE,
|
||||
translation_key="local_control",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="BSH.Common.Status.BatteryChargingState",
|
||||
key=StatusKey.BSH_COMMON_BATTERY_CHARGING_STATE,
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
boolean_map={
|
||||
"BSH.Common.EnumType.BatteryChargingState.Charging": True,
|
||||
|
@ -75,7 +72,7 @@ BINARY_SENSORS = (
|
|||
translation_key="battery_charging_state",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="BSH.Common.Status.ChargingConnection",
|
||||
key=StatusKey.BSH_COMMON_CHARGING_CONNECTION,
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
boolean_map={
|
||||
"BSH.Common.EnumType.ChargingConnection.Connected": True,
|
||||
|
@ -84,31 +81,31 @@ BINARY_SENSORS = (
|
|||
translation_key="charging_connection",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="ConsumerProducts.CleaningRobot.Status.DustBoxInserted",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_DUST_BOX_INSERTED,
|
||||
translation_key="dust_box_inserted",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="ConsumerProducts.CleaningRobot.Status.Lifted",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_LIFTED,
|
||||
translation_key="lifted",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key="ConsumerProducts.CleaningRobot.Status.Lost",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_LOST,
|
||||
translation_key="lost",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=REFRIGERATION_STATUS_DOOR_CHILLER,
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_CHILLER_COMMON,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="chiller_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=REFRIGERATION_STATUS_DOOR_FREEZER,
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_FREEZER,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="freezer_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_REFRIGERATOR,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="refrigerator_door",
|
||||
|
@ -123,19 +120,17 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect binary sensor."""
|
||||
|
||||
def get_entities() -> list[BinarySensorEntity]:
|
||||
entities: list[BinarySensorEntity] = []
|
||||
for device in entry.runtime_data.devices:
|
||||
entities.extend(
|
||||
HomeConnectBinarySensor(device, description)
|
||||
for description in BINARY_SENSORS
|
||||
if description.key in device.appliance.status
|
||||
)
|
||||
if BSH_DOOR_STATE in device.appliance.status:
|
||||
entities.append(HomeConnectDoorBinarySensor(device))
|
||||
return entities
|
||||
entities: list[BinarySensorEntity] = []
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities.extend(
|
||||
HomeConnectBinarySensor(entry.runtime_data, appliance, description)
|
||||
for description in BINARY_SENSORS
|
||||
if description.key in appliance.status
|
||||
)
|
||||
if StatusKey.BSH_COMMON_DOOR_STATE in appliance.status:
|
||||
entities.append(HomeConnectDoorBinarySensor(entry.runtime_data, appliance))
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
|
||||
|
@ -143,25 +138,15 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
|
|||
|
||||
entity_description: HomeConnectBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return true if the binary sensor is available."""
|
||||
return self._attr_is_on is not None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the binary sensor's status."""
|
||||
if not self.device.appliance.status or not (
|
||||
status := self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
|
||||
):
|
||||
self._attr_is_on = None
|
||||
return
|
||||
if self.entity_description.boolean_map:
|
||||
self._attr_is_on = self.entity_description.boolean_map.get(status)
|
||||
elif status not in [True, False]:
|
||||
self._attr_is_on = None
|
||||
else:
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the native value of the binary sensor."""
|
||||
status = self.appliance.status[cast(StatusKey, self.bsh_key)].value
|
||||
if isinstance(status, bool):
|
||||
self._attr_is_on = status
|
||||
_LOGGER.debug("Updated, new state: %s", self._attr_is_on)
|
||||
elif self.entity_description.boolean_map:
|
||||
self._attr_is_on = self.entity_description.boolean_map.get(status)
|
||||
else:
|
||||
self._attr_is_on = None
|
||||
|
||||
|
||||
class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
|
@ -171,13 +156,15 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
device: HomeConnectDevice,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
device,
|
||||
coordinator,
|
||||
appliance,
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=BSH_DOOR_STATE,
|
||||
key=StatusKey.BSH_COMMON_DOOR_STATE,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
boolean_map={
|
||||
BSH_DOOR_STATE_CLOSED: False,
|
||||
|
@ -186,8 +173,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
|||
},
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{device.appliance.haId}-Door"
|
||||
self._attr_name = f"{device.appliance.name} Door"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
|
||||
self._attr_name = f"{appliance.info.name} Door"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
|
@ -234,6 +221,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
|||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""Constants for the Home Connect integration."""
|
||||
|
||||
from aiohomeconnect.model import EventKey, SettingKey, StatusKey
|
||||
|
||||
DOMAIN = "home_connect"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize"
|
||||
OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token"
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"CleaningRobot",
|
||||
|
@ -17,93 +17,35 @@ APPLIANCES_WITH_PROGRAMS = (
|
|||
"WasherDryer",
|
||||
)
|
||||
|
||||
BSH_POWER_STATE = "BSH.Common.Setting.PowerState"
|
||||
|
||||
BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On"
|
||||
BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"
|
||||
BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"
|
||||
BSH_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram"
|
||||
BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"
|
||||
BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive"
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed"
|
||||
BSH_CHILD_LOCK_STATE = "BSH.Common.Setting.ChildLock"
|
||||
|
||||
BSH_REMAINING_PROGRAM_TIME = "BSH.Common.Option.RemainingProgramTime"
|
||||
BSH_COMMON_OPTION_DURATION = "BSH.Common.Option.Duration"
|
||||
BSH_COMMON_OPTION_PROGRAM_PROGRESS = "BSH.Common.Option.ProgramProgress"
|
||||
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT = "BSH.Common.EnumType.EventPresentState.Present"
|
||||
BSH_EVENT_PRESENT_STATE_CONFIRMED = "BSH.Common.EnumType.EventPresentState.Confirmed"
|
||||
BSH_EVENT_PRESENT_STATE_OFF = "BSH.Common.EnumType.EventPresentState.Off"
|
||||
|
||||
BSH_OPERATION_STATE = "BSH.Common.Status.OperationState"
|
||||
|
||||
BSH_OPERATION_STATE_RUN = "BSH.Common.EnumType.OperationState.Run"
|
||||
BSH_OPERATION_STATE_PAUSE = "BSH.Common.EnumType.OperationState.Pause"
|
||||
BSH_OPERATION_STATE_FINISHED = "BSH.Common.EnumType.OperationState.Finished"
|
||||
|
||||
COOKING_LIGHTING = "Cooking.Common.Setting.Lighting"
|
||||
COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness"
|
||||
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY = (
|
||||
"ConsumerProducts.CoffeeMaker.Event.BeanContainerEmpty"
|
||||
)
|
||||
COFFEE_EVENT_WATER_TANK_EMPTY = "ConsumerProducts.CoffeeMaker.Event.WaterTankEmpty"
|
||||
COFFEE_EVENT_DRIP_TRAY_FULL = "ConsumerProducts.CoffeeMaker.Event.DripTrayFull"
|
||||
|
||||
DISHWASHER_EVENT_SALT_NEARLY_EMPTY = "Dishcare.Dishwasher.Event.SaltNearlyEmpty"
|
||||
DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY = (
|
||||
"Dishcare.Dishwasher.Event.RinseAidNearlyEmpty"
|
||||
)
|
||||
|
||||
REFRIGERATION_INTERNAL_LIGHT_POWER = "Refrigeration.Common.Setting.Light.Internal.Power"
|
||||
REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS = (
|
||||
"Refrigeration.Common.Setting.Light.Internal.Brightness"
|
||||
)
|
||||
REFRIGERATION_EXTERNAL_LIGHT_POWER = "Refrigeration.Common.Setting.Light.External.Power"
|
||||
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS = (
|
||||
"Refrigeration.Common.Setting.Light.External.Brightness"
|
||||
)
|
||||
|
||||
REFRIGERATION_SUPERMODEFREEZER = "Refrigeration.FridgeFreezer.Setting.SuperModeFreezer"
|
||||
REFRIGERATION_SUPERMODEREFRIGERATOR = (
|
||||
"Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator"
|
||||
)
|
||||
REFRIGERATION_DISPENSER = "Refrigeration.Common.Setting.Dispenser.Enabled"
|
||||
|
||||
REFRIGERATION_STATUS_DOOR_CHILLER = "Refrigeration.Common.Status.Door.ChillerCommon"
|
||||
REFRIGERATION_STATUS_DOOR_FREEZER = "Refrigeration.Common.Status.Door.Freezer"
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR = "Refrigeration.Common.Status.Door.Refrigerator"
|
||||
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED = "Refrigeration.Common.EnumType.Door.States.Closed"
|
||||
REFRIGERATION_STATUS_DOOR_OPEN = "Refrigeration.Common.EnumType.Door.States.Open"
|
||||
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR = (
|
||||
"Refrigeration.FridgeFreezer.Event.DoorAlarmRefrigerator"
|
||||
)
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER = (
|
||||
"Refrigeration.FridgeFreezer.Event.DoorAlarmFreezer"
|
||||
)
|
||||
REFRIGERATION_EVENT_TEMP_ALARM_FREEZER = (
|
||||
"Refrigeration.FridgeFreezer.Event.TemperatureAlarmFreezer"
|
||||
)
|
||||
|
||||
|
||||
BSH_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled"
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness"
|
||||
BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor"
|
||||
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = (
|
||||
"BSH.Common.EnumType.AmbientLightColor.CustomColor"
|
||||
)
|
||||
BSH_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor"
|
||||
|
||||
BSH_DOOR_STATE = "BSH.Common.Status.DoorState"
|
||||
|
||||
BSH_DOOR_STATE_CLOSED = "BSH.Common.EnumType.DoorState.Closed"
|
||||
BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked"
|
||||
BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open"
|
||||
|
||||
BSH_PAUSE = "BSH.Common.Command.PauseProgram"
|
||||
BSH_RESUME = "BSH.Common.Command.ResumeProgram"
|
||||
|
||||
SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities"
|
||||
|
||||
SERVICE_OPTION_ACTIVE = "set_option_active"
|
||||
SERVICE_OPTION_SELECTED = "set_option_selected"
|
||||
|
@ -113,51 +55,44 @@ SERVICE_SELECT_PROGRAM = "select_program"
|
|||
SERVICE_SETTING = "change_setting"
|
||||
SERVICE_START_PROGRAM = "start_program"
|
||||
|
||||
ATTR_ALLOWED_VALUES = "allowedvalues"
|
||||
ATTR_AMBIENT = "ambient"
|
||||
ATTR_BSH_KEY = "bsh_key"
|
||||
ATTR_CONSTRAINTS = "constraints"
|
||||
ATTR_DESC = "desc"
|
||||
ATTR_DEVICE = "device"
|
||||
|
||||
ATTR_KEY = "key"
|
||||
ATTR_PROGRAM = "program"
|
||||
ATTR_SENSOR_TYPE = "sensor_type"
|
||||
ATTR_SIGN = "sign"
|
||||
ATTR_STEPSIZE = "stepsize"
|
||||
ATTR_UNIT = "unit"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
|
||||
|
||||
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID = "entity_id"
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM = "program"
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY = "key"
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
|
||||
|
||||
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
|
||||
"ChildLock": BSH_CHILD_LOCK_STATE,
|
||||
"Operation State": BSH_OPERATION_STATE,
|
||||
"Light": COOKING_LIGHTING,
|
||||
"AmbientLight": BSH_AMBIENT_LIGHT_ENABLED,
|
||||
"Power": BSH_POWER_STATE,
|
||||
"Remaining Program Time": BSH_REMAINING_PROGRAM_TIME,
|
||||
"Duration": BSH_COMMON_OPTION_DURATION,
|
||||
"Program Progress": BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
"Remote Control": BSH_REMOTE_CONTROL_ACTIVATION_STATE,
|
||||
"Remote Start": BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
"Supermode Freezer": REFRIGERATION_SUPERMODEFREEZER,
|
||||
"Supermode Refrigerator": REFRIGERATION_SUPERMODEREFRIGERATOR,
|
||||
"Dispenser Enabled": REFRIGERATION_DISPENSER,
|
||||
"Internal Light": REFRIGERATION_INTERNAL_LIGHT_POWER,
|
||||
"External Light": REFRIGERATION_EXTERNAL_LIGHT_POWER,
|
||||
"Chiller Door": REFRIGERATION_STATUS_DOOR_CHILLER,
|
||||
"Freezer Door": REFRIGERATION_STATUS_DOOR_FREEZER,
|
||||
"Refrigerator Door": REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
"Door Alarm Freezer": REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
"Door Alarm Refrigerator": REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
"Temperature Alarm Freezer": REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
|
||||
"Bean Container Empty": COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
"Water Tank Empty": COFFEE_EVENT_WATER_TANK_EMPTY,
|
||||
"Drip Tray Full": COFFEE_EVENT_DRIP_TRAY_FULL,
|
||||
"ChildLock": SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
"Operation State": StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
"Light": SettingKey.COOKING_COMMON_LIGHTING,
|
||||
"AmbientLight": SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED,
|
||||
"Power": SettingKey.BSH_COMMON_POWER_STATE,
|
||||
"Remaining Program Time": EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME,
|
||||
"Duration": EventKey.BSH_COMMON_OPTION_DURATION,
|
||||
"Program Progress": EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
"Remote Control": StatusKey.BSH_COMMON_REMOTE_CONTROL_ACTIVE,
|
||||
"Remote Start": StatusKey.BSH_COMMON_REMOTE_CONTROL_START_ALLOWED,
|
||||
"Supermode Freezer": SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER,
|
||||
"Supermode Refrigerator": SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_REFRIGERATOR,
|
||||
"Dispenser Enabled": SettingKey.REFRIGERATION_COMMON_DISPENSER_ENABLED,
|
||||
"Internal Light": SettingKey.REFRIGERATION_COMMON_LIGHT_INTERNAL_POWER,
|
||||
"External Light": SettingKey.REFRIGERATION_COMMON_LIGHT_EXTERNAL_POWER,
|
||||
"Chiller Door": StatusKey.REFRIGERATION_COMMON_DOOR_CHILLER,
|
||||
"Freezer Door": StatusKey.REFRIGERATION_COMMON_DOOR_FREEZER,
|
||||
"Refrigerator Door": StatusKey.REFRIGERATION_COMMON_DOOR_REFRIGERATOR,
|
||||
"Door Alarm Freezer": EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
"Door Alarm Refrigerator": EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
"Temperature Alarm Freezer": EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER,
|
||||
"Bean Container Empty": EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
"Water Tank Empty": EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY,
|
||||
"Drip Tray Full": EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
"""Coordinator for Home Connect."""
|
||||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import (
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
GetSetting,
|
||||
HomeAppliance,
|
||||
SettingKey,
|
||||
Status,
|
||||
StatusKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import (
|
||||
EventStreamInterruptedError,
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
HomeConnectRequestError,
|
||||
)
|
||||
from aiohomeconnect.model.program import EnumerateAvailableProgram
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeConnectConfigEntry = ConfigEntry[HomeConnectCoordinator]
|
||||
|
||||
EVENT_STREAM_RECONNECT_DELAY = 30
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeConnectApplianceData:
|
||||
"""Class to hold Home Connect appliance data."""
|
||||
|
||||
events: dict[EventKey, Event] = field(default_factory=dict)
|
||||
info: HomeAppliance
|
||||
programs: list[EnumerateAvailableProgram] = field(default_factory=list)
|
||||
settings: dict[SettingKey, GetSetting]
|
||||
status: dict[StatusKey, Status]
|
||||
|
||||
def update(self, other: "HomeConnectApplianceData") -> None:
|
||||
"""Update data with data from other instance."""
|
||||
self.events.update(other.events)
|
||||
self.info.connected = other.info.connected
|
||||
self.programs.clear()
|
||||
self.programs.extend(other.programs)
|
||||
self.settings.update(other.settings)
|
||||
self.status.update(other.status)
|
||||
|
||||
|
||||
class HomeConnectCoordinator(
|
||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||
):
|
||||
"""Class to manage fetching Home Connect data."""
|
||||
|
||||
config_entry: HomeConnectConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeConnectConfigEntry,
|
||||
client: HomeConnectClient,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=config_entry.entry_id,
|
||||
)
|
||||
self.client = client
|
||||
|
||||
@cached_property
|
||||
def context_listeners(self) -> dict[tuple[str, EventKey], list[CALLBACK_TYPE]]:
|
||||
"""Return a dict of all listeners registered for a given context."""
|
||||
listeners: dict[tuple[str, EventKey], list[CALLBACK_TYPE]] = defaultdict(list)
|
||||
for listener, context in list(self._listeners.values()):
|
||||
assert isinstance(context, tuple)
|
||||
listeners[context].append(listener)
|
||||
return listeners
|
||||
|
||||
@callback
|
||||
def async_add_listener(
|
||||
self, update_callback: CALLBACK_TYPE, context: Any = None
|
||||
) -> Callable[[], None]:
|
||||
"""Listen for data updates."""
|
||||
remove_listener = super().async_add_listener(update_callback, context)
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
def remove_listener_and_invalidate_context_listeners() -> None:
|
||||
remove_listener()
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
return remove_listener_and_invalidate_context_listeners
|
||||
|
||||
@callback
|
||||
def start_event_listener(self) -> None:
|
||||
"""Start event listener."""
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._event_listener(),
|
||||
f"home_connect-events_listener_task-{self.config_entry.entry_id}",
|
||||
)
|
||||
|
||||
async def _event_listener(self) -> None:
|
||||
"""Match event with listener for event type."""
|
||||
while True:
|
||||
try:
|
||||
async for event_message in self.client.stream_all_events():
|
||||
match event_message.type:
|
||||
case EventType.STATUS:
|
||||
statuses = self.data[event_message.ha_id].status
|
||||
for event in event_message.data.items:
|
||||
status_key = StatusKey(event.key)
|
||||
if status_key in statuses:
|
||||
statuses[status_key].value = event.value
|
||||
else:
|
||||
statuses[status_key] = Status(
|
||||
key=status_key,
|
||||
raw_key=status_key.value,
|
||||
value=event.value,
|
||||
)
|
||||
|
||||
case EventType.NOTIFY:
|
||||
settings = self.data[event_message.ha_id].settings
|
||||
events = self.data[event_message.ha_id].events
|
||||
for event in event_message.data.items:
|
||||
if event.key in SettingKey:
|
||||
setting_key = SettingKey(event.key)
|
||||
if setting_key in settings:
|
||||
settings[setting_key].value = event.value
|
||||
else:
|
||||
settings[setting_key] = GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=event.value,
|
||||
)
|
||||
else:
|
||||
events[event.key] = event
|
||||
|
||||
case EventType.EVENT:
|
||||
events = self.data[event_message.ha_id].events
|
||||
for event in event_message.data.items:
|
||||
events[event.key] = event
|
||||
|
||||
self._call_event_listener(event_message)
|
||||
|
||||
except (EventStreamInterruptedError, HomeConnectRequestError) as error:
|
||||
_LOGGER.debug(
|
||||
"Non-breaking error (%s) while listening for events,"
|
||||
" continuing in 30 seconds",
|
||||
type(error).__name__,
|
||||
)
|
||||
await asyncio.sleep(EVENT_STREAM_RECONNECT_DELAY)
|
||||
except HomeConnectApiError as error:
|
||||
_LOGGER.error("Error while listening for events: %s", error)
|
||||
self.hass.config_entries.async_schedule_reload(
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
break
|
||||
# if there was a non-breaking error, we continue listening
|
||||
# but we need to refresh the data to get the possible changes
|
||||
# that happened while the event stream was interrupted
|
||||
await self.async_refresh()
|
||||
|
||||
@callback
|
||||
def _call_event_listener(self, event_message: EventMessage):
|
||||
"""Call listener for event."""
|
||||
for event in event_message.data.items:
|
||||
for listener in self.context_listeners.get(
|
||||
(event_message.ha_id, event.key), []
|
||||
):
|
||||
listener()
|
||||
|
||||
async def _async_update_data(self) -> dict[str, HomeConnectApplianceData]:
|
||||
"""Fetch data from Home Connect."""
|
||||
try:
|
||||
appliances = await self.client.get_home_appliances()
|
||||
except HomeConnectError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="fetch_api_error",
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
|
||||
appliances_data = self.data or {}
|
||||
for appliance in appliances.homeappliances:
|
||||
try:
|
||||
settings = {
|
||||
setting.key: setting
|
||||
for setting in (
|
||||
await self.client.get_settings(appliance.ha_id)
|
||||
).settings
|
||||
}
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching settings for %s: %s",
|
||||
appliance.ha_id,
|
||||
error
|
||||
if isinstance(error, HomeConnectApiError)
|
||||
else type(error).__name__,
|
||||
)
|
||||
settings = {}
|
||||
try:
|
||||
status = {
|
||||
status.key: status
|
||||
for status in (await self.client.get_status(appliance.ha_id)).status
|
||||
}
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching status for %s: %s",
|
||||
appliance.ha_id,
|
||||
error
|
||||
if isinstance(error, HomeConnectApiError)
|
||||
else type(error).__name__,
|
||||
)
|
||||
status = {}
|
||||
appliance_data = HomeConnectApplianceData(
|
||||
info=appliance, settings=settings, status=status
|
||||
)
|
||||
if appliance.ha_id in appliances_data:
|
||||
appliances_data[appliance.ha_id].update(appliance_data)
|
||||
appliance_data = appliances_data[appliance.ha_id]
|
||||
else:
|
||||
appliances_data[appliance.ha_id] = appliance_data
|
||||
if (
|
||||
appliance.type in APPLIANCES_WITH_PROGRAMS
|
||||
and not appliance_data.programs
|
||||
):
|
||||
try:
|
||||
appliance_data.programs.extend(
|
||||
(
|
||||
await self.client.get_available_programs(appliance.ha_id)
|
||||
).programs
|
||||
)
|
||||
except HomeConnectError as error:
|
||||
_LOGGER.debug(
|
||||
"Error fetching programs for %s: %s",
|
||||
appliance.ha_id,
|
||||
error
|
||||
if isinstance(error, HomeConnectApiError)
|
||||
else type(error).__name__,
|
||||
)
|
||||
return appliances_data
|
|
@ -4,33 +4,25 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import HomeConnectConfigEntry, _get_appliance
|
||||
from .api import HomeConnectDevice
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
|
||||
|
||||
def _generate_appliance_diagnostics(appliance: HomeConnectAppliance) -> dict[str, Any]:
|
||||
try:
|
||||
programs = appliance.get_programs_available()
|
||||
except HomeConnectError:
|
||||
programs = None
|
||||
async def _generate_appliance_diagnostics(
|
||||
client: HomeConnectClient, appliance: HomeConnectApplianceData
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"connected": appliance.connected,
|
||||
"status": appliance.status,
|
||||
"programs": programs,
|
||||
}
|
||||
|
||||
|
||||
def _generate_entry_diagnostics(
|
||||
devices: list[HomeConnectDevice],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
device.appliance.haId: _generate_appliance_diagnostics(device.appliance)
|
||||
for device in devices
|
||||
**appliance.info.to_dict(),
|
||||
"status": {key.value: status.value for key, status in appliance.status.items()},
|
||||
"settings": {
|
||||
key.value: setting.value for key, setting in appliance.settings.items()
|
||||
},
|
||||
"programs": [program.raw_key for program in appliance.programs],
|
||||
}
|
||||
|
||||
|
||||
|
@ -38,14 +30,21 @@ async def async_get_config_entry_diagnostics(
|
|||
hass: HomeAssistant, entry: HomeConnectConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return await hass.async_add_executor_job(
|
||||
_generate_entry_diagnostics, entry.runtime_data.devices
|
||||
)
|
||||
return {
|
||||
appliance.info.ha_id: await _generate_appliance_diagnostics(
|
||||
entry.runtime_data.client, appliance
|
||||
)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeConnectConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
appliance = _get_appliance(hass, device_entry=device, entry=entry)
|
||||
return await hass.async_add_executor_job(_generate_appliance_diagnostics, appliance)
|
||||
ha_id = next(
|
||||
(identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN),
|
||||
)
|
||||
return await _generate_appliance_diagnostics(
|
||||
entry.runtime_data.client, entry.runtime_data.data[ha_id]
|
||||
)
|
||||
|
|
|
@ -1,55 +1,56 @@
|
|||
"""Home Connect entity base class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
|
||||
from aiohomeconnect.model import EventKey
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .api import HomeConnectDevice
|
||||
from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomeConnectEntity(Entity):
|
||||
class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
"""Generic Home Connect entity (base class)."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, device: HomeConnectDevice, desc: EntityDescription) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.device = device
|
||||
super().__init__(coordinator, (appliance.info.ha_id, EventKey(desc.key)))
|
||||
self.appliance = appliance
|
||||
self.entity_description = desc
|
||||
self._attr_unique_id = f"{device.appliance.haId}-{self.bsh_key}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.appliance.haId)},
|
||||
manufacturer=device.appliance.brand,
|
||||
model=device.appliance.vib,
|
||||
name=device.appliance.name,
|
||||
identifiers={(DOMAIN, appliance.info.ha_id)},
|
||||
manufacturer=appliance.info.brand,
|
||||
model=appliance.info.vib,
|
||||
name=appliance.info.name,
|
||||
)
|
||||
self.update_native_value()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITIES, self._update_callback
|
||||
)
|
||||
)
|
||||
@abstractmethod
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
|
||||
@callback
|
||||
def _update_callback(self, ha_id: str) -> None:
|
||||
"""Update data."""
|
||||
if ha_id == self.device.appliance.haId:
|
||||
self.async_entity_update()
|
||||
|
||||
@callback
|
||||
def async_entity_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
_LOGGER.debug("Entity update triggered on %s", self)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.update_native_value()
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, self.state)
|
||||
|
||||
@property
|
||||
def bsh_key(self) -> str:
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import EventKey, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -20,25 +20,18 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .api import HomeConnectDevice
|
||||
from .const import (
|
||||
ATTR_VALUE,
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS,
|
||||
BSH_AMBIENT_LIGHT_COLOR,
|
||||
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
|
||||
BSH_AMBIENT_LIGHT_ENABLED,
|
||||
COOKING_LIGHTING,
|
||||
COOKING_LIGHTING_BRIGHTNESS,
|
||||
DOMAIN,
|
||||
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
|
||||
REFRIGERATION_EXTERNAL_LIGHT_POWER,
|
||||
REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
|
||||
REFRIGERATION_INTERNAL_LIGHT_POWER,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -47,38 +40,38 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class HomeConnectLightEntityDescription(LightEntityDescription):
|
||||
"""Light entity description."""
|
||||
|
||||
brightness_key: str | None = None
|
||||
color_key: str | None = None
|
||||
brightness_key: SettingKey | None = None
|
||||
color_key: SettingKey | None = None
|
||||
enable_custom_color_value_key: str | None = None
|
||||
custom_color_key: str | None = None
|
||||
custom_color_key: SettingKey | None = None
|
||||
brightness_scale: tuple[float, float] = (0.0, 100.0)
|
||||
|
||||
|
||||
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
|
||||
HomeConnectLightEntityDescription(
|
||||
key=REFRIGERATION_INTERNAL_LIGHT_POWER,
|
||||
brightness_key=REFRIGERATION_INTERNAL_LIGHT_BRIGHTNESS,
|
||||
key=SettingKey.REFRIGERATION_COMMON_LIGHT_INTERNAL_POWER,
|
||||
brightness_key=SettingKey.REFRIGERATION_COMMON_LIGHT_INTERNAL_BRIGHTNESS,
|
||||
brightness_scale=(1.0, 100.0),
|
||||
translation_key="internal_light",
|
||||
),
|
||||
HomeConnectLightEntityDescription(
|
||||
key=REFRIGERATION_EXTERNAL_LIGHT_POWER,
|
||||
brightness_key=REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
|
||||
key=SettingKey.REFRIGERATION_COMMON_LIGHT_EXTERNAL_POWER,
|
||||
brightness_key=SettingKey.REFRIGERATION_COMMON_LIGHT_EXTERNAL_BRIGHTNESS,
|
||||
brightness_scale=(1.0, 100.0),
|
||||
translation_key="external_light",
|
||||
),
|
||||
HomeConnectLightEntityDescription(
|
||||
key=COOKING_LIGHTING,
|
||||
brightness_key=COOKING_LIGHTING_BRIGHTNESS,
|
||||
key=SettingKey.COOKING_COMMON_LIGHTING,
|
||||
brightness_key=SettingKey.COOKING_COMMON_LIGHTING_BRIGHTNESS,
|
||||
brightness_scale=(10.0, 100.0),
|
||||
translation_key="cooking_lighting",
|
||||
),
|
||||
HomeConnectLightEntityDescription(
|
||||
key=BSH_AMBIENT_LIGHT_ENABLED,
|
||||
brightness_key=BSH_AMBIENT_LIGHT_BRIGHTNESS,
|
||||
color_key=BSH_AMBIENT_LIGHT_COLOR,
|
||||
key=SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED,
|
||||
brightness_key=SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS,
|
||||
color_key=SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
|
||||
enable_custom_color_value_key=BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
custom_color_key=BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
|
||||
custom_color_key=SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR,
|
||||
brightness_scale=(10.0, 100.0),
|
||||
translation_key="ambient_light",
|
||||
),
|
||||
|
@ -92,16 +85,14 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect light."""
|
||||
|
||||
def get_entities() -> list[LightEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
HomeConnectLight(device, description)
|
||||
async_add_entities(
|
||||
[
|
||||
HomeConnectLight(entry.runtime_data, appliance, description)
|
||||
for description in LIGHTS
|
||||
for device in entry.runtime_data.devices
|
||||
if description.key in device.appliance.status
|
||||
]
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
if description.key in appliance.settings
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
|
@ -110,13 +101,17 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
entity_description: LightEntityDescription
|
||||
|
||||
def __init__(
|
||||
self, device: HomeConnectDevice, desc: HomeConnectLightEntityDescription
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: HomeConnectLightEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device, desc)
|
||||
|
||||
def get_setting_key_if_setting_exists(setting_key: str | None) -> str | None:
|
||||
if setting_key and setting_key in device.appliance.status:
|
||||
def get_setting_key_if_setting_exists(
|
||||
setting_key: SettingKey | None,
|
||||
) -> SettingKey | None:
|
||||
if setting_key and setting_key in appliance.settings:
|
||||
return setting_key
|
||||
return None
|
||||
|
||||
|
@ -131,6 +126,8 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
)
|
||||
self._brightness_scale = desc.brightness_scale
|
||||
|
||||
super().__init__(coordinator, appliance, desc)
|
||||
|
||||
match (self._brightness_key, self._custom_color_key):
|
||||
case (None, None):
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
|
@ -144,10 +141,11 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Switch the light on, change brightness, change color."""
|
||||
_LOGGER.debug("Switching light on for: %s", self.name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, self.bsh_key, True
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=True,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -158,15 +156,15 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
},
|
||||
) from err
|
||||
if self._custom_color_key:
|
||||
if self._color_key and self._custom_color_key:
|
||||
if (
|
||||
ATTR_RGB_COLOR in kwargs or ATTR_HS_COLOR in kwargs
|
||||
) and self._enable_custom_color_value_key:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
self._color_key,
|
||||
self._enable_custom_color_value_key,
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=self._color_key,
|
||||
value=self._enable_custom_color_value_key,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -181,10 +179,10 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
if ATTR_RGB_COLOR in kwargs:
|
||||
hex_val = color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
self._custom_color_key,
|
||||
f"#{hex_val}",
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=self._custom_color_key,
|
||||
value=f"#{hex_val}",
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -195,10 +193,11 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
},
|
||||
) from err
|
||||
elif (ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs) and (
|
||||
self._attr_brightness is not None or ATTR_BRIGHTNESS in kwargs
|
||||
return
|
||||
if (self._attr_brightness is not None or ATTR_BRIGHTNESS in kwargs) and (
|
||||
self._attr_hs_color is not None or ATTR_HS_COLOR in kwargs
|
||||
):
|
||||
brightness = 10 + ceil(
|
||||
brightness = round(
|
||||
color_util.brightness_to_value(
|
||||
self._brightness_scale,
|
||||
kwargs.get(ATTR_BRIGHTNESS, self._attr_brightness),
|
||||
|
@ -207,41 +206,36 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
|
||||
hs_color = kwargs.get(ATTR_HS_COLOR, self._attr_hs_color)
|
||||
|
||||
if hs_color is not None:
|
||||
rgb = color_util.color_hsv_to_RGB(
|
||||
hs_color[0], hs_color[1], brightness
|
||||
rgb = color_util.color_hsv_to_RGB(hs_color[0], hs_color[1], brightness)
|
||||
hex_val = color_util.color_rgb_to_hex(*rgb)
|
||||
try:
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=self._custom_color_key,
|
||||
value=f"#{hex_val}",
|
||||
)
|
||||
hex_val = color_util.color_rgb_to_hex(*rgb)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
self._custom_color_key,
|
||||
f"#{hex_val}",
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_light_color",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
},
|
||||
) from err
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_light_color",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
},
|
||||
) from err
|
||||
return
|
||||
|
||||
elif self._brightness_key and ATTR_BRIGHTNESS in kwargs:
|
||||
_LOGGER.debug(
|
||||
"Changing brightness for: %s, to: %s",
|
||||
self.name,
|
||||
kwargs[ATTR_BRIGHTNESS],
|
||||
)
|
||||
brightness = ceil(
|
||||
if self._brightness_key and ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = round(
|
||||
color_util.brightness_to_value(
|
||||
self._brightness_scale, kwargs[ATTR_BRIGHTNESS]
|
||||
)
|
||||
)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, self._brightness_key, brightness
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=self._brightness_key,
|
||||
value=brightness,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -253,14 +247,13 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
},
|
||||
) from err
|
||||
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Switch the light off."""
|
||||
_LOGGER.debug("Switching light off for: %s", self.name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, self.bsh_key, False
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=False,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -271,30 +264,50 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register listener."""
|
||||
await super().async_added_to_hass()
|
||||
keys_to_listen = []
|
||||
if self._brightness_key:
|
||||
keys_to_listen.append(self._brightness_key)
|
||||
if self._color_key and self._custom_color_key:
|
||||
keys_to_listen.extend([self._color_key, self._custom_color_key])
|
||||
for key in keys_to_listen:
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(
|
||||
self._handle_coordinator_update,
|
||||
(
|
||||
self.appliance.info.ha_id,
|
||||
EventKey(key),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the light's status."""
|
||||
if self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) is True:
|
||||
self._attr_is_on = True
|
||||
elif (
|
||||
self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE) is False
|
||||
):
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
self._attr_is_on = None
|
||||
self._attr_is_on = self.appliance.settings[SettingKey(self.bsh_key)].value
|
||||
|
||||
_LOGGER.debug("Updated, new light state: %s", self._attr_is_on)
|
||||
|
||||
if self._custom_color_key:
|
||||
color = self.device.appliance.status.get(self._custom_color_key, {})
|
||||
|
||||
if not color:
|
||||
if self._brightness_key:
|
||||
brightness = cast(
|
||||
float, self.appliance.settings[self._brightness_key].value
|
||||
)
|
||||
self._attr_brightness = color_util.value_to_brightness(
|
||||
self._brightness_scale, brightness
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Updated %s, new brightness: %s", self.entity_id, self._attr_brightness
|
||||
)
|
||||
if self._color_key and self._custom_color_key:
|
||||
color = cast(str, self.appliance.settings[self._color_key].value)
|
||||
if color != self._enable_custom_color_value_key:
|
||||
self._attr_rgb_color = None
|
||||
self._attr_hs_color = None
|
||||
self._attr_brightness = None
|
||||
else:
|
||||
color_value = color.get(ATTR_VALUE)[1:]
|
||||
custom_color = cast(
|
||||
str, self.appliance.settings[self._custom_color_key].value
|
||||
)
|
||||
color_value = custom_color[1:]
|
||||
rgb = color_util.rgb_hex_to_rgb_list(color_value)
|
||||
self._attr_rgb_color = (rgb[0], rgb[1], rgb[2])
|
||||
hsv = color_util.color_RGB_to_hsv(*rgb)
|
||||
|
@ -303,16 +316,8 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
|||
self._brightness_scale, hsv[2]
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Updated, new color (%s) and new brightness (%s) ",
|
||||
"Updated %s, new color (%s) and new brightness (%s) ",
|
||||
self.entity_id,
|
||||
color_value,
|
||||
self._attr_brightness,
|
||||
)
|
||||
elif self._brightness_key:
|
||||
brightness = self.device.appliance.status.get(self._brightness_key, {})
|
||||
if brightness is None:
|
||||
self._attr_brightness = None
|
||||
else:
|
||||
self._attr_brightness = color_util.value_to_brightness(
|
||||
self._brightness_scale, brightness[ATTR_VALUE]
|
||||
)
|
||||
_LOGGER.debug("Updated, new brightness: %s", self._attr_brightness)
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homeconnect"],
|
||||
"requirements": ["homeconnect==0.8.0"]
|
||||
"requirements": ["aiohomeconnect==0.12.1"]
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
"""Provides number enties for Home Connect."""
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import GetSetting, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
|
@ -15,66 +15,63 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .const import (
|
||||
ATTR_CONSTRAINTS,
|
||||
ATTR_STEPSIZE,
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .coordinator import HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
NUMBERS = (
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
|
||||
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="refrigerator_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.FridgeFreezer.Setting.SetpointTemperatureFreezer",
|
||||
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_FREEZER,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="freezer_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.BottleCooler.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_BOTTLE_COOLER_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="bottle_cooler_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.ChillerLeft.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_CHILLER_LEFT_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="chiller_left_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.ChillerCommon.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_CHILLER_COMMON_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="chiller_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.ChillerRight.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_CHILLER_RIGHT_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="chiller_right_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.WineCompartment.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_WINE_COMPARTMENT_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="wine_compartment_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.WineCompartment2.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_WINE_COMPARTMENT_2_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="wine_compartment_2_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key="Refrigeration.Common.Setting.WineCompartment3.SetpointTemperature",
|
||||
key=SettingKey.REFRIGERATION_COMMON_WINE_COMPARTMENT_3_SETPOINT_TEMPERATURE,
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="wine_compartment_3_setpoint_temperature",
|
||||
),
|
||||
|
@ -87,17 +84,14 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Home Connect number."""
|
||||
|
||||
def get_entities() -> list[HomeConnectNumberEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
HomeConnectNumberEntity(device, description)
|
||||
async_add_entities(
|
||||
[
|
||||
HomeConnectNumberEntity(entry.runtime_data, appliance, description)
|
||||
for description in NUMBERS
|
||||
for device in entry.runtime_data.devices
|
||||
if description.key in device.appliance.status
|
||||
]
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
if description.key in appliance.settings
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
|
@ -112,10 +106,10 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
|||
self.entity_id,
|
||||
)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
self.bsh_key,
|
||||
value,
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=value,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -132,34 +126,41 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
|||
async def async_fetch_constraints(self) -> None:
|
||||
"""Fetch the max and min values and step for the number entity."""
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self.device.appliance.get, f"/settings/{self.bsh_key}"
|
||||
data = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id, setting_key=SettingKey(self.bsh_key)
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("An error occurred: %s", err)
|
||||
return
|
||||
if not data or not (constraints := data.get(ATTR_CONSTRAINTS)):
|
||||
return
|
||||
self._attr_native_max_value = constraints.get(ATTR_MAX)
|
||||
self._attr_native_min_value = constraints.get(ATTR_MIN)
|
||||
self._attr_native_step = constraints.get(ATTR_STEPSIZE)
|
||||
self._attr_native_unit_of_measurement = data.get(ATTR_UNIT)
|
||||
else:
|
||||
self.set_constraints(data)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the number setting status."""
|
||||
if not (data := self.device.appliance.status.get(self.bsh_key)):
|
||||
_LOGGER.error("No value for %s", self.bsh_key)
|
||||
self._attr_native_value = None
|
||||
def set_constraints(self, setting: GetSetting) -> None:
|
||||
"""Set constraints for the number entity."""
|
||||
if not (constraints := setting.constraints):
|
||||
return
|
||||
self._attr_native_value = data.get(ATTR_VALUE, None)
|
||||
_LOGGER.debug("Updated, new value: %s", self._attr_native_value)
|
||||
if constraints.max:
|
||||
self._attr_native_max_value = constraints.max
|
||||
if constraints.min:
|
||||
self._attr_native_min_value = constraints.min
|
||||
if constraints.step_size:
|
||||
self._attr_native_step = constraints.step_size
|
||||
else:
|
||||
self._attr_native_step = 0.1 if setting.type == "Double" else 1
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update status when an event for the entity is received."""
|
||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||
self._attr_native_value = cast(float, data.value)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""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")
|
||||
or self._attr_native_min_value is None
|
||||
or not hasattr(self, "_attr_native_max_value")
|
||||
or self._attr_native_max_value is None
|
||||
or not hasattr(self, "_attr_native_step")
|
||||
or self._attr_native_step is None
|
||||
):
|
||||
await self.async_fetch_constraints()
|
||||
|
|
|
@ -1,191 +1,28 @@
|
|||
"""Provides a select platform for Home Connect."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import EventKey, ProgramKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import (
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN, SVE_TRANSLATION_PLACEHOLDER_PROGRAM
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
bsh_key_to_translation_key,
|
||||
get_dict_from_home_connect_error,
|
||||
)
|
||||
from .api import HomeConnectDevice
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
ATTR_VALUE,
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_SELECTED_PROGRAM,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program): program
|
||||
for program in (
|
||||
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanAll",
|
||||
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanMap",
|
||||
"ConsumerProducts.CleaningRobot.Program.Basic.GoHome",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.XLCoffee",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeGrande",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KleinerBrauner",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.GrosserBrauner",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Verlaengerter",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.VerlaengerterBraun",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.WienerMelange",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeCortado",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeConLeche",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeAuLait",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Doppio",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Kaapi",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KoffieVerkeerd",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Garoto",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.RedEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.BlackEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.DeadEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.HotWater",
|
||||
"Dishcare.Dishwasher.Program.PreRinse",
|
||||
"Dishcare.Dishwasher.Program.Auto1",
|
||||
"Dishcare.Dishwasher.Program.Auto2",
|
||||
"Dishcare.Dishwasher.Program.Auto3",
|
||||
"Dishcare.Dishwasher.Program.Eco50",
|
||||
"Dishcare.Dishwasher.Program.Quick45",
|
||||
"Dishcare.Dishwasher.Program.Intensiv70",
|
||||
"Dishcare.Dishwasher.Program.Normal65",
|
||||
"Dishcare.Dishwasher.Program.Glas40",
|
||||
"Dishcare.Dishwasher.Program.GlassCare",
|
||||
"Dishcare.Dishwasher.Program.NightWash",
|
||||
"Dishcare.Dishwasher.Program.Quick65",
|
||||
"Dishcare.Dishwasher.Program.Normal45",
|
||||
"Dishcare.Dishwasher.Program.Intensiv45",
|
||||
"Dishcare.Dishwasher.Program.AutoHalfLoad",
|
||||
"Dishcare.Dishwasher.Program.IntensivPower",
|
||||
"Dishcare.Dishwasher.Program.MagicDaily",
|
||||
"Dishcare.Dishwasher.Program.Super60",
|
||||
"Dishcare.Dishwasher.Program.Kurz60",
|
||||
"Dishcare.Dishwasher.Program.ExpressSparkle65",
|
||||
"Dishcare.Dishwasher.Program.MachineCare",
|
||||
"Dishcare.Dishwasher.Program.SteamFresh",
|
||||
"Dishcare.Dishwasher.Program.MaximumCleaning",
|
||||
"Dishcare.Dishwasher.Program.MixedLoad",
|
||||
"LaundryCare.Dryer.Program.Cotton",
|
||||
"LaundryCare.Dryer.Program.Synthetic",
|
||||
"LaundryCare.Dryer.Program.Mix",
|
||||
"LaundryCare.Dryer.Program.Blankets",
|
||||
"LaundryCare.Dryer.Program.BusinessShirts",
|
||||
"LaundryCare.Dryer.Program.DownFeathers",
|
||||
"LaundryCare.Dryer.Program.Hygiene",
|
||||
"LaundryCare.Dryer.Program.Jeans",
|
||||
"LaundryCare.Dryer.Program.Outdoor",
|
||||
"LaundryCare.Dryer.Program.SyntheticRefresh",
|
||||
"LaundryCare.Dryer.Program.Towels",
|
||||
"LaundryCare.Dryer.Program.Delicates",
|
||||
"LaundryCare.Dryer.Program.Super40",
|
||||
"LaundryCare.Dryer.Program.Shirts15",
|
||||
"LaundryCare.Dryer.Program.Pillow",
|
||||
"LaundryCare.Dryer.Program.AntiShrink",
|
||||
"LaundryCare.Dryer.Program.MyTime.MyDryingTime",
|
||||
"LaundryCare.Dryer.Program.TimeCold",
|
||||
"LaundryCare.Dryer.Program.TimeWarm",
|
||||
"LaundryCare.Dryer.Program.InBasket",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold20",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold30",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold60",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm30",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm40",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm60",
|
||||
"LaundryCare.Dryer.Program.Dessous",
|
||||
"Cooking.Common.Program.Hood.Automatic",
|
||||
"Cooking.Common.Program.Hood.Venting",
|
||||
"Cooking.Common.Program.Hood.DelayedShutOff",
|
||||
"Cooking.Oven.Program.HeatingMode.PreHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAirEco",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAirGrilling",
|
||||
"Cooking.Oven.Program.HeatingMode.TopBottomHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.TopBottomHeatingEco",
|
||||
"Cooking.Oven.Program.HeatingMode.BottomHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.PizzaSetting",
|
||||
"Cooking.Oven.Program.HeatingMode.SlowCook",
|
||||
"Cooking.Oven.Program.HeatingMode.IntensiveHeat",
|
||||
"Cooking.Oven.Program.HeatingMode.KeepWarm",
|
||||
"Cooking.Oven.Program.HeatingMode.PreheatOvenware",
|
||||
"Cooking.Oven.Program.HeatingMode.FrozenHeatupSpecial",
|
||||
"Cooking.Oven.Program.HeatingMode.Desiccation",
|
||||
"Cooking.Oven.Program.HeatingMode.Defrost",
|
||||
"Cooking.Oven.Program.HeatingMode.Proof",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir30Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir60Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir80Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir100Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.SabbathProgramme",
|
||||
"Cooking.Oven.Program.Microwave.90Watt",
|
||||
"Cooking.Oven.Program.Microwave.180Watt",
|
||||
"Cooking.Oven.Program.Microwave.360Watt",
|
||||
"Cooking.Oven.Program.Microwave.600Watt",
|
||||
"Cooking.Oven.Program.Microwave.900Watt",
|
||||
"Cooking.Oven.Program.Microwave.1000Watt",
|
||||
"Cooking.Oven.Program.Microwave.Max",
|
||||
"Cooking.Oven.Program.HeatingMode.WarmingDrawer",
|
||||
"LaundryCare.Washer.Program.Cotton",
|
||||
"LaundryCare.Washer.Program.Cotton.CottonEco",
|
||||
"LaundryCare.Washer.Program.Cotton.Eco4060",
|
||||
"LaundryCare.Washer.Program.Cotton.Colour",
|
||||
"LaundryCare.Washer.Program.EasyCare",
|
||||
"LaundryCare.Washer.Program.Mix",
|
||||
"LaundryCare.Washer.Program.Mix.NightWash",
|
||||
"LaundryCare.Washer.Program.DelicatesSilk",
|
||||
"LaundryCare.Washer.Program.Wool",
|
||||
"LaundryCare.Washer.Program.Sensitive",
|
||||
"LaundryCare.Washer.Program.Auto30",
|
||||
"LaundryCare.Washer.Program.Auto40",
|
||||
"LaundryCare.Washer.Program.Auto60",
|
||||
"LaundryCare.Washer.Program.Chiffon",
|
||||
"LaundryCare.Washer.Program.Curtains",
|
||||
"LaundryCare.Washer.Program.DarkWash",
|
||||
"LaundryCare.Washer.Program.Dessous",
|
||||
"LaundryCare.Washer.Program.Monsoon",
|
||||
"LaundryCare.Washer.Program.Outdoor",
|
||||
"LaundryCare.Washer.Program.PlushToy",
|
||||
"LaundryCare.Washer.Program.ShirtsBlouses",
|
||||
"LaundryCare.Washer.Program.SportFitness",
|
||||
"LaundryCare.Washer.Program.Towels",
|
||||
"LaundryCare.Washer.Program.WaterProof",
|
||||
"LaundryCare.Washer.Program.PowerSpeed59",
|
||||
"LaundryCare.Washer.Program.Super153045.Super15",
|
||||
"LaundryCare.Washer.Program.Super153045.Super1530",
|
||||
"LaundryCare.Washer.Program.DownDuvet.Duvet",
|
||||
"LaundryCare.Washer.Program.Rinse.RinseSpinDrain",
|
||||
"LaundryCare.Washer.Program.DrumClean",
|
||||
"LaundryCare.WasherDryer.Program.Cotton",
|
||||
"LaundryCare.WasherDryer.Program.Cotton.Eco4060",
|
||||
"LaundryCare.WasherDryer.Program.Mix",
|
||||
"LaundryCare.WasherDryer.Program.EasyCare",
|
||||
"LaundryCare.WasherDryer.Program.WashAndDry60",
|
||||
"LaundryCare.WasherDryer.Program.WashAndDry90",
|
||||
)
|
||||
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
|
||||
for program in ProgramKey
|
||||
if program != ProgramKey.UNKNOWN
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
|
@ -194,11 +31,11 @@ PROGRAMS_TRANSLATION_KEYS_MAP = {
|
|||
|
||||
PROGRAM_SELECT_ENTITY_DESCRIPTIONS = (
|
||||
SelectEntityDescription(
|
||||
key=BSH_ACTIVE_PROGRAM,
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
translation_key="active_program",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=BSH_SELECTED_PROGRAM,
|
||||
key=EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
translation_key="selected_program",
|
||||
),
|
||||
)
|
||||
|
@ -211,31 +48,12 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect select entities."""
|
||||
|
||||
def get_entities() -> list[HomeConnectProgramSelectEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[HomeConnectProgramSelectEntity] = []
|
||||
programs_not_found = set()
|
||||
for device in entry.runtime_data.devices:
|
||||
if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
programs = device.appliance.get_programs_available()
|
||||
if programs:
|
||||
for program in programs.copy():
|
||||
if program not in PROGRAMS_TRANSLATION_KEYS_MAP:
|
||||
programs.remove(program)
|
||||
if program not in programs_not_found:
|
||||
_LOGGER.info(
|
||||
'The program "%s" is not part of the official Home Connect API specification',
|
||||
program,
|
||||
)
|
||||
programs_not_found.add(program)
|
||||
entities.extend(
|
||||
HomeConnectProgramSelectEntity(device, programs, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
return entities
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
async_add_entities(
|
||||
HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
|
@ -243,48 +61,45 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
device: HomeConnectDevice,
|
||||
programs: list[str],
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: SelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
device,
|
||||
coordinator,
|
||||
appliance,
|
||||
desc,
|
||||
)
|
||||
self._attr_options = [
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP[program] for program in programs
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP[program.key]
|
||||
for program in appliance.programs
|
||||
if program.key != ProgramKey.UNKNOWN
|
||||
]
|
||||
self.start_on_select = desc.key == BSH_ACTIVE_PROGRAM
|
||||
self.start_on_select = desc.key == EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM
|
||||
self._attr_current_option = None
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the program selection status."""
|
||||
program = self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
|
||||
if not program:
|
||||
program_translation_key = None
|
||||
elif not (
|
||||
program_translation_key := PROGRAMS_TRANSLATION_KEYS_MAP.get(program)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
'The program "%s" is not part of the official Home Connect API specification',
|
||||
program,
|
||||
)
|
||||
self._attr_current_option = program_translation_key
|
||||
_LOGGER.debug("Updated, new program: %s", self._attr_current_option)
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the program value."""
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
self._attr_current_option = (
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP.get(cast(ProgramKey, event.value))
|
||||
if event
|
||||
else None
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new program."""
|
||||
bsh_key = TRANSLATION_KEYS_PROGRAMS_MAP[option]
|
||||
_LOGGER.debug(
|
||||
"Starting program: %s" if self.start_on_select else "Selecting program: %s",
|
||||
bsh_key,
|
||||
)
|
||||
if self.start_on_select:
|
||||
target = self.device.appliance.start_program
|
||||
else:
|
||||
target = self.device.appliance.select_program
|
||||
program_key = TRANSLATION_KEYS_PROGRAMS_MAP[option]
|
||||
try:
|
||||
await self.hass.async_add_executor_job(target, bsh_key)
|
||||
if self.start_on_select:
|
||||
await self.coordinator.client.start_program(
|
||||
self.appliance.info.ha_id, program_key=program_key
|
||||
)
|
||||
else:
|
||||
await self.coordinator.client.set_selected_program(
|
||||
self.appliance.info.ha_id, program_key=program_key
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
if self.start_on_select:
|
||||
translation_key = "start_program"
|
||||
|
@ -295,7 +110,6 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
|||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program_key.value,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"""Provides a sensor for Home Connect."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, StatusKey
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
|
@ -12,38 +13,26 @@ from homeassistant.components.sensor import (
|
|||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from . import HomeConnectConfigEntry
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
ATTR_VALUE,
|
||||
BSH_DOOR_STATE,
|
||||
BSH_OPERATION_STATE,
|
||||
BSH_OPERATION_STATE_FINISHED,
|
||||
BSH_OPERATION_STATE_PAUSE,
|
||||
BSH_OPERATION_STATE_RUN,
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
COFFEE_EVENT_DRIP_TRAY_FULL,
|
||||
COFFEE_EVENT_WATER_TANK_EMPTY,
|
||||
DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
|
||||
DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
|
||||
)
|
||||
from .coordinator import HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EVENT_OPTIONS = ["confirmed", "off", "present"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeConnectSensorEntityDescription(SensorEntityDescription):
|
||||
class HomeConnectSensorEntityDescription(
|
||||
SensorEntityDescription,
|
||||
):
|
||||
"""Entity Description class for sensors."""
|
||||
|
||||
default_value: str | None = None
|
||||
|
@ -52,7 +41,7 @@ class HomeConnectSensorEntityDescription(SensorEntityDescription):
|
|||
|
||||
BSH_PROGRAM_SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="BSH.Common.Option.RemainingProgramTime",
|
||||
key=EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
translation_key="program_finish_time",
|
||||
appliance_types=(
|
||||
|
@ -67,13 +56,13 @@ BSH_PROGRAM_SENSORS = (
|
|||
),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="BSH.Common.Option.Duration",
|
||||
key=EventKey.BSH_COMMON_OPTION_DURATION,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
appliance_types=("Oven",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="BSH.Common.Option.ProgramProgress",
|
||||
key=EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
translation_key="program_progress",
|
||||
appliance_types=APPLIANCES_WITH_PROGRAMS,
|
||||
|
@ -82,7 +71,7 @@ BSH_PROGRAM_SENSORS = (
|
|||
|
||||
SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=BSH_OPERATION_STATE,
|
||||
key=StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"inactive",
|
||||
|
@ -98,7 +87,7 @@ SENSORS = (
|
|||
translation_key="operation_state",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=BSH_DOOR_STATE,
|
||||
key=StatusKey.BSH_COMMON_DOOR_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"closed",
|
||||
|
@ -108,59 +97,59 @@ SENSORS = (
|
|||
translation_key="door",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffee",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="coffee_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterPowderCoffee",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_POWDER_COFFEE,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="powder_coffee_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWater",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER,
|
||||
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_water_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotWaterCups",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER_CUPS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_water_cups_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterHotMilk",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_MILK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterFrothyMilk",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_FROTHY_MILK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="frothy_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterMilk",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_MILK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterCoffeeAndMilk",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE_AND_MILK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="coffee_and_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Status.BeverageCounterRistrettoEspresso",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_RISTRETTO_ESPRESSO,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="ristretto_espresso_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="BSH.Common.Status.BatteryLevel",
|
||||
key=StatusKey.BSH_COMMON_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
translation_key="battery_level",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="BSH.Common.Status.Video.CameraState",
|
||||
key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"disabled",
|
||||
|
@ -174,7 +163,7 @@ SENSORS = (
|
|||
translation_key="camera_state",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key="ConsumerProducts.CleaningRobot.Status.LastSelectedMap",
|
||||
key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_LAST_SELECTED_MAP,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"tempmap",
|
||||
|
@ -188,7 +177,7 @@ SENSORS = (
|
|||
|
||||
EVENT_SENSORS = (
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -196,7 +185,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=REFRIGERATION_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -204,7 +193,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("FridgeFreezer", "Refrigerator"),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=REFRIGERATION_EVENT_TEMP_ALARM_FREEZER,
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -212,7 +201,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -220,7 +209,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=COFFEE_EVENT_WATER_TANK_EMPTY,
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -228,7 +217,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=COFFEE_EVENT_DRIP_TRAY_FULL,
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -236,7 +225,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -244,7 +233,7 @@ EVENT_SENSORS = (
|
|||
appliance_types=("Dishwasher",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
|
@ -261,33 +250,30 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect sensor."""
|
||||
|
||||
def get_entities() -> list[SensorEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[SensorEntity] = []
|
||||
for device in entry.runtime_data.devices:
|
||||
entities.extend(
|
||||
HomeConnectSensor(
|
||||
device,
|
||||
description,
|
||||
)
|
||||
for description in EVENT_SENSORS
|
||||
if description.appliance_types
|
||||
and device.appliance.type in description.appliance_types
|
||||
entities: list[SensorEntity] = []
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities.extend(
|
||||
HomeConnectEventSensor(
|
||||
entry.runtime_data,
|
||||
appliance,
|
||||
description,
|
||||
)
|
||||
entities.extend(
|
||||
HomeConnectProgramSensor(device, desc)
|
||||
for desc in BSH_PROGRAM_SENSORS
|
||||
if desc.appliance_types
|
||||
and device.appliance.type in desc.appliance_types
|
||||
)
|
||||
entities.extend(
|
||||
HomeConnectSensor(device, description)
|
||||
for description in SENSORS
|
||||
if description.key in device.appliance.status
|
||||
)
|
||||
return entities
|
||||
for description in EVENT_SENSORS
|
||||
if description.appliance_types
|
||||
and appliance.info.type in description.appliance_types
|
||||
)
|
||||
entities.extend(
|
||||
HomeConnectProgramSensor(entry.runtime_data, appliance, desc)
|
||||
for desc in BSH_PROGRAM_SENSORS
|
||||
if desc.appliance_types and appliance.info.type in desc.appliance_types
|
||||
)
|
||||
entities.extend(
|
||||
HomeConnectSensor(entry.runtime_data, appliance, description)
|
||||
for description in SENSORS
|
||||
if description.key in appliance.status
|
||||
)
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
||||
|
@ -295,44 +281,25 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
|||
|
||||
entity_description: HomeConnectSensorEntityDescription
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the sensor's status."""
|
||||
appliance_status = self.device.appliance.status
|
||||
if (
|
||||
self.bsh_key not in appliance_status
|
||||
or ATTR_VALUE not in appliance_status[self.bsh_key]
|
||||
):
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
_LOGGER.debug("Updated, new state: %s", self._attr_native_value)
|
||||
return
|
||||
status = appliance_status[self.bsh_key]
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the sensor."""
|
||||
status = self.appliance.status[cast(StatusKey, self.bsh_key)].value
|
||||
self._update_native_value(status)
|
||||
|
||||
def _update_native_value(self, status: str | float) -> None:
|
||||
"""Set the value of the sensor based on the given value."""
|
||||
match self.device_class:
|
||||
case SensorDeviceClass.TIMESTAMP:
|
||||
if ATTR_VALUE not in status:
|
||||
self._attr_native_value = None
|
||||
elif (
|
||||
self._attr_native_value is not None
|
||||
and isinstance(self._attr_native_value, datetime)
|
||||
and self._attr_native_value < dt_util.utcnow()
|
||||
):
|
||||
# if the date is supposed to be in the future but we're
|
||||
# already past it, set state to None.
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
seconds = float(status[ATTR_VALUE])
|
||||
self._attr_native_value = dt_util.utcnow() + timedelta(
|
||||
seconds=seconds
|
||||
)
|
||||
self._attr_native_value = dt_util.utcnow() + timedelta(
|
||||
seconds=cast(float, status)
|
||||
)
|
||||
case SensorDeviceClass.ENUM:
|
||||
# Value comes back as an enum, we only really care about the
|
||||
# last part, so split it off
|
||||
# https://developer.home-connect.com/docs/status/operation_state
|
||||
self._attr_native_value = slugify(
|
||||
cast(str, status.get(ATTR_VALUE)).split(".")[-1]
|
||||
)
|
||||
self._attr_native_value = slugify(cast(str, status).split(".")[-1])
|
||||
case _:
|
||||
self._attr_native_value = status.get(ATTR_VALUE)
|
||||
_LOGGER.debug("Updated, new state: %s", self._attr_native_value)
|
||||
self._attr_native_value = status
|
||||
|
||||
|
||||
class HomeConnectProgramSensor(HomeConnectSensor):
|
||||
|
@ -340,6 +307,31 @@ class HomeConnectProgramSensor(HomeConnectSensor):
|
|||
|
||||
program_running: bool = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register listener."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(
|
||||
self._handle_operation_state_event,
|
||||
(self.appliance.info.ha_id, EventKey.BSH_COMMON_STATUS_OPERATION_STATE),
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_operation_state_event(self) -> None:
|
||||
"""Update status when an event for the entity is received."""
|
||||
self.program_running = (
|
||||
status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE)
|
||||
) is not None and status.value in [
|
||||
BSH_OPERATION_STATE_RUN,
|
||||
BSH_OPERATION_STATE_PAUSE,
|
||||
BSH_OPERATION_STATE_FINISHED,
|
||||
]
|
||||
if not self.program_running:
|
||||
# reset the value when the program is not running, paused or finished
|
||||
self._attr_native_value = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return true if the sensor is available."""
|
||||
|
@ -347,20 +339,20 @@ class HomeConnectProgramSensor(HomeConnectSensor):
|
|||
# Otherwise, some sensors report erroneous values.
|
||||
return super().available and self.program_running
|
||||
|
||||
async def async_update(self) -> None:
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the program sensor's status."""
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
if event:
|
||||
self._update_native_value(event.value)
|
||||
|
||||
|
||||
class HomeConnectEventSensor(HomeConnectSensor):
|
||||
"""Sensor class for Home Connect events."""
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the sensor's status."""
|
||||
self.program_running = (
|
||||
BSH_OPERATION_STATE in (appliance_status := self.device.appliance.status)
|
||||
and ATTR_VALUE in appliance_status[BSH_OPERATION_STATE]
|
||||
and appliance_status[BSH_OPERATION_STATE][ATTR_VALUE]
|
||||
in [
|
||||
BSH_OPERATION_STATE_RUN,
|
||||
BSH_OPERATION_STATE_PAUSE,
|
||||
BSH_OPERATION_STATE_FINISHED,
|
||||
]
|
||||
)
|
||||
if self.program_running:
|
||||
await super().async_update()
|
||||
else:
|
||||
# reset the value when the program is not running, paused or finished
|
||||
self._attr_native_value = None
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
if event:
|
||||
self._update_native_value(event.value)
|
||||
elif not self._attr_native_value:
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
|
|
|
@ -26,64 +26,67 @@
|
|||
"message": "Appliance for device ID {device_id} not found"
|
||||
},
|
||||
"turn_on_light": {
|
||||
"message": "Error turning on {entity_id}: {description}"
|
||||
"message": "Error turning on {entity_id}: {error}"
|
||||
},
|
||||
"turn_off_light": {
|
||||
"message": "Error turning off {entity_id}: {description}"
|
||||
"message": "Error turning off {entity_id}: {error}"
|
||||
},
|
||||
"set_light_brightness": {
|
||||
"message": "Error setting brightness of {entity_id}: {description}"
|
||||
"message": "Error setting brightness of {entity_id}: {error}"
|
||||
},
|
||||
"select_light_custom_color": {
|
||||
"message": "Error selecting custom color of {entity_id}: {description}"
|
||||
"message": "Error selecting custom color of {entity_id}: {error}"
|
||||
},
|
||||
"set_light_color": {
|
||||
"message": "Error setting color of {entity_id}: {description}"
|
||||
"message": "Error setting color of {entity_id}: {error}"
|
||||
},
|
||||
"set_setting_entity": {
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\" for {entity_id}: {description}"
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\" for {entity_id}: {error}"
|
||||
},
|
||||
"set_setting": {
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\": {description}"
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{key}\": {error}"
|
||||
},
|
||||
"turn_on": {
|
||||
"message": "Error turning on {entity_id} ({key}): {description}"
|
||||
"message": "Error turning on {entity_id} ({key}): {error}"
|
||||
},
|
||||
"turn_off": {
|
||||
"message": "Error turning off {entity_id} ({key}): {description}"
|
||||
"message": "Error turning off {entity_id} ({key}): {error}"
|
||||
},
|
||||
"select_program": {
|
||||
"message": "Error selecting program {program}: {description}"
|
||||
"message": "Error selecting program {program}: {error}"
|
||||
},
|
||||
"start_program": {
|
||||
"message": "Error starting program {program}: {description}"
|
||||
"message": "Error starting program {program}: {error}"
|
||||
},
|
||||
"pause_program": {
|
||||
"message": "Error pausing program: {description}"
|
||||
"message": "Error pausing program: {error}"
|
||||
},
|
||||
"stop_program": {
|
||||
"message": "Error stopping program: {description}"
|
||||
"message": "Error stopping program: {error}"
|
||||
},
|
||||
"set_options_active_program": {
|
||||
"message": "Error setting options for the active program: {description}"
|
||||
"message": "Error setting options for the active program: {error}"
|
||||
},
|
||||
"set_options_selected_program": {
|
||||
"message": "Error setting options for the selected program: {description}"
|
||||
"message": "Error setting options for the selected program: {error}"
|
||||
},
|
||||
"execute_command": {
|
||||
"message": "Error executing command {command}: {description}"
|
||||
"message": "Error executing command {command}: {error}"
|
||||
},
|
||||
"power_on": {
|
||||
"message": "Error turning on {appliance_name}: {description}"
|
||||
"message": "Error turning on {appliance_name}: {error}"
|
||||
},
|
||||
"power_off": {
|
||||
"message": "Error turning off {appliance_name} with value \"{value}\": {description}"
|
||||
"message": "Error turning off {appliance_name} with value \"{value}\": {error}"
|
||||
},
|
||||
"turn_off_not_supported": {
|
||||
"message": "{appliance_name} does not support turning off or entering standby mode."
|
||||
},
|
||||
"unable_to_retrieve_turn_off": {
|
||||
"message": "Unable to turn off {appliance_name} because its support for turning off or entering standby mode could not be determined."
|
||||
},
|
||||
"fetch_api_error": {
|
||||
"message": "Error obtaining data from the API: {error}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
"""Provides a switch for Home Connect."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import EventKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import EnumerateAvailableProgram
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
|
@ -18,87 +19,83 @@ from homeassistant.helpers.issue_registry import (
|
|||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
ATTR_ALLOWED_VALUES,
|
||||
ATTR_CONSTRAINTS,
|
||||
ATTR_VALUE,
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
BSH_POWER_OFF,
|
||||
BSH_POWER_ON,
|
||||
BSH_POWER_STANDBY,
|
||||
BSH_POWER_STATE,
|
||||
DOMAIN,
|
||||
REFRIGERATION_DISPENSER,
|
||||
REFRIGERATION_SUPERMODEFREEZER,
|
||||
REFRIGERATION_SUPERMODEREFRIGERATOR,
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .entity import HomeConnectDevice, HomeConnectEntity
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SWITCHES = (
|
||||
SwitchEntityDescription(
|
||||
key=BSH_CHILD_LOCK_STATE,
|
||||
key=SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
translation_key="child_lock",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="ConsumerProducts.CoffeeMaker.Setting.CupWarmer",
|
||||
key=SettingKey.CONSUMER_PRODUCTS_COFFEE_MAKER_CUP_WARMER,
|
||||
translation_key="cup_warmer",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=REFRIGERATION_SUPERMODEFREEZER,
|
||||
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER,
|
||||
translation_key="freezer_super_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=REFRIGERATION_SUPERMODEREFRIGERATOR,
|
||||
key=SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_REFRIGERATOR,
|
||||
translation_key="refrigerator_super_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.EcoMode",
|
||||
key=SettingKey.REFRIGERATION_COMMON_ECO_MODE,
|
||||
translation_key="eco_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Cooking.Oven.Setting.SabbathMode",
|
||||
key=SettingKey.COOKING_OVEN_SABBATH_MODE,
|
||||
translation_key="sabbath_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.SabbathMode",
|
||||
key=SettingKey.REFRIGERATION_COMMON_SABBATH_MODE,
|
||||
translation_key="sabbath_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.VacationMode",
|
||||
key=SettingKey.REFRIGERATION_COMMON_VACATION_MODE,
|
||||
translation_key="vacation_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.FreshMode",
|
||||
key=SettingKey.REFRIGERATION_COMMON_FRESH_MODE,
|
||||
translation_key="fresh_mode",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=REFRIGERATION_DISPENSER,
|
||||
key=SettingKey.REFRIGERATION_COMMON_DISPENSER_ENABLED,
|
||||
translation_key="dispenser_enabled",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.Door.AssistantFridge",
|
||||
key=SettingKey.REFRIGERATION_COMMON_DOOR_ASSISTANT_FRIDGE,
|
||||
translation_key="door_assistant_fridge",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key="Refrigeration.Common.Setting.Door.AssistantFreezer",
|
||||
key=SettingKey.REFRIGERATION_COMMON_DOOR_ASSISTANT_FREEZER,
|
||||
translation_key="door_assistant_freezer",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
POWER_SWITCH_DESCRIPTION = SwitchEntityDescription(
|
||||
key=BSH_POWER_STATE,
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
translation_key="power",
|
||||
)
|
||||
|
||||
|
@ -110,29 +107,26 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect switch."""
|
||||
|
||||
def get_entities() -> list[SwitchEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[SwitchEntity] = []
|
||||
for device in entry.runtime_data.devices:
|
||||
if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
programs = device.appliance.get_programs_available()
|
||||
if programs:
|
||||
entities.extend(
|
||||
HomeConnectProgramSwitch(device, program)
|
||||
for program in programs
|
||||
)
|
||||
if BSH_POWER_STATE in device.appliance.status:
|
||||
entities.append(HomeConnectPowerSwitch(device))
|
||||
entities.extend(
|
||||
HomeConnectSwitch(device, description)
|
||||
for description in SWITCHES
|
||||
if description.key in device.appliance.status
|
||||
entities: list[SwitchEntity] = []
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities.extend(
|
||||
HomeConnectProgramSwitch(entry.runtime_data, appliance, program)
|
||||
for program in appliance.programs
|
||||
if program.key != ProgramKey.UNKNOWN
|
||||
)
|
||||
if SettingKey.BSH_COMMON_POWER_STATE in appliance.settings:
|
||||
entities.append(
|
||||
HomeConnectPowerSwitch(
|
||||
entry.runtime_data, appliance, POWER_SWITCH_DESCRIPTION
|
||||
)
|
||||
)
|
||||
entities.extend(
|
||||
HomeConnectSwitch(entry.runtime_data, appliance, description)
|
||||
for description in SWITCHES
|
||||
if description.key in appliance.settings
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
@ -140,11 +134,11 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
|||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on setting."""
|
||||
|
||||
_LOGGER.debug("Turning on %s", self.entity_description.key)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, self.entity_description.key, True
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=True,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_available = False
|
||||
|
@ -158,19 +152,15 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
|||
},
|
||||
) from err
|
||||
|
||||
self._attr_available = True
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off setting."""
|
||||
|
||||
_LOGGER.debug("Turning off %s", self.entity_description.key)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, self.entity_description.key, False
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=False,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("Error while trying to turn off: %s", err)
|
||||
self._attr_available = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
@ -182,38 +172,35 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
|||
},
|
||||
) from err
|
||||
|
||||
self._attr_available = True
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the switch's status."""
|
||||
|
||||
self._attr_is_on = self.device.appliance.status.get(
|
||||
self.entity_description.key, {}
|
||||
).get(ATTR_VALUE)
|
||||
self._attr_available = True
|
||||
_LOGGER.debug(
|
||||
"Updated %s, new state: %s",
|
||||
self.entity_description.key,
|
||||
self._attr_is_on,
|
||||
)
|
||||
self._attr_is_on = self.appliance.settings[SettingKey(self.bsh_key)].value
|
||||
|
||||
|
||||
class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
"""Switch class for Home Connect."""
|
||||
|
||||
def __init__(self, device: HomeConnectDevice, program_name: str) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
program: EnumerateAvailableProgram,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
desc = " ".join(["Program", program_name.split(".")[-1]])
|
||||
if device.appliance.type == "WasherDryer":
|
||||
desc = " ".join(["Program", program.key.split(".")[-1]])
|
||||
if appliance.info.type == "WasherDryer":
|
||||
desc = " ".join(
|
||||
["Program", program_name.split(".")[-3], program_name.split(".")[-1]]
|
||||
["Program", program.key.split(".")[-3], program.key.split(".")[-1]]
|
||||
)
|
||||
super().__init__(device, SwitchEntityDescription(key=program_name))
|
||||
self._attr_name = f"{device.appliance.name} {desc}"
|
||||
self._attr_unique_id = f"{device.appliance.haId}-{desc}"
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
|
||||
)
|
||||
self._attr_name = f"{appliance.info.name} {desc}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
|
||||
self._attr_has_entity_name = False
|
||||
self.program_name = program_name
|
||||
self.program = program
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
|
@ -266,10 +253,9 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
|||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start the program."""
|
||||
_LOGGER.debug("Tried to turn on program %s", self.program_name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.start_program, self.program_name
|
||||
await self.coordinator.client.start_program(
|
||||
self.appliance.info.ha_id, program_key=self.program.key
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -277,16 +263,14 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
|||
translation_key="start_program",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"program": self.program_name,
|
||||
"program": self.program.key,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop the program."""
|
||||
_LOGGER.debug("Tried to stop program %s", self.program_name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.device.appliance.stop_program)
|
||||
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
|
@ -295,48 +279,25 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
|||
**get_dict_from_home_connect_error(err),
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the switch's status."""
|
||||
state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {})
|
||||
if state.get(ATTR_VALUE) == self.program_name:
|
||||
self._attr_is_on = True
|
||||
else:
|
||||
self._attr_is_on = False
|
||||
_LOGGER.debug("Updated, new state: %s", self._attr_is_on)
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the switch's status based on if the program related to this entity is currently active."""
|
||||
event = self.appliance.events.get(EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM)
|
||||
self._attr_is_on = bool(event and event.value == self.program.key)
|
||||
|
||||
|
||||
class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
"""Power switch class for Home Connect."""
|
||||
|
||||
power_off_state: str | None
|
||||
|
||||
def __init__(self, device: HomeConnectDevice) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
device,
|
||||
POWER_SWITCH_DESCRIPTION,
|
||||
)
|
||||
if (
|
||||
power_state := device.appliance.status.get(BSH_POWER_STATE, {}).get(
|
||||
ATTR_VALUE
|
||||
)
|
||||
) and power_state in [BSH_POWER_OFF, BSH_POWER_STANDBY]:
|
||||
self.power_off_state = power_state
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Add the entity to the hass instance."""
|
||||
await super().async_added_to_hass()
|
||||
if not hasattr(self, "power_off_state"):
|
||||
await self.async_fetch_power_off_state()
|
||||
power_off_state: str | None | UndefinedType = UNDEFINED
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Switch the device on."""
|
||||
_LOGGER.debug("Tried to switch on %s", self.name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
value=BSH_POWER_ON,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_is_on = False
|
||||
|
@ -345,36 +306,36 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
|||
translation_key="power_on",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name,
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.appliance.info.name,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Switch the device off."""
|
||||
if not hasattr(self, "power_off_state"):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_retrieve_turn_off",
|
||||
translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name
|
||||
},
|
||||
)
|
||||
if self.power_off_state is UNDEFINED:
|
||||
await self.async_fetch_power_off_state()
|
||||
if self.power_off_state is UNDEFINED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_retrieve_turn_off",
|
||||
translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.appliance.info.name
|
||||
},
|
||||
)
|
||||
|
||||
if self.power_off_state is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_off_not_supported",
|
||||
translation_placeholders={
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.appliance.info.name
|
||||
},
|
||||
)
|
||||
_LOGGER.debug("tried to switch off %s", self.name)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
BSH_POWER_STATE,
|
||||
self.power_off_state,
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
value=self.power_off_state,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_is_on = True
|
||||
|
@ -383,46 +344,51 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
|||
translation_key="power_off",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.device.appliance.name,
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME: self.appliance.info.name,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: self.power_off_state,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the switch's status."""
|
||||
if (
|
||||
self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
|
||||
== BSH_POWER_ON
|
||||
):
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
power_state = self.appliance.settings[SettingKey.BSH_COMMON_POWER_STATE]
|
||||
value = cast(str, power_state.value)
|
||||
if value == BSH_POWER_ON:
|
||||
self._attr_is_on = True
|
||||
elif (
|
||||
hasattr(self, "power_off_state")
|
||||
and self.device.appliance.status.get(BSH_POWER_STATE, {}).get(ATTR_VALUE)
|
||||
== self.power_off_state
|
||||
isinstance(self.power_off_state, str)
|
||||
and self.power_off_state
|
||||
and value == self.power_off_state
|
||||
):
|
||||
self._attr_is_on = False
|
||||
elif self.power_off_state is UNDEFINED and value in [
|
||||
BSH_POWER_OFF,
|
||||
BSH_POWER_STANDBY,
|
||||
]:
|
||||
self.power_off_state = value
|
||||
self._attr_is_on = False
|
||||
else:
|
||||
self._attr_is_on = None
|
||||
_LOGGER.debug("Updated, new state: %s", self._attr_is_on)
|
||||
|
||||
async def async_fetch_power_off_state(self) -> None:
|
||||
"""Fetch the power off state."""
|
||||
try:
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self.device.appliance.get, f"/settings/{self.bsh_key}"
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("An error occurred: %s", err)
|
||||
return
|
||||
if not data or not (
|
||||
allowed_values := data.get(ATTR_CONSTRAINTS, {}).get(ATTR_ALLOWED_VALUES)
|
||||
):
|
||||
data = self.appliance.settings[SettingKey.BSH_COMMON_POWER_STATE]
|
||||
|
||||
if not data.constraints or not data.constraints.allowed_values:
|
||||
try:
|
||||
data = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("An error occurred fetching the power settings: %s", err)
|
||||
return
|
||||
if not data.constraints or not data.constraints.allowed_values:
|
||||
return
|
||||
|
||||
if BSH_POWER_OFF in allowed_values:
|
||||
if BSH_POWER_OFF in data.constraints.allowed_values:
|
||||
self.power_off_state = BSH_POWER_OFF
|
||||
elif BSH_POWER_STANDBY in allowed_values:
|
||||
elif BSH_POWER_STANDBY in data.constraints.allowed_values:
|
||||
self.power_off_state = BSH_POWER_STANDBY
|
||||
else:
|
||||
self.power_off_state = None
|
||||
|
|
|
@ -1,32 +1,30 @@
|
|||
"""Provides time enties for Home Connect."""
|
||||
|
||||
from datetime import time
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .const import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .coordinator import HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
TIME_ENTITIES = (
|
||||
TimeEntityDescription(
|
||||
key="BSH.Common.Setting.AlarmClock",
|
||||
key=SettingKey.BSH_COMMON_ALARM_CLOCK,
|
||||
translation_key="alarm_clock",
|
||||
),
|
||||
)
|
||||
|
@ -39,16 +37,14 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up the Home Connect switch."""
|
||||
|
||||
def get_entities() -> list[HomeConnectTimeEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
HomeConnectTimeEntity(device, description)
|
||||
async_add_entities(
|
||||
[
|
||||
HomeConnectTimeEntity(entry.runtime_data, appliance, description)
|
||||
for description in TIME_ENTITIES
|
||||
for device in entry.runtime_data.devices
|
||||
if description.key in device.appliance.status
|
||||
]
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
for appliance in entry.runtime_data.data.values()
|
||||
if description.key in appliance.settings
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def seconds_to_time(seconds: int) -> time:
|
||||
|
@ -68,17 +64,11 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
|||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Set the native value of the entity."""
|
||||
_LOGGER.debug(
|
||||
"Tried to set value %s to %s for %s",
|
||||
value,
|
||||
self.bsh_key,
|
||||
self.entity_id,
|
||||
)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.device.appliance.set_setting,
|
||||
self.bsh_key,
|
||||
time_to_seconds(value),
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=SettingKey(self.bsh_key),
|
||||
value=time_to_seconds(value),
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
|
@ -92,16 +82,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
|||
},
|
||||
) from err
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the Time setting status."""
|
||||
data = self.device.appliance.status.get(self.bsh_key)
|
||||
if data is None:
|
||||
_LOGGER.error("No value for %s", self.bsh_key)
|
||||
self._attr_native_value = None
|
||||
return
|
||||
seconds = data.get(ATTR_VALUE, None)
|
||||
if seconds is not None:
|
||||
self._attr_native_value = seconds_to_time(seconds)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
_LOGGER.debug("Updated, new value: %s", self._attr_native_value)
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||
self._attr_native_value = seconds_to_time(data.value)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
"""Utility functions for Home Connect."""
|
||||
|
||||
import re
|
||||
|
||||
from aiohomeconnect.model.error import HomeConnectApiError, HomeConnectError
|
||||
|
||||
RE_CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])|(?=\d)(?<=\D)")
|
||||
|
||||
|
||||
def get_dict_from_home_connect_error(
|
||||
err: HomeConnectError,
|
||||
) -> dict[str, str]:
|
||||
"""Return a translation string from a Home Connect error."""
|
||||
return {
|
||||
"error": str(err)
|
||||
if isinstance(err, HomeConnectApiError)
|
||||
else type(err).__name__
|
||||
}
|
||||
|
||||
|
||||
def bsh_key_to_translation_key(bsh_key: str) -> str:
|
||||
"""Convert a BSH key to a translation key format.
|
||||
|
||||
This function takes a BSH key, such as `Dishcare.Dishwasher.Program.Eco50`,
|
||||
and converts it to a translation key format, such as `dishcare_dishwasher_bsh_key_eco50`.
|
||||
"""
|
||||
return "_".join(
|
||||
RE_CAMEL_CASE.sub("_", split) for split in bsh_key.split(".")
|
||||
).lower()
|
|
@ -263,6 +263,9 @@ aioharmony==0.4.1
|
|||
# homeassistant.components.hassio
|
||||
aiohasupervisor==0.2.2b6
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.12.1
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.7
|
||||
|
||||
|
@ -1148,9 +1151,6 @@ home-assistant-frontend==20250129.0
|
|||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.1.28
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.8.0
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.1.7
|
||||
|
||||
|
|
|
@ -248,6 +248,9 @@ aioharmony==0.4.1
|
|||
# homeassistant.components.hassio
|
||||
aiohasupervisor==0.2.2b6
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.12.1
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.7
|
||||
|
||||
|
@ -977,9 +980,6 @@ home-assistant-frontend==20250129.0
|
|||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.1.28
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.8.0
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.1.7
|
||||
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
"""Test fixtures for home_connect."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable
|
||||
import copy
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfAvailablePrograms,
|
||||
ArrayOfEvents,
|
||||
ArrayOfHomeAppliances,
|
||||
ArrayOfSettings,
|
||||
ArrayOfStatus,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
Option,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectApiError, HomeConnectError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.home_connect import update_all_devices
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -20,12 +34,17 @@ from homeassistant.setup import async_setup_component
|
|||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
MOCK_APPLIANCES_PROPERTIES = {
|
||||
x["name"]: x
|
||||
for x in load_json_object_fixture("home_connect/appliances.json")["data"][
|
||||
"homeappliances"
|
||||
]
|
||||
}
|
||||
MOCK_APPLIANCES = ArrayOfHomeAppliances.from_dict(
|
||||
load_json_object_fixture("home_connect/appliances.json")["data"]
|
||||
)
|
||||
MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture(
|
||||
"home_connect/programs-available.json"
|
||||
)
|
||||
MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
|
||||
MOCK_STATUS = ArrayOfStatus.from_dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"]
|
||||
)
|
||||
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
|
@ -102,32 +121,23 @@ def platforms() -> list[Platform]:
|
|||
return []
|
||||
|
||||
|
||||
async def bypass_throttle(hass: HomeAssistant, config_entry: MockConfigEntry):
|
||||
"""Add kwarg to disable throttle."""
|
||||
await update_all_devices(hass, config_entry, no_throttle=True)
|
||||
|
||||
|
||||
@pytest.fixture(name="bypass_throttle")
|
||||
def mock_bypass_throttle() -> Generator[None]:
|
||||
"""Fixture to bypass the throttle decorator in __init__."""
|
||||
with patch(
|
||||
"homeassistant.components.home_connect.update_all_devices",
|
||||
side_effect=bypass_throttle,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="integration_setup")
|
||||
async def mock_integration_setup(
|
||||
hass: HomeAssistant,
|
||||
platforms: list[Platform],
|
||||
config_entry: MockConfigEntry,
|
||||
) -> Callable[[], Awaitable[bool]]:
|
||||
) -> Callable[[MagicMock], Awaitable[bool]]:
|
||||
"""Fixture to set up the integration."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
async def run() -> bool:
|
||||
with patch("homeassistant.components.home_connect.PLATFORMS", platforms):
|
||||
async def run(client: MagicMock) -> bool:
|
||||
with (
|
||||
patch("homeassistant.components.home_connect.PLATFORMS", platforms),
|
||||
patch(
|
||||
"homeassistant.components.home_connect.HomeConnectClient"
|
||||
) as client_mock,
|
||||
):
|
||||
client_mock.return_value = client
|
||||
result = await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return result
|
||||
|
@ -135,125 +145,205 @@ async def mock_integration_setup(
|
|||
return run
|
||||
|
||||
|
||||
@pytest.fixture(name="get_appliances")
|
||||
def mock_get_appliances() -> Generator[MagicMock]:
|
||||
"""Mock ConfigEntryAuth parent (HomeAssistantAPI) method."""
|
||||
with patch(
|
||||
"homeassistant.components.home_connect.api.ConfigEntryAuth.get_appliances",
|
||||
) as mock:
|
||||
yield mock
|
||||
def _get_set_program_side_effect(
|
||||
event_queue: asyncio.Queue[list[EventMessage]], event_key: EventKey
|
||||
):
|
||||
"""Set program side effect."""
|
||||
|
||||
async def set_program_side_effect(ha_id: str, *_, **kwargs) -> None:
|
||||
await event_queue.put(
|
||||
[
|
||||
EventMessage(
|
||||
ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=str(kwargs["program_key"]),
|
||||
),
|
||||
*[
|
||||
Event(
|
||||
key=(option_event := EventKey(option.key)),
|
||||
raw_key=option_event.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=str(option.key),
|
||||
)
|
||||
for option in cast(
|
||||
list[Option], kwargs.get("options", [])
|
||||
)
|
||||
],
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
return set_program_side_effect
|
||||
|
||||
|
||||
@pytest.fixture(name="appliance")
|
||||
def mock_appliance(request: pytest.FixtureRequest) -> MagicMock:
|
||||
def _get_set_key_value_side_effect(
|
||||
event_queue: asyncio.Queue[list[EventMessage]], parameter_key: str
|
||||
):
|
||||
"""Set program options side effect."""
|
||||
|
||||
async def set_key_value_side_effect(ha_id: str, *_, **kwargs) -> None:
|
||||
event_key = EventKey(kwargs[parameter_key])
|
||||
await event_queue.put(
|
||||
[
|
||||
EventMessage(
|
||||
ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=kwargs["value"],
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
return set_key_value_side_effect
|
||||
|
||||
|
||||
async def _get_available_programs_side_effect(ha_id: str) -> ArrayOfAvailablePrograms:
|
||||
"""Get available 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 ArrayOfAvailablePrograms.from_dict(MOCK_PROGRAMS[appliance_type]["data"])
|
||||
|
||||
|
||||
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": []})
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def mock_client(request: pytest.FixtureRequest) -> MagicMock:
|
||||
"""Fixture to mock Client from HomeConnect."""
|
||||
|
||||
mock = MagicMock(
|
||||
autospec=HomeConnectClient,
|
||||
)
|
||||
|
||||
event_queue: asyncio.Queue[list[EventMessage]] = asyncio.Queue()
|
||||
|
||||
async def add_events(events: list[EventMessage]) -> None:
|
||||
await event_queue.put(events)
|
||||
|
||||
mock.add_events = add_events
|
||||
|
||||
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=MOCK_APPLIANCES)
|
||||
mock.stream_all_events = stream_all_events
|
||||
mock.start_program = AsyncMock(
|
||||
side_effect=_get_set_program_side_effect(
|
||||
event_queue, EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM
|
||||
)
|
||||
)
|
||||
mock.set_selected_program = AsyncMock(
|
||||
side_effect=_get_set_program_side_effect(
|
||||
event_queue, EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM
|
||||
),
|
||||
)
|
||||
mock.set_active_program_option = AsyncMock(
|
||||
side_effect=_get_set_key_value_side_effect(event_queue, "option_key"),
|
||||
)
|
||||
mock.set_selected_program_option = AsyncMock(
|
||||
side_effect=_get_set_key_value_side_effect(event_queue, "option_key"),
|
||||
)
|
||||
mock.set_setting = AsyncMock(
|
||||
side_effect=_get_set_key_value_side_effect(event_queue, "setting_key"),
|
||||
)
|
||||
mock.get_settings = AsyncMock(side_effect=_get_settings_side_effect)
|
||||
mock.get_status = AsyncMock(return_value=copy.deepcopy(MOCK_STATUS))
|
||||
mock.get_available_programs = AsyncMock(
|
||||
side_effect=_get_available_programs_side_effect
|
||||
)
|
||||
mock.put_command = AsyncMock()
|
||||
|
||||
mock.side_effect = mock
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture(name="client_with_exception")
|
||||
def mock_client_with_exception(request: pytest.FixtureRequest) -> MagicMock:
|
||||
"""Fixture to mock Client from HomeConnect that raise exceptions."""
|
||||
mock = MagicMock(
|
||||
autospec=HomeConnectClient,
|
||||
)
|
||||
|
||||
exception = HomeConnectError()
|
||||
if hasattr(request, "param") and request.param:
|
||||
exception = request.param
|
||||
|
||||
event_queue: asyncio.Queue[list[EventMessage]] = asyncio.Queue()
|
||||
|
||||
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=MOCK_APPLIANCES)
|
||||
mock.stream_all_events = stream_all_events
|
||||
|
||||
mock.start_program = AsyncMock(side_effect=exception)
|
||||
mock.stop_program = AsyncMock(side_effect=exception)
|
||||
mock.get_available_programs = AsyncMock(side_effect=exception)
|
||||
mock.set_selected_program = AsyncMock(side_effect=exception)
|
||||
mock.set_active_program_option = AsyncMock(side_effect=exception)
|
||||
mock.set_selected_program_option = AsyncMock(side_effect=exception)
|
||||
mock.set_setting = AsyncMock(side_effect=exception)
|
||||
mock.get_settings = AsyncMock(side_effect=exception)
|
||||
mock.get_setting = AsyncMock(side_effect=exception)
|
||||
mock.get_status = AsyncMock(side_effect=exception)
|
||||
mock.get_available_programs = AsyncMock(side_effect=exception)
|
||||
mock.put_command = AsyncMock(side_effect=exception)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture(name="appliance_ha_id")
|
||||
def mock_appliance_ha_id(request: pytest.FixtureRequest) -> str:
|
||||
"""Fixture to mock Appliance."""
|
||||
app = "Washer"
|
||||
if hasattr(request, "param") and request.param:
|
||||
app = request.param
|
||||
|
||||
mock = MagicMock(
|
||||
autospec=HomeConnectAppliance,
|
||||
**MOCK_APPLIANCES_PROPERTIES.get(app),
|
||||
)
|
||||
mock.name = app
|
||||
type(mock).status = PropertyMock(return_value={})
|
||||
mock.get.return_value = {}
|
||||
mock.get_programs_available.return_value = []
|
||||
mock.get_status.return_value = {}
|
||||
mock.get_settings.return_value = {}
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture(name="problematic_appliance")
|
||||
def mock_problematic_appliance(request: pytest.FixtureRequest) -> Mock:
|
||||
"""Fixture to mock a problematic Appliance."""
|
||||
app = "Washer"
|
||||
if hasattr(request, "param") and request.param:
|
||||
app = request.param
|
||||
|
||||
mock = Mock(
|
||||
autospec=HomeConnectAppliance,
|
||||
**MOCK_APPLIANCES_PROPERTIES.get(app),
|
||||
)
|
||||
mock.name = app
|
||||
type(mock).status = PropertyMock(return_value={})
|
||||
mock.get.side_effect = HomeConnectError
|
||||
mock.get_programs_active.side_effect = HomeConnectError
|
||||
mock.get_programs_available.side_effect = HomeConnectError
|
||||
mock.start_program.side_effect = HomeConnectError
|
||||
mock.select_program.side_effect = HomeConnectError
|
||||
mock.pause_program.side_effect = HomeConnectError
|
||||
mock.stop_program.side_effect = HomeConnectError
|
||||
mock.set_options_active_program.side_effect = HomeConnectError
|
||||
mock.set_options_selected_program.side_effect = HomeConnectError
|
||||
mock.get_status.side_effect = HomeConnectError
|
||||
mock.get_settings.side_effect = HomeConnectError
|
||||
mock.set_setting.side_effect = HomeConnectError
|
||||
mock.set_setting.side_effect = HomeConnectError
|
||||
mock.execute_command.side_effect = HomeConnectError
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
def get_all_appliances():
|
||||
"""Return a list of `HomeConnectAppliance` instances for all appliances."""
|
||||
|
||||
appliances = {}
|
||||
|
||||
data = load_json_object_fixture("home_connect/appliances.json").get("data")
|
||||
programs_active = load_json_object_fixture("home_connect/programs-active.json")
|
||||
programs_available = load_json_object_fixture(
|
||||
"home_connect/programs-available.json"
|
||||
)
|
||||
|
||||
def listen_callback(mock, callback):
|
||||
callback["callback"](mock)
|
||||
|
||||
for home_appliance in data["homeappliances"]:
|
||||
api_status = load_json_object_fixture("home_connect/status.json")
|
||||
api_settings = load_json_object_fixture("home_connect/settings.json")
|
||||
|
||||
ha_id = home_appliance["haId"]
|
||||
ha_type = home_appliance["type"]
|
||||
|
||||
appliance = MagicMock(spec=HomeConnectAppliance, **home_appliance)
|
||||
appliance.name = home_appliance["name"]
|
||||
appliance.listen_events.side_effect = (
|
||||
lambda app=appliance, **x: listen_callback(app, x)
|
||||
)
|
||||
appliance.get_programs_active.return_value = programs_active.get(
|
||||
ha_type, {}
|
||||
).get("data", {})
|
||||
appliance.get_programs_available.return_value = [
|
||||
program["key"]
|
||||
for program in programs_available.get(ha_type, {})
|
||||
.get("data", {})
|
||||
.get("programs", [])
|
||||
]
|
||||
appliance.get_status.return_value = HomeConnectAppliance.json2dict(
|
||||
api_status.get("data", {}).get("status", [])
|
||||
)
|
||||
appliance.get_settings.return_value = HomeConnectAppliance.json2dict(
|
||||
api_settings.get(ha_type, {}).get("data", {}).get("settings", [])
|
||||
)
|
||||
setattr(appliance, "status", {})
|
||||
appliance.status.update(appliance.get_status.return_value)
|
||||
appliance.status.update(appliance.get_settings.return_value)
|
||||
appliance.set_setting.side_effect = (
|
||||
lambda x, y, appliance=appliance: appliance.status.update({x: {"value": y}})
|
||||
)
|
||||
appliance.start_program.side_effect = (
|
||||
lambda x, appliance=appliance: appliance.status.update(
|
||||
{"BSH.Common.Root.ActiveProgram": {"value": x}}
|
||||
)
|
||||
)
|
||||
appliance.stop_program.side_effect = (
|
||||
lambda appliance=appliance: appliance.status.update(
|
||||
{"BSH.Common.Root.ActiveProgram": {}}
|
||||
)
|
||||
)
|
||||
|
||||
appliances[ha_id] = appliance
|
||||
|
||||
return list(appliances.values())
|
||||
for appliance in MOCK_APPLIANCES.homeappliances:
|
||||
if appliance.type == app:
|
||||
return appliance.ha_id
|
||||
raise ValueError(f"Appliance {app} not found")
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
"Dishwasher": {
|
||||
"data": {
|
||||
"settings": [
|
||||
{
|
||||
"key": "BSH.Common.Setting.ChildLock",
|
||||
"value": false,
|
||||
"type": "Boolean"
|
||||
},
|
||||
{
|
||||
"key": "BSH.Common.Setting.AmbientLightEnabled",
|
||||
"value": true,
|
||||
|
@ -26,7 +31,13 @@
|
|||
{
|
||||
"key": "BSH.Common.Setting.PowerState",
|
||||
"value": "BSH.Common.EnumType.PowerState.On",
|
||||
"type": "BSH.Common.EnumType.PowerState"
|
||||
"type": "BSH.Common.EnumType.PowerState",
|
||||
"constraints": {
|
||||
"allowedvalues": [
|
||||
"BSH.Common.EnumType.PowerState.On",
|
||||
"BSH.Common.EnumType.PowerState.Off"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "BSH.Common.Setting.ChildLock",
|
||||
|
@ -92,6 +103,11 @@
|
|||
"key": "BSH.Common.Setting.PowerState",
|
||||
"value": "BSH.Common.EnumType.PowerState.On",
|
||||
"type": "BSH.Common.EnumType.PowerState"
|
||||
},
|
||||
{
|
||||
"key": "BSH.Common.Setting.AlarmClock",
|
||||
"value": 0,
|
||||
"type": "Integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -154,6 +170,12 @@
|
|||
"max": 100,
|
||||
"access": "readWrite"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
|
||||
"value": 8,
|
||||
"unit": "°C",
|
||||
"type": "Double"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,255 +2,209 @@
|
|||
# name: test_async_get_config_entry_diagnostics
|
||||
dict({
|
||||
'BOSCH-000000000-000000000000': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/00',
|
||||
'ha_id': 'BOSCH-000000000-000000000000',
|
||||
'name': 'DNE',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'DNE',
|
||||
'vib': 'HCS000000',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000001': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/01',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000001',
|
||||
'name': 'WasherDryer',
|
||||
'programs': list([
|
||||
'LaundryCare.WasherDryer.Program.Mix',
|
||||
'LaundryCare.Washer.Option.Temperature',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'WasherDryer',
|
||||
'vib': 'HCS000001',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000002': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/02',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000002',
|
||||
'name': 'Refrigerator',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Refrigerator',
|
||||
'vib': 'HCS000002',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000003': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/03',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000003',
|
||||
'name': 'Freezer',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Freezer',
|
||||
'vib': 'HCS000003',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000004': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/04',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000004',
|
||||
'name': 'Hood',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': dict({
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightColor': dict({
|
||||
'type': 'BSH.Common.EnumType.AmbientLightColor',
|
||||
'value': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': dict({
|
||||
'type': 'String',
|
||||
'value': '#4a88f8',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightEnabled': dict({
|
||||
'type': 'Boolean',
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Setting.ColorTemperature': dict({
|
||||
'type': 'BSH.Common.EnumType.ColorTemperature',
|
||||
'value': 'Cooking.Hood.EnumType.ColorTemperature.warmToNeutral',
|
||||
}),
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Cooking.Common.Setting.Lighting': dict({
|
||||
'type': 'Boolean',
|
||||
'value': True,
|
||||
}),
|
||||
'Cooking.Common.Setting.LightingBrightness': dict({
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'Cooking.Hood.Setting.ColorTemperaturePercent': dict({
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': 70,
|
||||
'BSH.Common.Setting.AmbientLightColor': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': '#4a88f8',
|
||||
'BSH.Common.Setting.AmbientLightEnabled': True,
|
||||
'Cooking.Common.Setting.Lighting': True,
|
||||
'Cooking.Common.Setting.LightingBrightness': 70,
|
||||
'Cooking.Hood.Setting.ColorTemperaturePercent': 70,
|
||||
'unknown': 'Cooking.Hood.EnumType.ColorTemperature.warmToNeutral',
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Hood',
|
||||
'vib': 'HCS000004',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000005': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/05',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000005',
|
||||
'name': 'Hob',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Hob',
|
||||
'vib': 'HCS000005',
|
||||
}),
|
||||
'BOSCH-HCS000000-D00000000006': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS000000/06',
|
||||
'ha_id': 'BOSCH-HCS000000-D00000000006',
|
||||
'name': 'CookProcessor',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'CookProcessor',
|
||||
'vib': 'HCS000006',
|
||||
}),
|
||||
'BOSCH-HCS01OVN1-43E0065FE245': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS01OVN1/03',
|
||||
'ha_id': 'BOSCH-HCS01OVN1-43E0065FE245',
|
||||
'name': 'Oven',
|
||||
'programs': list([
|
||||
'Cooking.Oven.Program.HeatingMode.HotAir',
|
||||
'Cooking.Oven.Program.HeatingMode.TopBottomHeating',
|
||||
'Cooking.Oven.Program.HeatingMode.PizzaSetting',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Root.ActiveProgram': dict({
|
||||
'value': 'Cooking.Oven.Program.HeatingMode.HotAir',
|
||||
}),
|
||||
'BSH.Common.Setting.PowerState': dict({
|
||||
'type': 'BSH.Common.EnumType.PowerState',
|
||||
'value': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.AlarmClock': 0,
|
||||
'BSH.Common.Setting.PowerState': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Oven',
|
||||
'vib': 'HCS01OVN1',
|
||||
}),
|
||||
'BOSCH-HCS04DYR1-831694AE3C5A': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS04DYR1/03',
|
||||
'ha_id': 'BOSCH-HCS04DYR1-831694AE3C5A',
|
||||
'name': 'Dryer',
|
||||
'programs': list([
|
||||
'LaundryCare.Dryer.Program.Cotton',
|
||||
'LaundryCare.Dryer.Program.Synthetic',
|
||||
'LaundryCare.Dryer.Program.Mix',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Dryer',
|
||||
'vib': 'HCS04DYR1',
|
||||
}),
|
||||
'BOSCH-HCS06COM1-D70390681C2C': dict({
|
||||
'brand': 'BOSCH',
|
||||
'connected': True,
|
||||
'e_number': 'HCS06COM1/03',
|
||||
'ha_id': 'BOSCH-HCS06COM1-D70390681C2C',
|
||||
'name': 'CoffeeMaker',
|
||||
'programs': list([
|
||||
'ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso',
|
||||
'ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato',
|
||||
|
@ -259,26 +213,24 @@
|
|||
'ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato',
|
||||
'ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'CoffeeMaker',
|
||||
'vib': 'HCS06COM1',
|
||||
}),
|
||||
'SIEMENS-HCS02DWH1-6BE58C26DCC1': dict({
|
||||
'brand': 'SIEMENS',
|
||||
'connected': True,
|
||||
'e_number': 'HCS02DWH1/03',
|
||||
'ha_id': 'SIEMENS-HCS02DWH1-6BE58C26DCC1',
|
||||
'name': 'Dishwasher',
|
||||
'programs': list([
|
||||
'Dishcare.Dishwasher.Program.Auto1',
|
||||
'Dishcare.Dishwasher.Program.Auto2',
|
||||
|
@ -286,51 +238,30 @@
|
|||
'Dishcare.Dishwasher.Program.Eco50',
|
||||
'Dishcare.Dishwasher.Program.Quick45',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': dict({
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightColor': dict({
|
||||
'type': 'BSH.Common.EnumType.AmbientLightColor',
|
||||
'value': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': dict({
|
||||
'type': 'String',
|
||||
'value': '#4a88f8',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightEnabled': dict({
|
||||
'type': 'Boolean',
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Setting.ChildLock': dict({
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'BSH.Common.Setting.PowerState': dict({
|
||||
'type': 'BSH.Common.EnumType.PowerState',
|
||||
'value': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': 70,
|
||||
'BSH.Common.Setting.AmbientLightColor': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': '#4a88f8',
|
||||
'BSH.Common.Setting.AmbientLightEnabled': True,
|
||||
'BSH.Common.Setting.ChildLock': False,
|
||||
'BSH.Common.Setting.PowerState': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Dishwasher',
|
||||
'vib': 'HCS02DWH1',
|
||||
}),
|
||||
'SIEMENS-HCS03WCH1-7BC6383CF794': dict({
|
||||
'brand': 'SIEMENS',
|
||||
'connected': True,
|
||||
'e_number': 'HCS03WCH1/03',
|
||||
'ha_id': 'SIEMENS-HCS03WCH1-7BC6383CF794',
|
||||
'name': 'Washer',
|
||||
'programs': list([
|
||||
'LaundryCare.Washer.Program.Cotton',
|
||||
'LaundryCare.Washer.Program.EasyCare',
|
||||
|
@ -338,97 +269,55 @@
|
|||
'LaundryCare.Washer.Program.DelicatesSilk',
|
||||
'LaundryCare.Washer.Program.Wool',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Root.ActiveProgram': dict({
|
||||
'value': 'BSH.Common.Root.ActiveProgram',
|
||||
}),
|
||||
'BSH.Common.Setting.ChildLock': dict({
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'BSH.Common.Setting.PowerState': dict({
|
||||
'type': 'BSH.Common.EnumType.PowerState',
|
||||
'value': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.ChildLock': False,
|
||||
'BSH.Common.Setting.PowerState': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Washer',
|
||||
'vib': 'HCS03WCH1',
|
||||
}),
|
||||
'SIEMENS-HCS05FRF1-304F4F9E541D': dict({
|
||||
'brand': 'SIEMENS',
|
||||
'connected': True,
|
||||
'e_number': 'HCS05FRF1/03',
|
||||
'ha_id': 'SIEMENS-HCS05FRF1-304F4F9E541D',
|
||||
'name': 'FridgeFreezer',
|
||||
'programs': list([
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Setting.Dispenser.Enabled': dict({
|
||||
'constraints': dict({
|
||||
'access': 'readWrite',
|
||||
}),
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'Refrigeration.Common.Setting.Light.External.Brightness': dict({
|
||||
'constraints': dict({
|
||||
'access': 'readWrite',
|
||||
'max': 100,
|
||||
'min': 0,
|
||||
}),
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'Refrigeration.Common.Setting.Light.External.Power': dict({
|
||||
'type': 'Boolean',
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'Refrigeration.FridgeFreezer.Setting.SuperModeFreezer': dict({
|
||||
'constraints': dict({
|
||||
'access': 'readWrite',
|
||||
}),
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator': dict({
|
||||
'constraints': dict({
|
||||
'access': 'readWrite',
|
||||
}),
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'settings': dict({
|
||||
'Refrigeration.Common.Setting.Dispenser.Enabled': False,
|
||||
'Refrigeration.Common.Setting.Light.External.Brightness': 70,
|
||||
'Refrigeration.Common.Setting.Light.External.Power': True,
|
||||
'Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator': 8,
|
||||
'Refrigeration.FridgeFreezer.Setting.SuperModeFreezer': False,
|
||||
'Refrigeration.FridgeFreezer.Setting.SuperModeRefrigerator': False,
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'FridgeFreezer',
|
||||
'vib': 'HCS05FRF1',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_async_get_device_diagnostics
|
||||
dict({
|
||||
'brand': 'SIEMENS',
|
||||
'connected': True,
|
||||
'e_number': 'HCS02DWH1/03',
|
||||
'ha_id': 'SIEMENS-HCS02DWH1-6BE58C26DCC1',
|
||||
'name': 'Dishwasher',
|
||||
'programs': list([
|
||||
'Dishcare.Dishwasher.Program.Auto1',
|
||||
'Dishcare.Dishwasher.Program.Auto2',
|
||||
|
@ -436,47 +325,22 @@
|
|||
'Dishcare.Dishwasher.Program.Eco50',
|
||||
'Dishcare.Dishwasher.Program.Quick45',
|
||||
]),
|
||||
'status': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': dict({
|
||||
'type': 'Double',
|
||||
'unit': '%',
|
||||
'value': 70,
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightColor': dict({
|
||||
'type': 'BSH.Common.EnumType.AmbientLightColor',
|
||||
'value': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': dict({
|
||||
'type': 'String',
|
||||
'value': '#4a88f8',
|
||||
}),
|
||||
'BSH.Common.Setting.AmbientLightEnabled': dict({
|
||||
'type': 'Boolean',
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Setting.ChildLock': dict({
|
||||
'type': 'Boolean',
|
||||
'value': False,
|
||||
}),
|
||||
'BSH.Common.Setting.PowerState': dict({
|
||||
'type': 'BSH.Common.EnumType.PowerState',
|
||||
'value': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'BSH.Common.Status.DoorState': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
}),
|
||||
'BSH.Common.Status.OperationState': dict({
|
||||
'value': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlActive': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': dict({
|
||||
'value': True,
|
||||
}),
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': dict({
|
||||
'value': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'settings': dict({
|
||||
'BSH.Common.Setting.AmbientLightBrightness': 70,
|
||||
'BSH.Common.Setting.AmbientLightColor': 'BSH.Common.EnumType.AmbientLightColor.Color43',
|
||||
'BSH.Common.Setting.AmbientLightCustomColor': '#4a88f8',
|
||||
'BSH.Common.Setting.AmbientLightEnabled': True,
|
||||
'BSH.Common.Setting.ChildLock': False,
|
||||
'BSH.Common.Setting.PowerState': 'BSH.Common.EnumType.PowerState.On',
|
||||
}),
|
||||
'status': dict({
|
||||
'BSH.Common.Status.DoorState': 'BSH.Common.EnumType.DoorState.Closed',
|
||||
'BSH.Common.Status.OperationState': 'BSH.Common.EnumType.OperationState.Ready',
|
||||
'BSH.Common.Status.RemoteControlActive': True,
|
||||
'BSH.Common.Status.RemoteControlStartAllowed': True,
|
||||
'Refrigeration.Common.Status.Door.Refrigerator': 'BSH.Common.EnumType.DoorState.Open',
|
||||
}),
|
||||
'type': 'Dishwasher',
|
||||
'vib': 'HCS02DWH1',
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
"""Tests for home_connect binary_sensor entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectAPI
|
||||
from aiohomeconnect.model import ArrayOfEvents, Event, EventKey, EventMessage, EventType
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation, script
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_DOOR_STATE,
|
||||
BSH_DOOR_STATE_CLOSED,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
DOMAIN,
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED,
|
||||
REFRIGERATION_STATUS_DOOR_OPEN,
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -35,123 +32,166 @@ def platforms() -> list[str]:
|
|||
return [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_binary_sensors(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test binary sensor entities."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("state", "expected"),
|
||||
("value", "expected"),
|
||||
[
|
||||
(BSH_DOOR_STATE_CLOSED, "off"),
|
||||
(BSH_DOOR_STATE_LOCKED, "off"),
|
||||
(BSH_DOOR_STATE_OPEN, "on"),
|
||||
("", "unavailable"),
|
||||
("", STATE_UNKNOWN),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_binary_sensors_door_states(
|
||||
appliance_ha_id: str,
|
||||
expected: str,
|
||||
state: str,
|
||||
value: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Tests for Appliance door states."""
|
||||
entity_id = "binary_sensor.washer_door"
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
appliance.status.update({BSH_DOOR_STATE: {"value": state}})
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
await async_update_entity(hass, entity_id)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.STATUS,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
raw_key=EventKey.BSH_COMMON_STATUS_DOOR_STATE.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=value,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status_key", "event_value_update", "expected", "appliance"),
|
||||
("entity_id", "event_key", "event_value_update", "expected", "appliance_ha_id"),
|
||||
[
|
||||
(
|
||||
"binary_sensor.washer_remote_control",
|
||||
EventKey.BSH_COMMON_STATUS_REMOTE_CONTROL_ACTIVE,
|
||||
False,
|
||||
STATE_OFF,
|
||||
"Washer",
|
||||
),
|
||||
(
|
||||
"binary_sensor.washer_remote_control",
|
||||
EventKey.BSH_COMMON_STATUS_REMOTE_CONTROL_ACTIVE,
|
||||
True,
|
||||
STATE_ON,
|
||||
"Washer",
|
||||
),
|
||||
(
|
||||
"binary_sensor.washer_remote_control",
|
||||
EventKey.BSH_COMMON_STATUS_REMOTE_CONTROL_ACTIVE,
|
||||
"",
|
||||
STATE_UNKNOWN,
|
||||
"Washer",
|
||||
),
|
||||
(
|
||||
"binary_sensor.fridgefreezer_refrigerator_door",
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
EventKey.REFRIGERATION_COMMON_STATUS_DOOR_REFRIGERATOR,
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED,
|
||||
STATE_OFF,
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"binary_sensor.fridgefreezer_refrigerator_door",
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
EventKey.REFRIGERATION_COMMON_STATUS_DOOR_REFRIGERATOR,
|
||||
REFRIGERATION_STATUS_DOOR_OPEN,
|
||||
STATE_ON,
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"binary_sensor.fridgefreezer_refrigerator_door",
|
||||
REFRIGERATION_STATUS_DOOR_REFRIGERATOR,
|
||||
EventKey.REFRIGERATION_COMMON_STATUS_DOOR_REFRIGERATOR,
|
||||
"",
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
"FridgeFreezer",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_bianry_sensors_fridge_door_states(
|
||||
async def test_binary_sensors_functionality(
|
||||
entity_id: str,
|
||||
status_key: str,
|
||||
event_key: EventKey,
|
||||
event_value_update: str,
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
expected: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Tests for Home Connect Fridge appliance door states."""
|
||||
appliance.status.update(
|
||||
HomeConnectAPI.json2dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"]["status"]
|
||||
)
|
||||
)
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
appliance.status.update({status_key: {"value": event_value_update}})
|
||||
await async_update_entity(hass, entity_id)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.STATUS,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=event_value_update,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, expected)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_create_issue(
|
||||
hass: HomeAssistant,
|
||||
appliance: Mock,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test we create an issue when an automation or script is using a deprecated entity."""
|
||||
entity_id = "binary_sensor.washer_door"
|
||||
get_appliances.return_value = [appliance]
|
||||
issue_id = f"deprecated_binary_common_door_sensor_{entity_id}"
|
||||
|
||||
assert await async_setup_component(
|
||||
|
@ -189,8 +229,7 @@ async def test_create_issue(
|
|||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
appliance.status.update({BSH_DOOR_STATE: {"value": BSH_DOOR_STATE_OPEN}})
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
|
@ -10,11 +11,7 @@ from homeassistant.components.application_credentials import (
|
|||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import (
|
||||
DOMAIN,
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
|
|
@ -0,0 +1,367 @@
|
|||
"""Test for Home Connect coordinator."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfEvents,
|
||||
ArrayOfSettings,
|
||||
ArrayOfStatus,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
Status,
|
||||
StatusKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import (
|
||||
EventStreamInterruptedError,
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
HomeConnectRequestError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
BSH_POWER_OFF,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntries, ConfigEntryState
|
||||
from homeassistant.const import EVENT_STATE_REPORTED, Platform
|
||||
from homeassistant.core import (
|
||||
Event as HassEvent,
|
||||
EventStateReportedData,
|
||||
HomeAssistant,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def test_coordinator_update(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the coordinator can update."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_coordinator_update_failing_get_appliances(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the coordinator raises ConfigEntryNotReady when it fails to get appliances."""
|
||||
client_with_exception.get_home_appliances.return_value = None
|
||||
client_with_exception.get_home_appliances.side_effect = HomeConnectError()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_coordinator_update_failing_get_settings_status(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test that although is not possible to get settings and status, the config entry is loaded.
|
||||
|
||||
This is for cases where some appliances are reachable and some are not in the same configuration entry.
|
||||
"""
|
||||
# Get home appliances does pass at client_with_exception.get_home_appliances mock, so no need to mock it again
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["Dishwasher"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("event_type", "event_key", "event_value", "entity_id"),
|
||||
[
|
||||
(
|
||||
EventType.STATUS,
|
||||
EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
"sensor.dishwasher_door",
|
||||
),
|
||||
(
|
||||
EventType.NOTIFY,
|
||||
EventKey.BSH_COMMON_SETTING_POWER_STATE,
|
||||
BSH_POWER_OFF,
|
||||
"switch.dishwasher_power",
|
||||
),
|
||||
(
|
||||
EventType.EVENT,
|
||||
EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
"sensor.dishwasher_salt_nearly_empty",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_event_listener(
|
||||
event_type: EventType,
|
||||
event_key: EventKey,
|
||||
event_value: str,
|
||||
entity_id: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that the event listener works."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
event_message = EventMessage(
|
||||
appliance_ha_id,
|
||||
event_type,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=event_value,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
await client.add_events([event_message])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
new_state = hass.states.get(entity_id)
|
||||
assert new_state
|
||||
assert new_state.state != state.state
|
||||
|
||||
# Following, we are gonna check that the listeners are clean up correctly
|
||||
new_entity_id = entity_id + "_new"
|
||||
listener = MagicMock()
|
||||
|
||||
@callback
|
||||
def listener_callback(event: HassEvent[EventStateReportedData]) -> None:
|
||||
listener(event.data["entity_id"])
|
||||
|
||||
@callback
|
||||
def event_filter(_: EventStateReportedData) -> bool:
|
||||
return True
|
||||
|
||||
hass.bus.async_listen(EVENT_STATE_REPORTED, listener_callback, event_filter)
|
||||
|
||||
entity_registry.async_update_entity(entity_id, new_entity_id=new_entity_id)
|
||||
await hass.async_block_till_done()
|
||||
await client.add_events([event_message])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Because the entity's id has been updated, the entity has been unloaded
|
||||
# and the listener has been removed, and the new entity adds a new listener,
|
||||
# so the only entity that should report states is the one with the new entity id
|
||||
listener.assert_called_once_with(new_entity_id)
|
||||
|
||||
|
||||
async def tests_receive_setting_and_status_for_first_time_at_events(
|
||||
hass: HomeAssistant,
|
||||
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 capable of receiving settings and status for the first time."""
|
||||
client.get_setting = AsyncMock(return_value=ArrayOfSettings([]))
|
||||
client.get_status = AsyncMock(return_value=ArrayOfStatus([]))
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=EventKey.LAUNDRY_CARE_WASHER_SETTING_I_DOS_1_BASE_LEVEL,
|
||||
raw_key=EventKey.LAUNDRY_CARE_WASHER_SETTING_I_DOS_1_BASE_LEVEL.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value="some value",
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.STATUS,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
raw_key=EventKey.BSH_COMMON_STATUS_DOOR_STATE.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value="some value",
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(config_entry._background_tasks) == 1
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_event_listener_error(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test that the configuration entry is reloaded when the event stream raises an API error."""
|
||||
client_with_exception.stream_all_events = MagicMock(
|
||||
side_effect=HomeConnectApiError("error.key", "error description")
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
ConfigEntries,
|
||||
"async_schedule_reload",
|
||||
) as mock_schedule_reload:
|
||||
await integration_setup(client_with_exception)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client_with_exception.stream_all_events.assert_called_once()
|
||||
mock_schedule_reload.assert_called_once_with(config_entry.entry_id)
|
||||
assert not config_entry._background_tasks
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[HomeConnectRequestError(), EventStreamInterruptedError()],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"initial_state",
|
||||
"status_key",
|
||||
"status_value",
|
||||
"after_refresh_expected_state",
|
||||
"event_key",
|
||||
"event_value",
|
||||
"after_event_expected_state",
|
||||
),
|
||||
[
|
||||
(
|
||||
"sensor.washer_door",
|
||||
"closed",
|
||||
StatusKey.BSH_COMMON_DOOR_STATE,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
"locked",
|
||||
EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
"open",
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch(
|
||||
"homeassistant.components.home_connect.coordinator.EVENT_STREAM_RECONNECT_DELAY", 0
|
||||
)
|
||||
async def test_event_listener_resilience(
|
||||
entity_id: str,
|
||||
initial_state: str,
|
||||
status_key: StatusKey,
|
||||
status_value: Any,
|
||||
after_refresh_expected_state: str,
|
||||
event_key: EventKey,
|
||||
event_value: Any,
|
||||
after_event_expected_state: str,
|
||||
exception: HomeConnectError,
|
||||
hass: HomeAssistant,
|
||||
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()
|
||||
|
||||
async def stream_exception():
|
||||
yield await future
|
||||
|
||||
client.stream_all_events = MagicMock(
|
||||
side_effect=[stream_exception(), client.stream_all_events()]
|
||||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
await integration_setup(client)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert len(config_entry._background_tasks) == 1
|
||||
|
||||
assert hass.states.is_state(entity_id, initial_state)
|
||||
|
||||
client.get_status.return_value = ArrayOfStatus(
|
||||
[Status(key=status_key, raw_key=status_key.value, value=status_value)],
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
future.set_exception(exception)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert client.stream_all_events.call_count == 2
|
||||
assert hass.states.is_state(entity_id, after_refresh_expected_state)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.STATUS,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=event_value,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.is_state(entity_id, after_event_expected_state)
|
|
@ -1,11 +1,9 @@
|
|||
"""Test diagnostics for Home Connect."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.components.home_connect.diagnostics import (
|
||||
|
@ -16,43 +14,37 @@ from homeassistant.config_entries import ConfigEntryState
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert await async_get_config_entry_diagnostics(hass, config_entry) == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_async_get_device_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test device config entry diagnostics."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
|
@ -61,69 +53,3 @@ async def test_async_get_device_diagnostics(
|
|||
)
|
||||
|
||||
assert await async_get_device_diagnostics(hass, config_entry, device) == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_async_device_diagnostics_not_found(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test device config entry diagnostics."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, "Random-Device-ID")},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await async_get_device_diagnostics(hass, config_entry, device)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_error", "expected_connection_status"),
|
||||
[
|
||||
(HomeConnectError(), "unknown"),
|
||||
(
|
||||
HomeConnectError(
|
||||
{
|
||||
"key": "SDK.Error.HomeAppliance.Connection.Initialization.Failed",
|
||||
}
|
||||
),
|
||||
"offline",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_async_device_diagnostics_api_error(
|
||||
api_error: HomeConnectError,
|
||||
expected_connection_status: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test device config entry diagnostics."""
|
||||
appliance.get_programs_available.side_effect = api_error
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, appliance.haId)},
|
||||
)
|
||||
|
||||
diagnostics = await async_get_device_diagnostics(hass, config_entry, device)
|
||||
assert diagnostics["programs"] is None
|
||||
|
|
|
@ -2,27 +2,18 @@
|
|||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from aiohomeconnect.const import OAUTH2_TOKEN
|
||||
from aiohomeconnect.model import SettingKey, StatusKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import pytest
|
||||
from requests import HTTPError
|
||||
import requests_mock
|
||||
import respx
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.home_connect import (
|
||||
SCAN_INTERVAL,
|
||||
bsh_key_to_translation_key,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
BSH_OPERATION_STATE,
|
||||
BSH_POWER_STATE,
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
COOKING_LIGHTING,
|
||||
DOMAIN,
|
||||
OAUTH2_TOKEN,
|
||||
)
|
||||
from homeassistant.components.home_connect.const import DOMAIN
|
||||
from homeassistant.components.home_connect.utils import bsh_key_to_translation_key
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
@ -39,7 +30,6 @@ from .conftest import (
|
|||
FAKE_ACCESS_TOKEN,
|
||||
FAKE_REFRESH_TOKEN,
|
||||
SERVER_ACCESS_TOKEN,
|
||||
get_all_appliances,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -126,28 +116,26 @@ SERVICE_PROGRAM_CALL_PARAMS = [
|
|||
]
|
||||
|
||||
SERVICE_APPLIANCE_METHOD_MAPPING = {
|
||||
"set_option_active": "set_options_active_program",
|
||||
"set_option_selected": "set_options_selected_program",
|
||||
"set_option_active": "set_active_program_option",
|
||||
"set_option_selected": "set_selected_program_option",
|
||||
"change_setting": "set_setting",
|
||||
"pause_program": "execute_command",
|
||||
"resume_program": "execute_command",
|
||||
"select_program": "select_program",
|
||||
"pause_program": "put_command",
|
||||
"resume_program": "put_command",
|
||||
"select_program": "set_selected_program",
|
||||
"start_program": "start_program",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_api_setup(
|
||||
async def test_entry_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup and unload."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
@ -156,72 +144,60 @@ async def test_api_setup(
|
|||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_update_throttle(
|
||||
appliance: Mock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
) -> None:
|
||||
"""Test to check Throttle functionality."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
get_appliances_call_count = get_appliances.call_count
|
||||
|
||||
# First re-load after 1 minute is not blocked.
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
freezer.tick(SCAN_INTERVAL.seconds + 0.1)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||
|
||||
# Second re-load is blocked by Throttle.
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
freezer.tick(SCAN_INTERVAL.seconds - 0.1)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
assert get_appliances.call_count == get_appliances_call_count + 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_exception_handling(
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
problematic_appliance: Mock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test exception handling."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("token_expiration_time", [12345])
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
@respx.mock
|
||||
async def test_token_refresh_success(
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
hass: HomeAssistant,
|
||||
platforms: list[Platform],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
requests_mock: requests_mock.Mocker,
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test where token is expired and the refresh attempt succeeds."""
|
||||
|
||||
assert config_entry.data["token"]["access_token"] == FAKE_ACCESS_TOKEN
|
||||
|
||||
requests_mock.post(OAUTH2_TOKEN, json=SERVER_ACCESS_TOKEN)
|
||||
requests_mock.get("/api/homeappliances", json={"data": {"homeappliances": []}})
|
||||
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json=SERVER_ACCESS_TOKEN,
|
||||
)
|
||||
assert await integration_setup()
|
||||
appliances = client.get_home_appliances.return_value
|
||||
|
||||
async def mock_get_home_appliances():
|
||||
await client._auth.async_get_access_token()
|
||||
return appliances
|
||||
|
||||
client.get_home_appliances.return_value = None
|
||||
client.get_home_appliances.side_effect = mock_get_home_appliances
|
||||
|
||||
def init_side_effect(auth) -> MagicMock:
|
||||
client._auth = auth
|
||||
return client
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
with (
|
||||
patch("homeassistant.components.home_connect.PLATFORMS", platforms),
|
||||
patch("homeassistant.components.home_connect.HomeConnectClient") as client_mock,
|
||||
):
|
||||
client_mock.side_effect = MagicMock(side_effect=init_side_effect)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Verify token request
|
||||
|
@ -240,45 +216,43 @@ async def test_token_refresh_success(
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_http_error(
|
||||
async def test_client_error(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test HTTP errors during setup integration."""
|
||||
get_appliances.side_effect = HTTPError(response=MagicMock())
|
||||
"""Test client errors during setup integration."""
|
||||
client_with_exception.get_home_appliances.return_value = None
|
||||
client_with_exception.get_home_appliances.side_effect = HomeConnectError()
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
assert get_appliances.call_count == 1
|
||||
assert not await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
assert client_with_exception.get_home_appliances.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services(
|
||||
service_call: list[dict[str, Any]],
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"""Create and test services."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_name = service_call["service"]
|
||||
|
@ -286,8 +260,7 @@ async def test_services(
|
|||
await hass.services.async_call(**service_call)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
getattr(appliance, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count
|
||||
== 1
|
||||
getattr(client, SERVICE_APPLIANCE_METHOD_MAPPING[service_name]).call_count == 1
|
||||
)
|
||||
|
||||
|
||||
|
@ -295,26 +268,24 @@ async def test_services(
|
|||
"service_call",
|
||||
SERVICE_KV_CALL_PARAMS + SERVICE_COMMAND_CALL_PARAMS + SERVICE_PROGRAM_CALL_PARAMS,
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_exception(
|
||||
service_call: list[dict[str, Any]],
|
||||
service_call: dict[str, Any],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
problematic_appliance: Mock,
|
||||
client_with_exception: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a HomeAssistantError when there is an API error."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={(DOMAIN, problematic_appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
@ -323,25 +294,47 @@ async def test_services_exception(
|
|||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_services_appliance_not_found(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Raise a ServiceValidationError when device id does not match."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
service_call = SERVICE_KV_CALL_PARAMS[0]
|
||||
|
||||
service_call["service_data"]["device_id"] = "DOES_NOT_EXISTS"
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Device entry.*not found"):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
unrelated_config_entry = MockConfigEntry(
|
||||
domain="TEST",
|
||||
)
|
||||
unrelated_config_entry.add_to_hass(hass)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=unrelated_config_entry.entry_id,
|
||||
identifiers={("RANDOM", "ABCD")},
|
||||
)
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match=r"Home Connect config entry.*not found"
|
||||
):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("RANDOM", "ABCD")},
|
||||
)
|
||||
service_call["service_data"]["device_id"] = device_entry.id
|
||||
|
||||
with pytest.raises(ServiceValidationError, match=r"Appliance.*not found"):
|
||||
await hass.services.async_call(**service_call)
|
||||
|
||||
|
@ -351,7 +344,7 @@ async def test_entity_migration(
|
|||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry_v1_1: MockConfigEntry,
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
platforms: list[Platform],
|
||||
) -> None:
|
||||
"""Test entity migration."""
|
||||
|
@ -360,34 +353,39 @@ async def test_entity_migration(
|
|||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry_v1_1.entry_id,
|
||||
identifiers={(DOMAIN, appliance.haId)},
|
||||
identifiers={(DOMAIN, appliance_ha_id)},
|
||||
)
|
||||
|
||||
test_entities = [
|
||||
(
|
||||
SENSOR_DOMAIN,
|
||||
"Operation State",
|
||||
BSH_OPERATION_STATE,
|
||||
StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
),
|
||||
(
|
||||
SWITCH_DOMAIN,
|
||||
"ChildLock",
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
),
|
||||
(
|
||||
SWITCH_DOMAIN,
|
||||
"Power",
|
||||
BSH_POWER_STATE,
|
||||
SettingKey.BSH_COMMON_POWER_STATE,
|
||||
),
|
||||
(
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"Remote Start",
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE,
|
||||
StatusKey.BSH_COMMON_REMOTE_CONTROL_START_ALLOWED,
|
||||
),
|
||||
(
|
||||
LIGHT_DOMAIN,
|
||||
"Light",
|
||||
COOKING_LIGHTING,
|
||||
SettingKey.COOKING_COMMON_LIGHTING,
|
||||
),
|
||||
( # An already migrated entity
|
||||
SWITCH_DOMAIN,
|
||||
SettingKey.REFRIGERATION_COMMON_VACATION_MODE,
|
||||
SettingKey.REFRIGERATION_COMMON_VACATION_MODE,
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -395,7 +393,7 @@ async def test_entity_migration(
|
|||
entity_registry.async_get_or_create(
|
||||
domain,
|
||||
DOMAIN,
|
||||
f"{appliance.haId}-{old_unique_id_suffix}",
|
||||
f"{appliance_ha_id}-{old_unique_id_suffix}",
|
||||
device_id=device_entry.id,
|
||||
config_entry=config_entry_v1_1,
|
||||
)
|
||||
|
@ -406,7 +404,7 @@ async def test_entity_migration(
|
|||
|
||||
for domain, _, expected_unique_id_suffix in test_entities:
|
||||
assert entity_registry.async_get_entity_id(
|
||||
domain, DOMAIN, f"{appliance.haId}-{expected_unique_id_suffix}"
|
||||
domain, DOMAIN, f"{appliance_ha_id}-{expected_unique_id_suffix}"
|
||||
)
|
||||
assert config_entry_v1_1.minor_version == 2
|
||||
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
"""Tests for home_connect light entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfEvents,
|
||||
ArrayOfSettings,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
GetSetting,
|
||||
SettingKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS,
|
||||
BSH_AMBIENT_LIGHT_COLOR,
|
||||
BSH_AMBIENT_LIGHT_CUSTOM_COLOR,
|
||||
BSH_AMBIENT_LIGHT_ENABLED,
|
||||
COOKING_LIGHTING,
|
||||
COOKING_LIGHTING_BRIGHTNESS,
|
||||
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS,
|
||||
REFRIGERATION_EXTERNAL_LIGHT_POWER,
|
||||
BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
)
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
@ -23,26 +27,15 @@ from homeassistant.const import (
|
|||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HC_APP = "Hood"
|
||||
|
||||
SETTINGS_STATUS = {
|
||||
setting.pop("key"): setting
|
||||
for setting in load_json_object_fixture("home_connect/settings.json")
|
||||
.get(TEST_HC_APP)
|
||||
.get("data")
|
||||
.get("settings")
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[str]:
|
||||
|
@ -51,29 +44,31 @@ def platforms() -> list[str]:
|
|||
|
||||
|
||||
async def test_light(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch entities."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status", "service", "service_data", "state", "appliance"),
|
||||
(
|
||||
"entity_id",
|
||||
"set_settings_args",
|
||||
"service",
|
||||
"exprected_attributes",
|
||||
"state",
|
||||
"appliance_ha_id",
|
||||
),
|
||||
[
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {
|
||||
"value": True,
|
||||
},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: True,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
|
@ -83,58 +78,18 @@ async def test_light(
|
|||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {
|
||||
"value": True,
|
||||
},
|
||||
COOKING_LIGHTING_BRIGHTNESS: {"value": 70},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: True,
|
||||
SettingKey.COOKING_COMMON_LIGHTING_BRIGHTNESS: 80,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"brightness": 200},
|
||||
{"brightness": 199},
|
||||
STATE_ON,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {"value": False},
|
||||
COOKING_LIGHTING_BRIGHTNESS: {"value": 70},
|
||||
},
|
||||
SERVICE_TURN_OFF,
|
||||
{},
|
||||
STATE_OFF,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {
|
||||
"value": None,
|
||||
},
|
||||
COOKING_LIGHTING_BRIGHTNESS: None,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
STATE_UNKNOWN,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {
|
||||
"value": True,
|
||||
},
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70},
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"brightness": 200},
|
||||
STATE_ON,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {"value": False},
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: False,
|
||||
},
|
||||
SERVICE_TURN_OFF,
|
||||
{},
|
||||
|
@ -144,8 +99,28 @@ async def test_light(
|
|||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {"value": True},
|
||||
BSH_AMBIENT_LIGHT_CUSTOM_COLOR: {},
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS: 80,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"brightness": 199},
|
||||
STATE_ON,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: False,
|
||||
},
|
||||
SERVICE_TURN_OFF,
|
||||
{},
|
||||
STATE_OFF,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
|
@ -155,15 +130,28 @@ async def test_light(
|
|||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {"value": True},
|
||||
BSH_AMBIENT_LIGHT_COLOR: {
|
||||
"value": "",
|
||||
},
|
||||
BSH_AMBIENT_LIGHT_CUSTOM_COLOR: {},
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR: BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR: "#ffff00",
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
"rgb_color": [255, 255, 0],
|
||||
"rgb_color": (255, 255, 0),
|
||||
},
|
||||
STATE_ON,
|
||||
"Hood",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR: BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR: "#b5adcc",
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
"hs_color": (255.484, 15.196),
|
||||
"brightness": 199,
|
||||
},
|
||||
STATE_ON,
|
||||
"Hood",
|
||||
|
@ -171,10 +159,7 @@ async def test_light(
|
|||
(
|
||||
"light.fridgefreezer_external_light",
|
||||
{
|
||||
REFRIGERATION_EXTERNAL_LIGHT_POWER: {
|
||||
"value": True,
|
||||
},
|
||||
REFRIGERATION_EXTERNAL_LIGHT_BRIGHTNESS: {"value": 75},
|
||||
SettingKey.REFRIGERATION_COMMON_LIGHT_EXTERNAL_POWER: True,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
|
@ -182,167 +167,268 @@ async def test_light(
|
|||
"FridgeFreezer",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_light_functionality(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
set_settings_args: dict[SettingKey, Any],
|
||||
service: str,
|
||||
service_data: dict,
|
||||
exprected_attributes: dict[str, Any],
|
||||
state: str,
|
||||
appliance: Mock,
|
||||
bypass_throttle: Generator[None],
|
||||
appliance_ha_id: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test light functionality."""
|
||||
appliance.status.update(
|
||||
HomeConnectAppliance.json2dict(
|
||||
load_json_object_fixture("home_connect/settings.json")
|
||||
.get(appliance.name)
|
||||
.get("data")
|
||||
.get("settings")
|
||||
)
|
||||
)
|
||||
get_appliances.return_value = [appliance]
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update(status)
|
||||
service_data = exprected_attributes.copy()
|
||||
service_data["entity_id"] = entity_id
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
service,
|
||||
service_data,
|
||||
blocking=True,
|
||||
{key: value for key, value in service_data.items() if value is not None},
|
||||
)
|
||||
assert hass.states.is_state(entity_id, state)
|
||||
await hass.async_block_till_done()
|
||||
client.set_setting.assert_has_calls(
|
||||
[
|
||||
call(appliance_ha_id, setting_key=setting_key, value=value)
|
||||
for setting_key, value in set_settings_args.items()
|
||||
]
|
||||
)
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == state
|
||||
for key, value in exprected_attributes.items():
|
||||
assert entity_state.attributes[key] == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"status",
|
||||
"events",
|
||||
"appliance_ha_id",
|
||||
),
|
||||
[
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
EventKey.BSH_COMMON_SETTING_AMBIENT_LIGHT_COLOR: "BSH.Common.EnumType.AmbientLightColor.Color1",
|
||||
},
|
||||
"Hood",
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_light_color_different_than_custom(
|
||||
entity_id: str,
|
||||
events: dict[EventKey, Any],
|
||||
appliance_ha_id: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that light color attributes are not set if color is different than custom."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
"rgb_color": (255, 255, 0),
|
||||
"entity_id": entity_id,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
assert entity_state.attributes["rgb_color"] is not None
|
||||
assert entity_state.attributes["hs_color"] is not None
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=value,
|
||||
)
|
||||
for event_key, value in events.items()
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
assert entity_state.attributes["rgb_color"] is None
|
||||
assert entity_state.attributes["hs_color"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"setting",
|
||||
"service",
|
||||
"service_data",
|
||||
"mock_attr",
|
||||
"attr_side_effect",
|
||||
"problematic_appliance",
|
||||
"exception_match",
|
||||
),
|
||||
[
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {
|
||||
"value": False,
|
||||
},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: True,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
"set_setting",
|
||||
[HomeConnectError, HomeConnectError],
|
||||
"Hood",
|
||||
r"Error.*turn.*on.*",
|
||||
),
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {
|
||||
"value": True,
|
||||
},
|
||||
COOKING_LIGHTING_BRIGHTNESS: {"value": 70},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: True,
|
||||
SettingKey.COOKING_COMMON_LIGHTING_BRIGHTNESS: 70,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"brightness": 200},
|
||||
"set_setting",
|
||||
[HomeConnectError, HomeConnectError],
|
||||
"Hood",
|
||||
r"Error.*turn.*on.*",
|
||||
),
|
||||
(
|
||||
"light.hood_functional_light",
|
||||
{
|
||||
COOKING_LIGHTING: {"value": False},
|
||||
SettingKey.COOKING_COMMON_LIGHTING: False,
|
||||
},
|
||||
SERVICE_TURN_OFF,
|
||||
{},
|
||||
"set_setting",
|
||||
[HomeConnectError, HomeConnectError],
|
||||
"Hood",
|
||||
r"Error.*turn.*off.*",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {
|
||||
"value": True,
|
||||
},
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70},
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS: 70,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{},
|
||||
"set_setting",
|
||||
[HomeConnectError, HomeConnectError],
|
||||
"Hood",
|
||||
r"Error.*turn.*on.*",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
BSH_AMBIENT_LIGHT_ENABLED: {
|
||||
"value": True,
|
||||
},
|
||||
BSH_AMBIENT_LIGHT_BRIGHTNESS: {"value": 70},
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS: 70,
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"brightness": 200},
|
||||
"set_setting",
|
||||
[HomeConnectError, None, HomeConnectError],
|
||||
"Hood",
|
||||
r"Error.*set.*brightness.*",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS: 70,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR: 70,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR: "#ffff00",
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"rgb_color": (255, 255, 0)},
|
||||
[HomeConnectError, None, HomeConnectError],
|
||||
r"Error.*select.*custom color.*",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_BRIGHTNESS: 70,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR: BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR: "#ffff00",
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{"rgb_color": (255, 255, 0)},
|
||||
[HomeConnectError, None, None, HomeConnectError],
|
||||
r"Error.*set.*color.*",
|
||||
),
|
||||
(
|
||||
"light.hood_ambient_light",
|
||||
{
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_ENABLED: True,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR: BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR,
|
||||
SettingKey.BSH_COMMON_AMBIENT_LIGHT_CUSTOM_COLOR: "#b5adcc",
|
||||
},
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
"hs_color": (255.484, 15.196),
|
||||
"brightness": 199,
|
||||
},
|
||||
[HomeConnectError, None, None, HomeConnectError],
|
||||
r"Error.*set.*color.*",
|
||||
),
|
||||
],
|
||||
indirect=["problematic_appliance"],
|
||||
)
|
||||
async def test_switch_exception_handling(
|
||||
async def test_light_exception_handling(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
setting: dict[SettingKey, dict[str, Any]],
|
||||
service: str,
|
||||
service_data: dict,
|
||||
mock_attr: str,
|
||||
attr_side_effect: list,
|
||||
problematic_appliance: Mock,
|
||||
attr_side_effect: list[type[HomeConnectError] | None],
|
||||
exception_match: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test light exception handling."""
|
||||
problematic_appliance.status.update(SETTINGS_STATUS)
|
||||
problematic_appliance.set_setting.side_effect = attr_side_effect
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
|
||||
client_with_exception.get_settings.side_effect = None
|
||||
client_with_exception.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=value,
|
||||
)
|
||||
for setting_key, value in setting.items()
|
||||
]
|
||||
)
|
||||
client_with_exception.set_setting.side_effect = [
|
||||
exception() if exception else None for exception in attr_side_effect
|
||||
]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Assert that an exception is called.
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
await client_with_exception.set_setting()
|
||||
|
||||
problematic_appliance.status.update(status)
|
||||
service_data["entity_id"] = entity_id
|
||||
with pytest.raises(HomeAssistantError, match=exception_match):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN, service, service_data, blocking=True
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == len(attr_side_effect)
|
||||
assert client_with_exception.set_setting.call_count == len(attr_side_effect)
|
||||
|
|
|
@ -1,22 +1,17 @@
|
|||
"""Tests for home_connect number entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from collections.abc import Awaitable, Callable
|
||||
import random
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import ArrayOfSettings, GetSetting, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.setting import SettingConstraints
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
ATTR_CONSTRAINTS,
|
||||
ATTR_STEPSIZE,
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
)
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_VALUE as SERVICE_ATTR_VALUE,
|
||||
DEFAULT_MAX_VALUE,
|
||||
DEFAULT_MIN_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
|
@ -26,8 +21,6 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
|
@ -38,25 +31,24 @@ def platforms() -> list[str]:
|
|||
|
||||
|
||||
async def test_number(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test number entity."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Refrigerator"], indirect=True)
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["FridgeFreezer"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"setting_key",
|
||||
"type",
|
||||
"expected_state",
|
||||
"min_value",
|
||||
"max_value",
|
||||
"step_size",
|
||||
|
@ -64,102 +56,132 @@ async def test_number(
|
|||
),
|
||||
[
|
||||
(
|
||||
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
|
||||
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
|
||||
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
|
||||
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
"Double",
|
||||
8,
|
||||
7,
|
||||
15,
|
||||
0.1,
|
||||
"°C",
|
||||
),
|
||||
(
|
||||
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
|
||||
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
"Double",
|
||||
8,
|
||||
7,
|
||||
15,
|
||||
5,
|
||||
"°C",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_number_entity_functionality(
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
setting_key: str,
|
||||
bypass_throttle: Generator[None],
|
||||
setting_key: SettingKey,
|
||||
type: str,
|
||||
expected_state: int,
|
||||
min_value: int,
|
||||
max_value: int,
|
||||
step_size: float,
|
||||
unit_of_measurement: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test number entity functionality."""
|
||||
appliance.get.side_effect = [
|
||||
{
|
||||
ATTR_CONSTRAINTS: {
|
||||
ATTR_MIN: min_value,
|
||||
ATTR_MAX: max_value,
|
||||
ATTR_STEPSIZE: step_size,
|
||||
},
|
||||
ATTR_UNIT: unit_of_measurement,
|
||||
}
|
||||
]
|
||||
get_appliances.return_value = [appliance]
|
||||
current_value = min_value
|
||||
appliance.status.update({setting_key: {ATTR_VALUE: current_value}})
|
||||
client.get_setting.side_effect = None
|
||||
client.get_setting = AsyncMock(
|
||||
return_value=GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value="", # This should not change the value
|
||||
unit=unit_of_measurement,
|
||||
type=type,
|
||||
constraints=SettingConstraints(
|
||||
min=min_value,
|
||||
max=max_value,
|
||||
step_size=step_size if isinstance(step_size, int) else None,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.is_state(entity_id, str(current_value))
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["min"] == min_value
|
||||
assert state.attributes["max"] == max_value
|
||||
assert state.attributes["step"] == step_size
|
||||
assert state.attributes["unit_of_measurement"] == unit_of_measurement
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state
|
||||
assert entity_state.state == str(expected_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
|
||||
|
||||
new_value = random.randint(min_value + 1, max_value)
|
||||
value = random.choice(
|
||||
[num for num in range(min_value, max_value + 1) if num != expected_state]
|
||||
)
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
SERVICE_ATTR_VALUE: new_value,
|
||||
SERVICE_ATTR_VALUE: value,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
appliance.set_setting.assert_called_once_with(setting_key, new_value)
|
||||
await hass.async_block_till_done()
|
||||
client.set_setting.assert_awaited_once_with(
|
||||
appliance_ha_id, setting_key=setting_key, value=value
|
||||
)
|
||||
assert hass.states.is_state(entity_id, str(float(value)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("problematic_appliance", ["Refrigerator"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "setting_key", "mock_attr"),
|
||||
[
|
||||
(
|
||||
f"{NUMBER_DOMAIN.lower()}.refrigerator_refrigerator_temperature",
|
||||
"Refrigeration.FridgeFreezer.Setting.SetpointTemperatureRefrigerator",
|
||||
f"{NUMBER_DOMAIN.lower()}.fridgefreezer_refrigerator_temperature",
|
||||
SettingKey.REFRIGERATION_FRIDGE_FREEZER_SETPOINT_TEMPERATURE_REFRIGERATOR,
|
||||
"set_setting",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_number_entity_error(
|
||||
problematic_appliance: Mock,
|
||||
entity_id: str,
|
||||
setting_key: str,
|
||||
setting_key: SettingKey,
|
||||
mock_attr: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test number entity error."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
|
||||
client_with_exception.get_settings.side_effect = None
|
||||
client_with_exception.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=DEFAULT_MIN_VALUE,
|
||||
constraints=SettingConstraints(
|
||||
min=int(DEFAULT_MIN_VALUE),
|
||||
max=int(DEFAULT_MAX_VALUE),
|
||||
step_size=1,
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
problematic_appliance.status.update({setting_key: {}})
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
await getattr(client_with_exception, mock_attr)()
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match=r"Error.*assign.*value.*to.*setting.*"
|
||||
|
@ -173,4 +195,4 @@ async def test_number_entity_error(
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
||||
assert getattr(client_with_exception, mock_attr).call_count == 2
|
||||
|
|
|
@ -1,39 +1,38 @@
|
|||
"""Tests for home_connect select entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfAvailablePrograms,
|
||||
ArrayOfEvents,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
ProgramKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import EnumerateAvailableProgram
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_SELECTED_PROGRAM,
|
||||
)
|
||||
from homeassistant.components.select import (
|
||||
ATTR_OPTION,
|
||||
ATTR_OPTIONS,
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SELECT_OPTION, Platform
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_SELECT_OPTION,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
SETTINGS_STATUS = {
|
||||
setting.pop("key"): setting
|
||||
for setting in load_json_object_fixture("home_connect/settings.json")
|
||||
.get("Washer")
|
||||
.get("data")
|
||||
.get("settings")
|
||||
}
|
||||
|
||||
PROGRAM = "Dishcare.Dishwasher.Program.Eco50"
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -43,119 +42,148 @@ def platforms() -> list[str]:
|
|||
|
||||
|
||||
async def test_select(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test select entity."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_filter_unknown_programs(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test select that programs that are not part of the official Home Connect API specification are filtered out.
|
||||
|
||||
We use two programs to ensure that programs are iterated over a copy of the list,
|
||||
and it does not raise problems when removing an element from the original list.
|
||||
"""
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
appliance.get_programs_available.return_value = [
|
||||
PROGRAM,
|
||||
"NonOfficialProgram",
|
||||
"AntotherNonOfficialProgram",
|
||||
]
|
||||
get_appliances.return_value = [appliance]
|
||||
"""Test select that only known programs are shown."""
|
||||
client.get_available_programs.side_effect = None
|
||||
client.get_available_programs.return_value = ArrayOfAvailablePrograms(
|
||||
[
|
||||
EnumerateAvailableProgram(
|
||||
key=ProgramKey.DISHCARE_DISHWASHER_ECO_50,
|
||||
raw_key=ProgramKey.DISHCARE_DISHWASHER_ECO_50.value,
|
||||
),
|
||||
EnumerateAvailableProgram(
|
||||
key=ProgramKey.UNKNOWN,
|
||||
raw_key="an unknown program",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
entity = entity_registry.async_get("select.washer_selected_program")
|
||||
entity = entity_registry.async_get("select.dishwasher_selected_program")
|
||||
assert entity
|
||||
assert entity.capabilities.get(ATTR_OPTIONS) == [
|
||||
"dishcare_dishwasher_program_eco_50"
|
||||
]
|
||||
assert entity.capabilities
|
||||
assert entity.capabilities[ATTR_OPTIONS] == ["dishcare_dishwasher_program_eco_50"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status", "program_to_set"),
|
||||
(
|
||||
"appliance_ha_id",
|
||||
"entity_id",
|
||||
"mock_method",
|
||||
"program_key",
|
||||
"program_to_set",
|
||||
"event_key",
|
||||
),
|
||||
[
|
||||
(
|
||||
"select.washer_selected_program",
|
||||
{BSH_SELECTED_PROGRAM: {"value": PROGRAM}},
|
||||
"Dishwasher",
|
||||
"select.dishwasher_selected_program",
|
||||
"set_selected_program",
|
||||
ProgramKey.DISHCARE_DISHWASHER_ECO_50,
|
||||
"dishcare_dishwasher_program_eco_50",
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
),
|
||||
(
|
||||
"select.washer_active_program",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
|
||||
"Dishwasher",
|
||||
"select.dishwasher_active_program",
|
||||
"start_program",
|
||||
ProgramKey.DISHCARE_DISHWASHER_ECO_50,
|
||||
"dishcare_dishwasher_program_eco_50",
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_select_functionality(
|
||||
async def test_select_program_functionality(
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
mock_method: str,
|
||||
program_key: ProgramKey,
|
||||
program_to_set: str,
|
||||
bypass_throttle: Generator[None],
|
||||
event_key: EventKey,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test select functionality."""
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
appliance.get_programs_available.return_value = [PROGRAM]
|
||||
get_appliances.return_value = [appliance]
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update(status)
|
||||
assert hass.states.is_state(entity_id, "unknown")
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: program_to_set},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
getattr(client, mock_method).assert_awaited_once_with(
|
||||
appliance_ha_id, program_key=program_key
|
||||
)
|
||||
assert hass.states.is_state(entity_id, program_to_set)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value="A not known program",
|
||||
)
|
||||
]
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, STATE_UNKNOWN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"status",
|
||||
"program_to_set",
|
||||
"mock_attr",
|
||||
"exception_match",
|
||||
),
|
||||
[
|
||||
(
|
||||
"select.washer_selected_program",
|
||||
{BSH_SELECTED_PROGRAM: {"value": PROGRAM}},
|
||||
"select.dishwasher_selected_program",
|
||||
"dishcare_dishwasher_program_eco_50",
|
||||
"select_program",
|
||||
"set_selected_program",
|
||||
r"Error.*select.*program.*",
|
||||
),
|
||||
(
|
||||
"select.washer_active_program",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
|
||||
"select.dishwasher_active_program",
|
||||
"dishcare_dishwasher_program_eco_50",
|
||||
"start_program",
|
||||
r"Error.*start.*program.*",
|
||||
|
@ -164,32 +192,36 @@ async def test_select_functionality(
|
|||
)
|
||||
async def test_select_exception_handling(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
program_to_set: str,
|
||||
mock_attr: str,
|
||||
exception_match: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
problematic_appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test exception handling."""
|
||||
problematic_appliance.get_programs_available.side_effect = None
|
||||
problematic_appliance.get_programs_available.return_value = [PROGRAM]
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
client_with_exception.get_available_programs.side_effect = None
|
||||
client_with_exception.get_available_programs.return_value = (
|
||||
ArrayOfAvailablePrograms(
|
||||
[
|
||||
EnumerateAvailableProgram(
|
||||
key=ProgramKey.DISHCARE_DISHWASHER_ECO_50,
|
||||
raw_key=ProgramKey.DISHCARE_DISHWASHER_ECO_50.value,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Assert that an exception is called.
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
await getattr(client_with_exception, mock_attr)()
|
||||
|
||||
problematic_appliance.status.update(status)
|
||||
with pytest.raises(HomeAssistantError, match=exception_match):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
|
@ -197,4 +229,4 @@ async def test_select_exception_handling(
|
|||
{"entity_id": entity_id, "option": program_to_set},
|
||||
blocking=True,
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
||||
assert getattr(client_with_exception, mock_attr).call_count == 2
|
||||
|
|
|
@ -1,75 +1,77 @@
|
|||
"""Tests for home_connect sensor entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfEvents,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
Status,
|
||||
StatusKey,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from homeconnect.api import HomeConnectAPI
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import (
|
||||
BSH_DOOR_STATE,
|
||||
BSH_DOOR_STATE_CLOSED,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
BSH_EVENT_PRESENT_STATE_CONFIRMED,
|
||||
BSH_EVENT_PRESENT_STATE_OFF,
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_HC_APP = "Dishwasher"
|
||||
|
||||
|
||||
EVENT_PROG_DELAYED_START = {
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.DelayedStart"
|
||||
},
|
||||
}
|
||||
|
||||
EVENT_PROG_REMAIN_NO_VALUE = {
|
||||
"BSH.Common.Option.RemainingProgramTime": {},
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.DelayedStart"
|
||||
EventType.STATUS: {
|
||||
EventKey.BSH_COMMON_STATUS_OPERATION_STATE: "BSH.Common.EnumType.OperationState.DelayedStart",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
EVENT_PROG_RUN = {
|
||||
"BSH.Common.Option.RemainingProgramTime": {"value": "0"},
|
||||
"BSH.Common.Option.ProgramProgress": {"value": "60"},
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.Run"
|
||||
EventType.STATUS: {
|
||||
EventKey.BSH_COMMON_STATUS_OPERATION_STATE: "BSH.Common.EnumType.OperationState.Run",
|
||||
},
|
||||
EventType.EVENT: {
|
||||
EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME: 0,
|
||||
EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS: 60,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
EVENT_PROG_UPDATE_1 = {
|
||||
"BSH.Common.Option.RemainingProgramTime": {"value": "0"},
|
||||
"BSH.Common.Option.ProgramProgress": {"value": "80"},
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.Run"
|
||||
EventType.EVENT: {
|
||||
EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME: 0,
|
||||
EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS: 80,
|
||||
},
|
||||
EventType.STATUS: {
|
||||
EventKey.BSH_COMMON_STATUS_OPERATION_STATE: "BSH.Common.EnumType.OperationState.Run",
|
||||
},
|
||||
}
|
||||
|
||||
EVENT_PROG_UPDATE_2 = {
|
||||
"BSH.Common.Option.RemainingProgramTime": {"value": "20"},
|
||||
"BSH.Common.Option.ProgramProgress": {"value": "99"},
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.Run"
|
||||
EventType.EVENT: {
|
||||
EventKey.BSH_COMMON_OPTION_REMAINING_PROGRAM_TIME: 20,
|
||||
EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS: 99,
|
||||
},
|
||||
EventType.STATUS: {
|
||||
EventKey.BSH_COMMON_STATUS_OPERATION_STATE: "BSH.Common.EnumType.OperationState.Run",
|
||||
},
|
||||
}
|
||||
|
||||
EVENT_PROG_END = {
|
||||
"BSH.Common.Status.OperationState": {
|
||||
"value": "BSH.Common.EnumType.OperationState.Ready"
|
||||
EventType.STATUS: {
|
||||
EventKey.BSH_COMMON_STATUS_OPERATION_STATE: "BSH.Common.EnumType.OperationState.Ready",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -80,22 +82,19 @@ def platforms() -> list[str]:
|
|||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_sensors(
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test sensor entities."""
|
||||
get_appliances.return_value = [appliance]
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
# Appliance program sequence with a delayed start.
|
||||
# Appliance_ha_id program sequence with a delayed start.
|
||||
PROGRAM_SEQUENCE_EVENTS = (
|
||||
EVENT_PROG_DELAYED_START,
|
||||
EVENT_PROG_RUN,
|
||||
|
@ -130,7 +129,7 @@ ENTITY_ID_STATES = {
|
|||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True)
|
||||
@pytest.mark.parametrize("appliance_ha_id", [TEST_HC_APP], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("states", "event_run"),
|
||||
list(
|
||||
|
@ -141,17 +140,16 @@ ENTITY_ID_STATES = {
|
|||
)
|
||||
),
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_event_sensors(
|
||||
appliance: Mock,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
states: tuple,
|
||||
event_run: dict,
|
||||
event_run: dict[EventType, dict[EventKey, str | int]],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
) -> None:
|
||||
"""Test sequence for sensors that are only available after an event happens."""
|
||||
entity_ids = ENTITY_ID_STATES.keys()
|
||||
|
@ -159,24 +157,48 @@ async def test_event_sensors(
|
|||
time_to_freeze = "2021-01-09 12:00:00+00:00"
|
||||
freezer.move_to(time_to_freeze)
|
||||
|
||||
get_appliances.return_value = [appliance]
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
appliance.get_programs_available = MagicMock(return_value=["dummy_program"])
|
||||
appliance.status.update(EVENT_PROG_DELAYED_START)
|
||||
assert await integration_setup()
|
||||
client.get_status.return_value.status.extend(
|
||||
Status(
|
||||
key=StatusKey(event_key.value),
|
||||
raw_key=event_key.value,
|
||||
value=value,
|
||||
)
|
||||
for event_key, value in EVENT_PROG_DELAYED_START[EventType.STATUS].items()
|
||||
)
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update(event_run)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
event_type,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=value,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
for event_type, events in event_run.items()
|
||||
for event_key, value in events.items()
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
for entity_id, state in zip(entity_ids, states, strict=False):
|
||||
await async_update_entity(hass, entity_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, state)
|
||||
|
||||
|
||||
# Program sequence for SensorDeviceClass.TIMESTAMP edge cases.
|
||||
PROGRAM_SEQUENCE_EDGE_CASE = [
|
||||
EVENT_PROG_REMAIN_NO_VALUE,
|
||||
EVENT_PROG_DELAYED_START,
|
||||
EVENT_PROG_RUN,
|
||||
EVENT_PROG_END,
|
||||
EVENT_PROG_END,
|
||||
|
@ -191,60 +213,86 @@ ENTITY_ID_EDGE_CASE_STATES = [
|
|||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", [TEST_HC_APP], indirect=True)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
@pytest.mark.parametrize("appliance_ha_id", [TEST_HC_APP], indirect=True)
|
||||
async def test_remaining_prog_time_edge_cases(
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Run program sequence to test edge cases for the remaining_prog_time entity."""
|
||||
get_appliances.return_value = [appliance]
|
||||
entity_id = "sensor.dishwasher_program_finish_time"
|
||||
time_to_freeze = "2021-01-09 12:00:00+00:00"
|
||||
freezer.move_to(time_to_freeze)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
appliance.get_programs_available = MagicMock(return_value=["dummy_program"])
|
||||
appliance.status.update(EVENT_PROG_REMAIN_NO_VALUE)
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
for (
|
||||
event,
|
||||
expected_state,
|
||||
) in zip(PROGRAM_SEQUENCE_EDGE_CASE, ENTITY_ID_EDGE_CASE_STATES, strict=False):
|
||||
appliance.status.update(event)
|
||||
await async_update_entity(hass, entity_id)
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
event_type,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=event_key.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=value,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
for event_type, events in event.items()
|
||||
for event_key, value in events.items()
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
freezer.tick()
|
||||
assert hass.states.is_state(entity_id, expected_state)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status_key", "event_value_update", "expected", "appliance"),
|
||||
(
|
||||
"entity_id",
|
||||
"event_key",
|
||||
"event_type",
|
||||
"event_value_update",
|
||||
"expected",
|
||||
"appliance_ha_id",
|
||||
),
|
||||
[
|
||||
(
|
||||
"sensor.dishwasher_door",
|
||||
BSH_DOOR_STATE,
|
||||
EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
EventType.STATUS,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
"locked",
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"sensor.dishwasher_door",
|
||||
BSH_DOOR_STATE,
|
||||
EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
EventType.STATUS,
|
||||
BSH_DOOR_STATE_CLOSED,
|
||||
"closed",
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"sensor.dishwasher_door",
|
||||
BSH_DOOR_STATE,
|
||||
EventKey.BSH_COMMON_STATUS_DOOR_STATE,
|
||||
EventType.STATUS,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
"open",
|
||||
"Dishwasher",
|
||||
|
@ -252,33 +300,38 @@ async def test_remaining_prog_time_edge_cases(
|
|||
(
|
||||
"sensor.fridgefreezer_freezer_door_alarm",
|
||||
"EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF",
|
||||
EventType.EVENT,
|
||||
"",
|
||||
"off",
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"sensor.fridgefreezer_freezer_door_alarm",
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_OFF,
|
||||
"off",
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"sensor.fridgefreezer_freezer_door_alarm",
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
"present",
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"sensor.fridgefreezer_freezer_door_alarm",
|
||||
REFRIGERATION_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_CONFIRMED,
|
||||
"confirmed",
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"sensor.coffeemaker_bean_container_empty",
|
||||
EventType.EVENT,
|
||||
"EVENT_NOT_IN_STATUS_YET_SO_SET_TO_OFF",
|
||||
"",
|
||||
"off",
|
||||
|
@ -286,52 +339,68 @@ async def test_remaining_prog_time_edge_cases(
|
|||
),
|
||||
(
|
||||
"sensor.coffeemaker_bean_container_empty",
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_OFF,
|
||||
"off",
|
||||
"CoffeeMaker",
|
||||
),
|
||||
(
|
||||
"sensor.coffeemaker_bean_container_empty",
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_PRESENT,
|
||||
"present",
|
||||
"CoffeeMaker",
|
||||
),
|
||||
(
|
||||
"sensor.coffeemaker_bean_container_empty",
|
||||
COFFEE_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
EventType.EVENT,
|
||||
BSH_EVENT_PRESENT_STATE_CONFIRMED,
|
||||
"confirmed",
|
||||
"CoffeeMaker",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_sensors_states(
|
||||
entity_id: str,
|
||||
status_key: str,
|
||||
event_key: EventKey,
|
||||
event_type: EventType,
|
||||
event_value_update: str,
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
expected: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Tests for Appliance alarm sensors."""
|
||||
appliance.status.update(
|
||||
HomeConnectAPI.json2dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"]["status"]
|
||||
)
|
||||
)
|
||||
get_appliances.return_value = [appliance]
|
||||
"""Tests for Appliance_ha_id alarm sensors."""
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
appliance.status.update({status_key: {"value": event_value_update}})
|
||||
await async_update_entity(hass, entity_id)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
event_type,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=event_key,
|
||||
raw_key=str(event_key),
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=event_value_update,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, expected)
|
||||
|
|
|
@ -1,24 +1,34 @@
|
|||
"""Tests for home_connect sensor entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectAppliance, HomeConnectError
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfSettings,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
GetSetting,
|
||||
ProgramKey,
|
||||
SettingKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.event import ArrayOfEvents, EventType
|
||||
from aiohomeconnect.model.program import (
|
||||
ArrayOfAvailablePrograms,
|
||||
EnumerateAvailableProgram,
|
||||
)
|
||||
from aiohomeconnect.model.setting import SettingConstraints
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import automation, script
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.home_connect.const import (
|
||||
ATTR_ALLOWED_VALUES,
|
||||
ATTR_CONSTRAINTS,
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_CHILD_LOCK_STATE,
|
||||
BSH_POWER_OFF,
|
||||
BSH_POWER_ON,
|
||||
BSH_POWER_STANDBY,
|
||||
BSH_POWER_STATE,
|
||||
DOMAIN,
|
||||
REFRIGERATION_SUPERMODEFREEZER,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
|
@ -36,19 +46,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
SETTINGS_STATUS = {
|
||||
setting.pop("key"): setting
|
||||
for setting in load_json_object_fixture("home_connect/settings.json")
|
||||
.get("Dishwasher")
|
||||
.get("data")
|
||||
.get("settings")
|
||||
}
|
||||
|
||||
PROGRAM = "LaundryCare.Dryer.Program.Mix"
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -58,231 +56,285 @@ def platforms() -> list[str]:
|
|||
|
||||
|
||||
async def test_switches(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch entities."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status", "service", "state", "appliance"),
|
||||
[
|
||||
(
|
||||
"switch.dishwasher_program_mix",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_program_mix",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": ""}},
|
||||
SERVICE_TURN_OFF,
|
||||
STATE_OFF,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
{BSH_CHILD_LOCK_STATE: {"value": True}},
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
{BSH_CHILD_LOCK_STATE: {"value": False}},
|
||||
SERVICE_TURN_OFF,
|
||||
STATE_OFF,
|
||||
"Dishwasher",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
async def test_switch_functionality(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
service: str,
|
||||
state: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch functionality."""
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
appliance.get_programs_available.return_value = [PROGRAM]
|
||||
get_appliances.return_value = [appliance]
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update(status)
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {"entity_id": entity_id}, blocking=True
|
||||
)
|
||||
assert hass.states.is_state(entity_id, state)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"status",
|
||||
"service",
|
||||
"settings_key_arg",
|
||||
"setting_value_arg",
|
||||
"state",
|
||||
"appliance_ha_id",
|
||||
),
|
||||
[
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
SERVICE_TURN_ON,
|
||||
SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
True,
|
||||
STATE_ON,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
SERVICE_TURN_OFF,
|
||||
SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
False,
|
||||
STATE_OFF,
|
||||
"Dishwasher",
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_switch_functionality(
|
||||
entity_id: str,
|
||||
settings_key_arg: SettingKey,
|
||||
setting_value_arg: Any,
|
||||
service: str,
|
||||
state: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance_ha_id: str,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch functionality."""
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id})
|
||||
await hass.async_block_till_done()
|
||||
client.set_setting.assert_awaited_once_with(
|
||||
appliance_ha_id, setting_key=settings_key_arg, value=setting_value_arg
|
||||
)
|
||||
assert hass.states.is_state(entity_id, state)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "program_key", "appliance_ha_id"),
|
||||
[
|
||||
(
|
||||
"switch.dryer_program_mix",
|
||||
ProgramKey.LAUNDRY_CARE_DRYER_MIX,
|
||||
"Dryer",
|
||||
),
|
||||
],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_program_switch_functionality(
|
||||
entity_id: str,
|
||||
program_key: ProgramKey,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance_ha_id: str,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch functionality."""
|
||||
|
||||
async def mock_stop_program(ha_id: str) -> None:
|
||||
"""Mock stop program."""
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
ha_id,
|
||||
EventType.NOTIFY,
|
||||
ArrayOfEvents(
|
||||
[
|
||||
Event(
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
raw_key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM.value,
|
||||
timestamp=0,
|
||||
level="",
|
||||
handling="",
|
||||
value=ProgramKey.UNKNOWN,
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
client.stop_program = AsyncMock(side_effect=mock_stop_program)
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, STATE_ON)
|
||||
client.start_program.assert_awaited_once_with(
|
||||
appliance_ha_id, program_key=program_key
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, STATE_OFF)
|
||||
client.stop_program.assert_awaited_once_with(appliance_ha_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id",
|
||||
"service",
|
||||
"mock_attr",
|
||||
"problematic_appliance",
|
||||
"exception_match",
|
||||
),
|
||||
[
|
||||
(
|
||||
"switch.dishwasher_program_mix",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
|
||||
"switch.dishwasher_program_eco50",
|
||||
SERVICE_TURN_ON,
|
||||
"start_program",
|
||||
"Dishwasher",
|
||||
r"Error.*start.*program.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_program_mix",
|
||||
{BSH_ACTIVE_PROGRAM: {"value": PROGRAM}},
|
||||
"switch.dishwasher_program_eco50",
|
||||
SERVICE_TURN_OFF,
|
||||
"stop_program",
|
||||
"Dishwasher",
|
||||
r"Error.*stop.*program.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": BSH_POWER_OFF}},
|
||||
SERVICE_TURN_OFF,
|
||||
"set_setting",
|
||||
"Dishwasher",
|
||||
r"Error.*turn.*off.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": ""}},
|
||||
SERVICE_TURN_ON,
|
||||
"set_setting",
|
||||
"Dishwasher",
|
||||
r"Error.*turn.*on.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
{BSH_CHILD_LOCK_STATE: {"value": ""}},
|
||||
SERVICE_TURN_ON,
|
||||
"set_setting",
|
||||
"Dishwasher",
|
||||
r"Error.*turn.*on.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_child_lock",
|
||||
{BSH_CHILD_LOCK_STATE: {"value": ""}},
|
||||
SERVICE_TURN_OFF,
|
||||
"set_setting",
|
||||
"Dishwasher",
|
||||
r"Error.*turn.*off.*",
|
||||
),
|
||||
],
|
||||
indirect=["problematic_appliance"],
|
||||
)
|
||||
async def test_switch_exception_handling(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
service: str,
|
||||
mock_attr: str,
|
||||
exception_match: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
problematic_appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test exception handling."""
|
||||
problematic_appliance.get_programs_available.side_effect = None
|
||||
problematic_appliance.get_programs_available.return_value = [PROGRAM]
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
client_with_exception.get_available_programs.side_effect = None
|
||||
client_with_exception.get_available_programs.return_value = (
|
||||
ArrayOfAvailablePrograms(
|
||||
[
|
||||
EnumerateAvailableProgram(
|
||||
key=ProgramKey.DISHCARE_DISHWASHER_ECO_50,
|
||||
raw_key=ProgramKey.DISHCARE_DISHWASHER_ECO_50.value,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
client_with_exception.get_settings.side_effect = None
|
||||
client_with_exception.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
raw_key=SettingKey.BSH_COMMON_CHILD_LOCK.value,
|
||||
value=False,
|
||||
),
|
||||
GetSetting(
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
raw_key=SettingKey.BSH_COMMON_POWER_STATE.value,
|
||||
value=BSH_POWER_ON,
|
||||
constraints=SettingConstraints(
|
||||
allowed_values=[BSH_POWER_ON, BSH_POWER_OFF]
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
problematic_appliance.status.update(status)
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Assert that an exception is called.
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
await getattr(client_with_exception, mock_attr)()
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=exception_match):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {"entity_id": entity_id}, blocking=True
|
||||
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
||||
assert getattr(client_with_exception, mock_attr).call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status", "service", "state", "appliance"),
|
||||
("entity_id", "status", "service", "state", "appliance_ha_id"),
|
||||
[
|
||||
(
|
||||
"switch.fridgefreezer_freezer_super_mode",
|
||||
{REFRIGERATION_SUPERMODEFREEZER: {"value": True}},
|
||||
{SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: True},
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
"FridgeFreezer",
|
||||
),
|
||||
(
|
||||
"switch.fridgefreezer_freezer_super_mode",
|
||||
{REFRIGERATION_SUPERMODEFREEZER: {"value": False}},
|
||||
{SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: False},
|
||||
SERVICE_TURN_OFF,
|
||||
STATE_OFF,
|
||||
"FridgeFreezer",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_ent_desc_switch_functionality(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
service: str,
|
||||
state: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch functionality - entity description setup."""
|
||||
appliance.status.update(
|
||||
HomeConnectAppliance.json2dict(
|
||||
load_json_object_fixture("home_connect/settings.json")
|
||||
.get(appliance.name)
|
||||
.get("data")
|
||||
.get("settings")
|
||||
)
|
||||
)
|
||||
get_appliances.return_value = [appliance]
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update(status)
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id})
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.is_state(entity_id, state)
|
||||
|
||||
|
||||
|
@ -292,13 +344,13 @@ async def test_ent_desc_switch_functionality(
|
|||
"status",
|
||||
"service",
|
||||
"mock_attr",
|
||||
"problematic_appliance",
|
||||
"appliance_ha_id",
|
||||
"exception_match",
|
||||
),
|
||||
[
|
||||
(
|
||||
"switch.fridgefreezer_freezer_super_mode",
|
||||
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
|
||||
{SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: ""},
|
||||
SERVICE_TURN_ON,
|
||||
"set_setting",
|
||||
"FridgeFreezer",
|
||||
|
@ -306,203 +358,257 @@ async def test_ent_desc_switch_functionality(
|
|||
),
|
||||
(
|
||||
"switch.fridgefreezer_freezer_super_mode",
|
||||
{REFRIGERATION_SUPERMODEFREEZER: {"value": ""}},
|
||||
{SettingKey.REFRIGERATION_FRIDGE_FREEZER_SUPER_MODE_FREEZER: ""},
|
||||
SERVICE_TURN_OFF,
|
||||
"set_setting",
|
||||
"FridgeFreezer",
|
||||
r"Error.*turn.*off.*",
|
||||
),
|
||||
],
|
||||
indirect=["problematic_appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
async def test_ent_desc_switch_exception_handling(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
status: dict[SettingKey, str],
|
||||
service: str,
|
||||
mock_attr: str,
|
||||
exception_match: str,
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
config_entry: MockConfigEntry,
|
||||
setup_credentials: None,
|
||||
problematic_appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test switch exception handling - entity description setup."""
|
||||
problematic_appliance.status.update(
|
||||
HomeConnectAppliance.json2dict(
|
||||
load_json_object_fixture("home_connect/settings.json")
|
||||
.get(problematic_appliance.name)
|
||||
.get("data")
|
||||
.get("settings")
|
||||
)
|
||||
client_with_exception.get_settings.side_effect = None
|
||||
client_with_exception.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=key,
|
||||
raw_key=key.value,
|
||||
value=value,
|
||||
)
|
||||
for key, value in status.items()
|
||||
]
|
||||
)
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# Assert that an exception is called.
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
|
||||
problematic_appliance.status.update(status)
|
||||
await client_with_exception.set_setting()
|
||||
with pytest.raises(HomeAssistantError, match=exception_match):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
||||
assert client_with_exception.set_setting.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "status", "allowed_values", "service", "power_state", "appliance"),
|
||||
(
|
||||
"entity_id",
|
||||
"allowed_values",
|
||||
"service",
|
||||
"setting_value_arg",
|
||||
"power_state",
|
||||
"appliance_ha_id",
|
||||
),
|
||||
[
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": BSH_POWER_ON}},
|
||||
[BSH_POWER_ON, BSH_POWER_OFF],
|
||||
SERVICE_TURN_ON,
|
||||
BSH_POWER_ON,
|
||||
STATE_ON,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": BSH_POWER_OFF}},
|
||||
[BSH_POWER_ON, BSH_POWER_OFF],
|
||||
SERVICE_TURN_OFF,
|
||||
BSH_POWER_OFF,
|
||||
STATE_OFF,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": BSH_POWER_ON}},
|
||||
[BSH_POWER_ON, BSH_POWER_STANDBY],
|
||||
SERVICE_TURN_ON,
|
||||
BSH_POWER_ON,
|
||||
STATE_ON,
|
||||
"Dishwasher",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
{BSH_POWER_STATE: {"value": BSH_POWER_STANDBY}},
|
||||
[BSH_POWER_ON, BSH_POWER_STANDBY],
|
||||
SERVICE_TURN_OFF,
|
||||
BSH_POWER_STANDBY,
|
||||
STATE_OFF,
|
||||
"Dishwasher",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
indirect=["appliance_ha_id"],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_power_swtich(
|
||||
entity_id: str,
|
||||
status: dict,
|
||||
allowed_values: list[str],
|
||||
allowed_values: list[str | None] | None,
|
||||
service: str,
|
||||
setting_value_arg: str,
|
||||
power_state: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance: Mock,
|
||||
get_appliances: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test power switch functionality."""
|
||||
appliance.get.side_effect = [
|
||||
{
|
||||
ATTR_CONSTRAINTS: {
|
||||
ATTR_ALLOWED_VALUES: allowed_values,
|
||||
},
|
||||
}
|
||||
]
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
appliance.status.update(status)
|
||||
get_appliances.return_value = [appliance]
|
||||
client.get_settings.side_effect = None
|
||||
client.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
raw_key=SettingKey.BSH_COMMON_POWER_STATE.value,
|
||||
value="",
|
||||
constraints=SettingConstraints(
|
||||
allowed_values=allowed_values,
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
await hass.services.async_call(SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id})
|
||||
await hass.async_block_till_done()
|
||||
client.set_setting.assert_awaited_once_with(
|
||||
appliance_ha_id,
|
||||
setting_key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
value=setting_value_arg,
|
||||
)
|
||||
assert hass.states.is_state(entity_id, power_state)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "allowed_values", "service", "appliance", "exception_match"),
|
||||
("initial_value"),
|
||||
[
|
||||
(BSH_POWER_OFF),
|
||||
(BSH_POWER_STANDBY),
|
||||
],
|
||||
)
|
||||
async def test_power_switch_fetch_off_state_from_current_value(
|
||||
initial_value: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test power switch functionality to fetch the off state from the current value."""
|
||||
client.get_settings.side_effect = None
|
||||
client.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
raw_key=SettingKey.BSH_COMMON_POWER_STATE.value,
|
||||
value=initial_value,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.is_state("switch.dishwasher_power", STATE_OFF)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "allowed_values", "service", "exception_match"),
|
||||
[
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
[BSH_POWER_ON],
|
||||
SERVICE_TURN_OFF,
|
||||
"Dishwasher",
|
||||
r".*not support.*turn.*off.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
None,
|
||||
SERVICE_TURN_OFF,
|
||||
"Dishwasher",
|
||||
r".*Unable.*turn.*off.*support.*not.*determined.*",
|
||||
),
|
||||
(
|
||||
"switch.dishwasher_power",
|
||||
HomeConnectError(),
|
||||
SERVICE_TURN_OFF,
|
||||
r".*Unable.*turn.*off.*support.*not.*determined.*",
|
||||
),
|
||||
],
|
||||
indirect=["appliance"],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_power_switch_service_validation_errors(
|
||||
entity_id: str,
|
||||
allowed_values: list[str],
|
||||
allowed_values: list[str | None] | None | HomeConnectError,
|
||||
service: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
appliance: Mock,
|
||||
exception_match: str,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test power switch functionality validation errors."""
|
||||
if allowed_values:
|
||||
appliance.get.side_effect = [
|
||||
{
|
||||
ATTR_CONSTRAINTS: {
|
||||
ATTR_ALLOWED_VALUES: allowed_values,
|
||||
},
|
||||
}
|
||||
]
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
get_appliances.return_value = [appliance]
|
||||
client.get_settings.side_effect = None
|
||||
if isinstance(allowed_values, HomeConnectError):
|
||||
exception = allowed_values
|
||||
client.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
raw_key=SettingKey.BSH_COMMON_POWER_STATE.value,
|
||||
value=BSH_POWER_ON,
|
||||
)
|
||||
]
|
||||
)
|
||||
client.get_setting = AsyncMock(side_effect=exception)
|
||||
else:
|
||||
setting = GetSetting(
|
||||
key=SettingKey.BSH_COMMON_POWER_STATE,
|
||||
raw_key=SettingKey.BSH_COMMON_POWER_STATE.value,
|
||||
value=BSH_POWER_ON,
|
||||
constraints=SettingConstraints(
|
||||
allowed_values=allowed_values,
|
||||
),
|
||||
)
|
||||
client.get_settings.return_value = ArrayOfSettings([setting])
|
||||
client.get_setting = AsyncMock(return_value=setting)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
appliance.status.update({BSH_POWER_STATE: {"value": BSH_POWER_ON}})
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=exception_match):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN, service, {"entity_id": entity_id}, blocking=True
|
||||
SWITCH_DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_create_issue(
|
||||
hass: HomeAssistant,
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test we create an issue when an automation or script is using a deprecated entity."""
|
||||
entity_id = "switch.washer_program_mix"
|
||||
appliance.status.update(SETTINGS_STATUS)
|
||||
appliance.get_programs_available.return_value = [PROGRAM]
|
||||
get_appliances.return_value = [appliance]
|
||||
issue_id = f"deprecated_program_switch_{entity_id}"
|
||||
|
||||
assert await async_setup_component(
|
||||
|
@ -539,7 +645,7 @@ async def test_create_issue(
|
|||
)
|
||||
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
"""Tests for home_connect time entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Generator
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import time
|
||||
from unittest.mock import MagicMock, Mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
from aiohomeconnect.model import ArrayOfSettings, GetSetting, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.home_connect.const import ATTR_VALUE
|
||||
from homeassistant.components.time import DOMAIN as TIME_DOMAIN, SERVICE_SET_VALUE
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import get_all_appliances
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
|
@ -26,114 +24,98 @@ def platforms() -> list[str]:
|
|||
|
||||
|
||||
async def test_time(
|
||||
bypass_throttle: Generator[None],
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: Mock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test time entity."""
|
||||
get_appliances.side_effect = get_all_appliances
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
|
||||
@pytest.mark.parametrize("appliance_ha_id", ["Oven"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "setting_key", "setting_value", "expected_state"),
|
||||
("entity_id", "setting_key"),
|
||||
[
|
||||
(
|
||||
f"{TIME_DOMAIN}.oven_alarm_clock",
|
||||
"BSH.Common.Setting.AlarmClock",
|
||||
{ATTR_VALUE: 59},
|
||||
str(time(second=59)),
|
||||
),
|
||||
(
|
||||
f"{TIME_DOMAIN}.oven_alarm_clock",
|
||||
"BSH.Common.Setting.AlarmClock",
|
||||
{ATTR_VALUE: None},
|
||||
"unknown",
|
||||
),
|
||||
(
|
||||
f"{TIME_DOMAIN}.oven_alarm_clock",
|
||||
"BSH.Common.Setting.AlarmClock",
|
||||
None,
|
||||
"unknown",
|
||||
SettingKey.BSH_COMMON_ALARM_CLOCK,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_time_entity_functionality(
|
||||
appliance: Mock,
|
||||
appliance_ha_id: str,
|
||||
entity_id: str,
|
||||
setting_key: str,
|
||||
setting_value: dict,
|
||||
expected_state: str,
|
||||
bypass_throttle: Generator[None],
|
||||
setting_key: SettingKey,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client: MagicMock,
|
||||
) -> None:
|
||||
"""Test time entity functionality."""
|
||||
get_appliances.return_value = [appliance]
|
||||
appliance.status.update({setting_key: setting_value})
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.is_state(entity_id, expected_state)
|
||||
|
||||
new_value = 30
|
||||
assert hass.states.get(entity_id).state != new_value
|
||||
value = 30
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state != value
|
||||
await hass.services.async_call(
|
||||
TIME_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_TIME: time(second=new_value),
|
||||
ATTR_TIME: time(second=value),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
appliance.set_setting.assert_called_once_with(setting_key, new_value)
|
||||
await hass.async_block_till_done()
|
||||
client.set_setting.assert_awaited_once_with(
|
||||
appliance_ha_id, setting_key=setting_key, value=value
|
||||
)
|
||||
assert hass.states.is_state(entity_id, str(time(second=value)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("problematic_appliance", ["Oven"], indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "setting_key", "mock_attr"),
|
||||
[
|
||||
(
|
||||
f"{TIME_DOMAIN}.oven_alarm_clock",
|
||||
"BSH.Common.Setting.AlarmClock",
|
||||
SettingKey.BSH_COMMON_ALARM_CLOCK,
|
||||
"set_setting",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("bypass_throttle")
|
||||
async def test_time_entity_error(
|
||||
problematic_appliance: Mock,
|
||||
entity_id: str,
|
||||
setting_key: str,
|
||||
setting_key: SettingKey,
|
||||
mock_attr: str,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
get_appliances: MagicMock,
|
||||
client_with_exception: MagicMock,
|
||||
) -> None:
|
||||
"""Test time entity error."""
|
||||
get_appliances.return_value = [problematic_appliance]
|
||||
|
||||
client_with_exception.get_settings.side_effect = None
|
||||
client_with_exception.get_settings.return_value = ArrayOfSettings(
|
||||
[
|
||||
GetSetting(
|
||||
key=setting_key,
|
||||
raw_key=setting_key.value,
|
||||
value=30,
|
||||
)
|
||||
]
|
||||
)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
problematic_appliance.status.update({setting_key: {}})
|
||||
assert await integration_setup()
|
||||
assert await integration_setup(client_with_exception)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with pytest.raises(HomeConnectError):
|
||||
getattr(problematic_appliance, mock_attr)()
|
||||
await getattr(client_with_exception, mock_attr)()
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError, match=r"Error.*assign.*value.*to.*setting.*"
|
||||
|
@ -147,4 +129,4 @@ async def test_time_entity_error(
|
|||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert getattr(problematic_appliance, mock_attr).call_count == 2
|
||||
assert getattr(client_with_exception, mock_attr).call_count == 2
|
||||
|
|
Loading…
Reference in New Issue