mirror of
https://github.com/home-assistant/core.git
synced 2026-05-21 00:05:16 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7dfef5c82a | |||
| b75cd0f6a7 | |||
| 7859aba432 |
Generated
+2
@@ -464,6 +464,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/electrasmart/ @jafar-atili
|
||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||
/tests/components/electric_kiwi/ @mikey0000
|
||||
/homeassistant/components/electrolux/ @electrolux-oss
|
||||
/tests/components/electrolux/ @electrolux-oss
|
||||
/homeassistant/components/elevenlabs/ @sorgfresser
|
||||
/tests/components/elevenlabs/ @sorgfresser
|
||||
/homeassistant/components/elgato/ @frenck
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""The Electrolux integration."""
|
||||
|
||||
from asyncio import CancelledError
|
||||
from collections.abc import Awaitable, Callable
|
||||
import logging
|
||||
|
||||
from electrolux_group_developer_sdk.auth.token_manager import TokenManager
|
||||
from electrolux_group_developer_sdk.client.appliance_client import ApplianceClient
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.client_exception import (
|
||||
ApplianceClientException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DOMAIN, NEW_APPLIANCE_SIGNAL, USER_AGENT
|
||||
from .coordinator import (
|
||||
ElectroluxConfigEntry,
|
||||
ElectroluxData,
|
||||
ElectroluxDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ElectroluxConfigEntry) -> bool:
|
||||
"""Set up Electrolux integration entry."""
|
||||
|
||||
token_manager = create_token_manager(hass, entry)
|
||||
client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
|
||||
try:
|
||||
await client.test_connection()
|
||||
except BadCredentialsException as e:
|
||||
raise ConfigEntryAuthFailed("Bad credentials detected.") from e
|
||||
except FailedConnectionException as e:
|
||||
raise ConfigEntryNotReady("Connection with client failed.") from e
|
||||
|
||||
try:
|
||||
appliances = await fetch_appliance_data(client)
|
||||
except ApplianceClientException as e:
|
||||
raise ConfigEntryNotReady from e
|
||||
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator] = {}
|
||||
on_livestream_opening_callback_list: list[Callable[[], Awaitable[None]]] = []
|
||||
|
||||
async def check_for_new_devices_callback() -> None:
|
||||
"""Trigger _check_for_new_devices asynchronously."""
|
||||
await _check_for_new_devices(
|
||||
hass, entry, client, on_livestream_opening_callback_list
|
||||
)
|
||||
|
||||
on_livestream_opening_callback_list.append(check_for_new_devices_callback)
|
||||
|
||||
for appliance in appliances:
|
||||
appliance_id = appliance.appliance.applianceId
|
||||
coordinator = ElectroluxDataUpdateCoordinator(
|
||||
hass, entry, client=client, appliance_id=appliance_id
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Subscribe this coordinator to its appliance events
|
||||
coordinator.add_client_listener()
|
||||
|
||||
coordinators[appliance_id] = coordinator
|
||||
# Device state is refreshed whenever the SSE connection opens.
|
||||
on_livestream_opening_callback_list.append(coordinator.async_refresh)
|
||||
|
||||
sse_task = entry.async_create_background_task(
|
||||
hass,
|
||||
client.start_event_stream(on_livestream_opening_callback_list),
|
||||
"electrolux event listener",
|
||||
)
|
||||
|
||||
entry.runtime_data = ElectroluxData(
|
||||
client=client,
|
||||
appliances=appliances,
|
||||
coordinators=coordinators,
|
||||
sse_task=sse_task,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElectroluxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Remove SSE listeners
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
for coordinator in coordinators.values():
|
||||
coordinator.remove_client_listeners()
|
||||
|
||||
# Cancel SSE task
|
||||
sse_task = entry.runtime_data.sse_task
|
||||
sse_task.cancel()
|
||||
try:
|
||||
await sse_task
|
||||
except CancelledError:
|
||||
_LOGGER.info("SSE stream cancelled for entry %s", entry.entry_id)
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def create_token_manager(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
) -> TokenManager:
|
||||
"""Create a token manager for the Electrolux integration."""
|
||||
|
||||
def save_tokens(new_access: str, new_refresh: str, new_api_key: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_API_KEY: new_api_key,
|
||||
CONF_ACCESS_TOKEN: new_access,
|
||||
CONF_REFRESH_TOKEN: new_refresh,
|
||||
},
|
||||
)
|
||||
|
||||
api_key = entry.data.get(CONF_API_KEY)
|
||||
refresh_token = entry.data.get(CONF_REFRESH_TOKEN)
|
||||
access_token = entry.data.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if access_token and refresh_token and api_key:
|
||||
return TokenManager(access_token, refresh_token, api_key, save_tokens)
|
||||
raise ConfigEntryAuthFailed("Missing access token, refresh token or API key")
|
||||
|
||||
|
||||
async def _check_for_new_devices(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
client: ApplianceClient,
|
||||
on_livestream_opening_callback_list: list[Callable[[], Awaitable[None]]],
|
||||
) -> None:
|
||||
"""Fetch appliances from API and trigger discovery for any new ones."""
|
||||
_LOGGER.info("Checking for new devices")
|
||||
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
appliances = await fetch_appliance_data(client)
|
||||
entry.runtime_data.appliances = appliances
|
||||
|
||||
existing_ids = set(coordinators.keys())
|
||||
|
||||
for appliance in appliances:
|
||||
appliance_id = appliance.appliance.applianceId
|
||||
# Detect NEW appliances
|
||||
if appliance_id not in existing_ids:
|
||||
# Create coordinator for appliance
|
||||
coordinator = ElectroluxDataUpdateCoordinator(
|
||||
hass, entry, client=client, appliance_id=appliance_id
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
coordinator.add_client_listener()
|
||||
coordinators[appliance_id] = coordinator
|
||||
on_livestream_opening_callback_list.append(coordinator.async_refresh)
|
||||
|
||||
# Notify all platforms
|
||||
async_dispatcher_send(
|
||||
hass, f"{NEW_APPLIANCE_SIGNAL}_{entry.entry_id}", appliance
|
||||
)
|
||||
|
||||
# Detect MISSING appliances
|
||||
discovered_ids = {appliance.appliance.applianceId for appliance in appliances}
|
||||
missing_ids = existing_ids - discovered_ids
|
||||
device_registry = dr.async_get(hass)
|
||||
for missing_id in missing_ids:
|
||||
_LOGGER.warning("Appliance %s no longer found, removing", missing_id)
|
||||
|
||||
# Remove coordinator
|
||||
coordinator = coordinators.pop(missing_id)
|
||||
coordinator.remove_client_listeners()
|
||||
on_livestream_opening_callback_list.remove(coordinator.async_refresh)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, missing_id)}
|
||||
)
|
||||
|
||||
if device_entry:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def fetch_appliance_data(client: ApplianceClient) -> list[ApplianceData]:
|
||||
"""Helper method to retrieve all the appliances data from the Electrolux APIs."""
|
||||
try:
|
||||
appliances = await client.get_appliance_data()
|
||||
except ApplianceClientException as e:
|
||||
_LOGGER.warning("Failed to get appliances: %s", e)
|
||||
raise
|
||||
|
||||
# Filter out appliances where details or state is None
|
||||
return [
|
||||
appliance
|
||||
for appliance in appliances
|
||||
if appliance.details is not None and appliance.state is not None
|
||||
]
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Config flow for Electrolux integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from electrolux_group_developer_sdk.auth.invalid_credentials_exception import (
|
||||
InvalidCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.auth.token_manager import TokenManager
|
||||
from electrolux_group_developer_sdk.client.appliance_client import ApplianceClient
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DOMAIN, USER_AGENT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for the Electrolux integration."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step of the config flow."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
token_manager: TokenManager
|
||||
email: str
|
||||
try:
|
||||
token_manager = await _authenticate_user(user_input)
|
||||
client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
email = (await client.get_user_email()).email
|
||||
except InvalidCredentialsException, BadCredentialsException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except FailedConnectionException:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(token_manager.get_user_id())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"Electrolux for {email}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self._show_form(step_id="user", errors=errors)
|
||||
|
||||
def _show_form(self, step_id: str, errors: dict[str, str]) -> ConfigFlowResult:
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_ACCESS_TOKEN): str,
|
||||
vol.Required(CONF_REFRESH_TOKEN): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"portal_link": "https://developer.electrolux.one/generateToken"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _authenticate_user(user_input: Mapping[str, Any]) -> TokenManager:
|
||||
token_manager = TokenManager(
|
||||
access_token=user_input[CONF_ACCESS_TOKEN],
|
||||
refresh_token=user_input[CONF_REFRESH_TOKEN],
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
)
|
||||
|
||||
token_manager.ensure_credentials()
|
||||
|
||||
appliance_client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
|
||||
# Test a connection in the config flow
|
||||
await appliance_client.test_connection()
|
||||
|
||||
return token_manager
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Constants for Electrolux integration."""
|
||||
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
|
||||
DOMAIN = "electrolux"
|
||||
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
NEW_APPLIANCE_SIGNAL = "electrolux_new_appliance"
|
||||
|
||||
USER_AGENT = f"HomeAssistant/{HA_VERSION}"
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Electrolux coordinator class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import Task
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliance_client import (
|
||||
ApplianceClient,
|
||||
apply_sse_update,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.client_exception import (
|
||||
ApplianceClientException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.dto.appliance_state import ApplianceState
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class ElectroluxData:
|
||||
"""Electrolux data type."""
|
||||
|
||||
client: ApplianceClient
|
||||
appliances: list[ApplianceData]
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator]
|
||||
sse_task: Task
|
||||
|
||||
|
||||
type ElectroluxConfigEntry = ConfigEntry[ElectroluxData]
|
||||
|
||||
|
||||
class ElectroluxDataUpdateCoordinator(DataUpdateCoordinator[ApplianceState]):
|
||||
"""Class for fetching appliance data from the API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ElectroluxConfigEntry,
|
||||
client: ApplianceClient,
|
||||
appliance_id: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.client = client
|
||||
self._appliance_id = appliance_id
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}_{config_entry.entry_id}_{appliance_id}",
|
||||
update_interval=None,
|
||||
always_update=False,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> ApplianceState:
|
||||
"""Return the current appliance state (SSE keeps it updated)."""
|
||||
try:
|
||||
appliance_state = await self.client.get_appliance_state(self._appliance_id)
|
||||
except ValueError as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
except ApplianceClientException as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
else:
|
||||
return appliance_state
|
||||
|
||||
def add_client_listener(self) -> None:
|
||||
"""Register an SSE listener to the appliance client for appliance state updates."""
|
||||
self.client.add_listener(self._appliance_id, self.callback_handle_event)
|
||||
|
||||
def remove_client_listeners(self) -> None:
|
||||
"""Remove all SSE listeners."""
|
||||
self.client.remove_all_listeners_by_appliance_id(self._appliance_id)
|
||||
|
||||
def callback_handle_event(self, event: dict) -> None:
|
||||
"""Handle an incoming SSE event. Event will look like: {"userId": "...", "applianceId": "...", "property": "timeToEnd", "value": 720}."""
|
||||
|
||||
current_state = self.data
|
||||
if not current_state:
|
||||
return
|
||||
|
||||
updated_state = apply_sse_update(
|
||||
current_state,
|
||||
event,
|
||||
)
|
||||
|
||||
self.async_set_updated_data(updated_state)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Base entity for Electrolux integration."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ElectroluxDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxBaseEntity[T: ApplianceData](
|
||||
CoordinatorEntity[ElectroluxDataUpdateCoordinator]
|
||||
):
|
||||
"""Base class for Electrolux entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: T,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
unique_id_suffix: str,
|
||||
) -> None:
|
||||
"""Initialize the base device."""
|
||||
super().__init__(coordinator)
|
||||
appliance_name = appliance_data.appliance.applianceName
|
||||
appliance_id = appliance_data.appliance.applianceId
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert appliance_data.details
|
||||
assert appliance_data.state
|
||||
|
||||
appliance_info = appliance_data.details.applianceInfo
|
||||
|
||||
self._appliance_data = appliance_data
|
||||
self._attr_unique_id = f"{appliance_id}_{unique_id_suffix}"
|
||||
self._appliance_id = appliance_id
|
||||
self._appliance_capabilities = appliance_data.details.capabilities
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, appliance_id)},
|
||||
name=appliance_name,
|
||||
manufacturer=appliance_info.brand,
|
||||
model=appliance_info.model,
|
||||
serial_number=appliance_info.serialNumber,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to HA."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
|
||||
@abstractmethod
|
||||
def _update_attr_state(self) -> bool:
|
||||
"""Update entity-specific attributes. Returns True if any attributes were changed, otherwise False."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""When the coordinator updates."""
|
||||
appliance_state = self.coordinator.data
|
||||
if not appliance_state:
|
||||
_LOGGER.warning("Appliance %s not found in update", self._appliance_id)
|
||||
return
|
||||
|
||||
# Update state
|
||||
self._appliance_data.update_state(appliance_state)
|
||||
state_changed = self._update_attr_state()
|
||||
|
||||
if state_changed:
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Contains entity helper methods."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import NEW_APPLIANCE_SIGNAL
|
||||
from .coordinator import ElectroluxConfigEntry, ElectroluxDataUpdateCoordinator
|
||||
from .entity import ElectroluxBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entities_helper(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
build_entities_fn: Callable[
|
||||
[ApplianceData, dict[str, ElectroluxDataUpdateCoordinator]],
|
||||
list[ElectroluxBaseEntity],
|
||||
],
|
||||
) -> None:
|
||||
"""Provide async_setup_entry helper."""
|
||||
|
||||
appliances: list[ApplianceData] = entry.runtime_data.appliances
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
|
||||
entities: list[ElectroluxBaseEntity] = []
|
||||
|
||||
for appliance_data in appliances:
|
||||
entities.extend(build_entities_fn(appliance_data, coordinators))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
# Listen for new/removed appliances
|
||||
async def _new_appliance(appliance_data: ApplianceData):
|
||||
new_entities = build_entities_fn(appliance_data, coordinators)
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{NEW_APPLIANCE_SIGNAL}_{entry.entry_id}", _new_appliance
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"appliance_state": {
|
||||
"default": "mdi:information-outline"
|
||||
},
|
||||
"food_probe_state": {
|
||||
"default": "mdi:thermometer-probe"
|
||||
},
|
||||
"food_probe_temperature": {
|
||||
"default": "mdi:thermometer-probe"
|
||||
},
|
||||
"remote_control": {
|
||||
"default": "mdi:remote"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "electrolux",
|
||||
"name": "Electrolux",
|
||||
"codeowners": ["@electrolux-oss"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/electrolux",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["electrolux-group-developer-sdk==0.5.0"]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions are implemented currently.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: |
|
||||
Polling is only performed on infrequent events (when the livestream of events is opened, in order to sync),
|
||||
otherwise the integration works via push
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions are implemented currently.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: todo
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Sensor entity for Electrolux Integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.appliances.cr_appliance import CRAppliance
|
||||
from electrolux_group_developer_sdk.client.appliances.ov_appliance import OVAppliance
|
||||
from electrolux_group_developer_sdk.feature_constants import (
|
||||
APPLIANCE_STATE,
|
||||
DISPLAY_FOOD_PROBE_TEMPERATURE_C,
|
||||
DISPLAY_FOOD_PROBE_TEMPERATURE_F,
|
||||
DISPLAY_TEMPERATURE_C,
|
||||
DISPLAY_TEMPERATURE_F,
|
||||
FOOD_PROBE_STATE,
|
||||
REMOTE_CONTROL,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .coordinator import ElectroluxConfigEntry, ElectroluxDataUpdateCoordinator
|
||||
from .entity import ElectroluxBaseEntity
|
||||
from .entity_helper import async_setup_entities_helper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ELECTROLUX_TO_HA_TEMPERATURE_UNIT = {
|
||||
"CELSIUS": UnitOfTemperature.CELSIUS,
|
||||
"FAHRENHEIT": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ElectroluxSensorDescription(SensorEntityDescription):
|
||||
"""Custom sensor description for Electrolux sensors."""
|
||||
|
||||
value_fn: Callable[..., StateType]
|
||||
exists_fn: Callable[[ApplianceData], bool] = lambda *args: True
|
||||
feature_name: str | None = None
|
||||
known_values: set[str] | None = None
|
||||
|
||||
|
||||
OVEN_ELECTROLUX_SENSORS: tuple[ElectroluxSensorDescription, ...] = (
|
||||
ElectroluxSensorDescription(
|
||||
key="appliance_state",
|
||||
translation_key="appliance_state",
|
||||
value_fn=lambda appliance: appliance.get_current_appliance_state(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=APPLIANCE_STATE,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(APPLIANCE_STATE),
|
||||
known_values={
|
||||
"alarm",
|
||||
"delayed_start",
|
||||
"end_of_cycle",
|
||||
"idle",
|
||||
"off",
|
||||
"paused",
|
||||
"ready_to_start",
|
||||
"running",
|
||||
},
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="food_probe_state",
|
||||
translation_key="food_probe_state",
|
||||
value_fn=lambda appliance: appliance.get_current_food_probe_insertion_state(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=FOOD_PROBE_STATE,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(FOOD_PROBE_STATE),
|
||||
known_values={
|
||||
"inserted",
|
||||
"not_inserted",
|
||||
},
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="remote_control",
|
||||
translation_key="remote_control",
|
||||
value_fn=lambda appliance: appliance.get_current_remote_control(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=REMOTE_CONTROL,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(REMOTE_CONTROL),
|
||||
known_values={
|
||||
"disabled",
|
||||
"enabled",
|
||||
"not_safety_relevant_enabled",
|
||||
"temporary_locked",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
OVEN_TEMPERATURE_ELECTROLUX_SENSORS: tuple[ElectroluxSensorDescription, ...] = (
|
||||
ElectroluxSensorDescription(
|
||||
key="food_probe_temperature",
|
||||
translation_key="food_probe_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda appliance, temp_unit=None: (
|
||||
appliance.get_current_display_food_probe_temperature_f()
|
||||
if temp_unit == UnitOfTemperature.FAHRENHEIT
|
||||
else appliance.get_current_display_food_probe_temperature_c()
|
||||
),
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(
|
||||
[DISPLAY_FOOD_PROBE_TEMPERATURE_F, DISPLAY_FOOD_PROBE_TEMPERATURE_C]
|
||||
),
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="display_temperature",
|
||||
translation_key="display_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda appliance, temp_unit=None: (
|
||||
appliance.get_current_display_temperature_f()
|
||||
if temp_unit == UnitOfTemperature.FAHRENHEIT
|
||||
else appliance.get_current_display_temperature_c()
|
||||
),
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(
|
||||
[DISPLAY_TEMPERATURE_C, DISPLAY_TEMPERATURE_F]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_entities_for_appliance(
|
||||
appliance_data: ApplianceData,
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator],
|
||||
) -> list[ElectroluxBaseEntity]:
|
||||
"""Return all entities for a single appliance."""
|
||||
appliance = appliance_data.appliance
|
||||
coordinator = coordinators[appliance.applianceId]
|
||||
entities: list[ElectroluxBaseEntity] = []
|
||||
|
||||
if isinstance(appliance_data, OVAppliance):
|
||||
entities.extend(
|
||||
ElectroluxSensor(appliance_data, coordinator, description)
|
||||
for description in OVEN_ELECTROLUX_SENSORS
|
||||
if description.exists_fn(appliance_data)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
ElectroluxTemperatureSensor(appliance_data, coordinator, description)
|
||||
for description in OVEN_TEMPERATURE_ELECTROLUX_SENSORS
|
||||
if description.exists_fn(appliance_data)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set sensor for Electrolux Integration."""
|
||||
await async_setup_entities_helper(
|
||||
hass, entry, async_add_entities, build_entities_for_appliance
|
||||
)
|
||||
|
||||
|
||||
class ElectroluxSensor(ElectroluxBaseEntity[ApplianceData], SensorEntity):
|
||||
"""Representation of a generic sensor for Electrolux appliances."""
|
||||
|
||||
entity_description: ElectroluxSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: ApplianceData,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
description: ElectroluxSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(appliance_data, coordinator, description.key)
|
||||
|
||||
if (
|
||||
description.feature_name is not None
|
||||
and description.known_values is not None
|
||||
):
|
||||
options = appliance_data.get_feature_state_string_options(
|
||||
description.feature_name
|
||||
)
|
||||
snake_case_options = [
|
||||
snake_case_option
|
||||
for option in options
|
||||
if (snake_case_option := _convert_to_snake_case(option))
|
||||
in description.known_values
|
||||
]
|
||||
|
||||
if len(snake_case_options) > 0:
|
||||
self._attr_options = snake_case_options
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
def _update_attr_state(self) -> bool:
|
||||
new_value = self._get_value()
|
||||
if isinstance(new_value, str):
|
||||
new_value = _convert_to_snake_case(new_value)
|
||||
|
||||
if self.entity_description.known_values:
|
||||
new_value = _map_to_known_value(
|
||||
self.entity_description.known_values,
|
||||
self.entity_description.key,
|
||||
new_value,
|
||||
)
|
||||
|
||||
if self._attr_native_value != new_value:
|
||||
self._attr_native_value = new_value
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_value(self) -> StateType:
|
||||
return self.entity_description.value_fn(self._appliance_data)
|
||||
|
||||
|
||||
class ElectroluxTemperatureSensor(ElectroluxSensor):
|
||||
"""Representation of a temperature sensor for Electrolux appliances."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: ApplianceData,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
description: ElectroluxSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._appliance = cast(OVAppliance | CRAppliance, appliance_data)
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
super().__init__(appliance_data, coordinator, description)
|
||||
|
||||
def _get_value(self) -> StateType:
|
||||
temp_unit = self._get_temperature_unit()
|
||||
temp_value: float | None = cast(
|
||||
float | None,
|
||||
self.entity_description.value_fn(self._appliance_data, temp_unit=temp_unit),
|
||||
)
|
||||
if temp_value is None:
|
||||
return None
|
||||
return TemperatureConverter.convert(
|
||||
temp_value, temp_unit, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
def _get_temperature_unit(self) -> UnitOfTemperature:
|
||||
temp_unit = self._appliance.get_current_temperature_unit()
|
||||
|
||||
if temp_unit is not None:
|
||||
temp_unit = temp_unit.upper()
|
||||
|
||||
return ELECTROLUX_TO_HA_TEMPERATURE_UNIT.get(
|
||||
temp_unit, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_snake_case(x: str) -> str:
|
||||
"""Converts a string to snake case."""
|
||||
lower_case = x.lower()
|
||||
return "".join([_convert_char_to_snake_case(char) for char in lower_case])
|
||||
|
||||
|
||||
def _convert_char_to_snake_case(char: str) -> str:
|
||||
if char.isspace():
|
||||
return "_"
|
||||
return char
|
||||
|
||||
|
||||
def _map_to_known_value(
|
||||
known_values: set[str], entity_key: str, value: str
|
||||
) -> str | None:
|
||||
"""Return provided value if it is known, otherwise log warn message and return None."""
|
||||
if value not in known_values:
|
||||
_LOGGER.warning(
|
||||
"An unknown value %s was reported for a sensor of the Electrolux integration. "
|
||||
"Please report it for the integration, and include the following information: "
|
||||
'entity key="%s", reported value="%s"',
|
||||
value,
|
||||
entity_key,
|
||||
value,
|
||||
)
|
||||
return None
|
||||
return value
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This Electrolux account is already configured."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to the Electrolux API. Please check credentials and try again.",
|
||||
"invalid_auth": "Authentication failed. Please check your credentials."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"refresh_token": "Refresh token"
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "The access token from Electrolux Group for Developer.",
|
||||
"api_key": "Your Electrolux Group for Developer API key.",
|
||||
"refresh_token": "The refresh token used to renew your access token."
|
||||
},
|
||||
"description": "Please go to the [developer portal]({portal_link}) to generate new access and refresh tokens, then paste them below.",
|
||||
"title": "Configure your Electrolux Group account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"appliance_state": {
|
||||
"name": "Appliance state",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"delayed_start": "Delayed start",
|
||||
"end_of_cycle": "Cycle ended",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"paused": "[%key:common::state::paused%]",
|
||||
"ready_to_start": "Ready to start",
|
||||
"running": "Running"
|
||||
}
|
||||
},
|
||||
"display_temperature": {
|
||||
"name": "Current temperature"
|
||||
},
|
||||
"food_probe_state": {
|
||||
"name": "Food probe state",
|
||||
"state": {
|
||||
"inserted": "Inserted",
|
||||
"not_inserted": "Not inserted"
|
||||
}
|
||||
},
|
||||
"food_probe_temperature": {
|
||||
"name": "Food probe temperature"
|
||||
},
|
||||
"remote_control": {
|
||||
"name": "Remote control",
|
||||
"state": {
|
||||
"disabled": "[%key:common::state::disabled%]",
|
||||
"enabled": "[%key:common::state::enabled%]",
|
||||
"not_safety_relevant_enabled": "Not safety relevant enabled",
|
||||
"temporary_locked": "Temporarily locked"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -191,6 +191,7 @@ FLOWS = {
|
||||
"ekeybionyx",
|
||||
"electrasmart",
|
||||
"electric_kiwi",
|
||||
"electrolux",
|
||||
"elevenlabs",
|
||||
"elgato",
|
||||
"elkm1",
|
||||
|
||||
@@ -1702,6 +1702,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"electrolux": {
|
||||
"name": "Electrolux",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"elevenlabs": {
|
||||
"name": "ElevenLabs",
|
||||
"integration_type": "service",
|
||||
|
||||
Generated
+3
@@ -891,6 +891,9 @@ ekey-bionyxpy==1.0.1
|
||||
# homeassistant.components.electric_kiwi
|
||||
electrickiwi-api==0.9.14
|
||||
|
||||
# homeassistant.components.electrolux
|
||||
electrolux-group-developer-sdk==0.5.0
|
||||
|
||||
# homeassistant.components.elevenlabs
|
||||
elevenlabs==2.3.0
|
||||
|
||||
|
||||
Generated
+3
@@ -797,6 +797,9 @@ ekey-bionyxpy==1.0.1
|
||||
# homeassistant.components.electric_kiwi
|
||||
electrickiwi-api==0.9.14
|
||||
|
||||
# homeassistant.components.electrolux
|
||||
electrolux-group-developer-sdk==0.5.0
|
||||
|
||||
# homeassistant.components.elevenlabs
|
||||
elevenlabs==2.3.0
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Tests for the electrolux integration."""
|
||||
|
||||
from functools import cache
|
||||
|
||||
from electrolux_group_developer_sdk.client.dto.appliance import Appliance
|
||||
from electrolux_group_developer_sdk.client.dto.appliance_details import ApplianceDetails
|
||||
from electrolux_group_developer_sdk.client.dto.appliance_state import ApplianceState
|
||||
|
||||
from homeassistant.components.electrolux.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
APPLIANCE_FIXTURES = ["fenix_oven", "pux_oven"]
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Set up Electrolux integration for tests."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@cache
|
||||
def get_fixture_name(appliance_id: str) -> str:
|
||||
"""Get the fixture name for the given appliance ID."""
|
||||
for name in APPLIANCE_FIXTURES:
|
||||
if load_appliance(name).applianceId == appliance_id:
|
||||
return name
|
||||
|
||||
raise KeyError(f"Fixture name for appliance ID {appliance_id} does not exist")
|
||||
|
||||
|
||||
def load_appliance(appliance_name: str) -> Appliance:
|
||||
"""Load an Appliance object from a fixture for the given appliance name."""
|
||||
json_string = load_fixture(f"appliances/{appliance_name}.json", DOMAIN)
|
||||
return Appliance.model_validate_json(json_string)
|
||||
|
||||
|
||||
def load_appliance_details(appliance_name: str) -> ApplianceDetails:
|
||||
"""Load an ApplianceDetails object from a fixture for the given appliance name."""
|
||||
json_string = load_fixture(f"appliance_details/{appliance_name}.json", DOMAIN)
|
||||
return ApplianceDetails.model_validate_json(json_string)
|
||||
|
||||
|
||||
def load_appliance_state(appliance_name: str) -> ApplianceState:
|
||||
"""Load an ApplianceState object from a fixture for the given appliance name."""
|
||||
json_string = load_fixture(f"appliance_states/{appliance_name}.json", DOMAIN)
|
||||
return ApplianceState.model_validate_json(json_string)
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Common fixtures for the electrolux tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliance_data_factory import (
|
||||
appliance_data_factory,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.dto.appliance_state import ApplianceState
|
||||
from electrolux_group_developer_sdk.client.dto.email import Email
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.electrolux.const import CONF_REFRESH_TOKEN, DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
APPLIANCE_FIXTURES,
|
||||
get_fixture_name,
|
||||
load_appliance,
|
||||
load_appliance_details,
|
||||
load_appliance_state,
|
||||
setup_integration,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.electrolux.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Set up Electrolux integration for tests."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Create mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Electrolux",
|
||||
unique_id="mock_user_id",
|
||||
data={
|
||||
CONF_API_KEY: "mock_api_key",
|
||||
CONF_ACCESS_TOKEN: "mock_access_token",
|
||||
CONF_REFRESH_TOKEN: "mock_refresh_token",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_appliance_client() -> Generator[AsyncMock]:
|
||||
"""Mock the Electrolux Group Developer SDK client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.electrolux.ApplianceClient",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.electrolux.config_flow.ApplianceClient",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
|
||||
def get_appliance_state(appliance_id: str) -> ApplianceState | None:
|
||||
return load_appliance_state(get_fixture_name(appliance_id))
|
||||
|
||||
client.get_appliance_state.side_effect = get_appliance_state
|
||||
|
||||
client.get_user_email.return_value = Email(email="mock@email.com")
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_token_manager() -> Generator[AsyncMock]:
|
||||
"""Mock the Electrolux Group Developer SDK token manager."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.electrolux.TokenManager",
|
||||
autospec=True,
|
||||
) as mock_token_manager,
|
||||
patch(
|
||||
"homeassistant.components.electrolux.config_flow.TokenManager",
|
||||
new=mock_token_manager,
|
||||
),
|
||||
):
|
||||
token_manager = mock_token_manager.return_value
|
||||
|
||||
token_manager.ensure_credentials.return_value = None
|
||||
token_manager.get_user_id.return_value = "mock_user_id"
|
||||
|
||||
yield token_manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def appliance_fixture() -> str | None:
|
||||
"""Return the appliance fixture that should be loaded, or None if all appliances should be loaded."""
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def appliances(
|
||||
mock_appliance_client: AsyncMock, appliance_fixture: str | None
|
||||
) -> AsyncMock:
|
||||
"""Mock the list of appliances."""
|
||||
appliance_names = []
|
||||
if appliance_fixture is not None:
|
||||
appliance_names.append(appliance_fixture)
|
||||
else:
|
||||
appliance_names.extend(APPLIANCE_FIXTURES)
|
||||
|
||||
appliance_data_list = []
|
||||
for appliance_name in appliance_names:
|
||||
appliance = load_appliance(appliance_name)
|
||||
details = load_appliance_details(appliance_name)
|
||||
state = load_appliance_state(appliance_name)
|
||||
|
||||
appliance_data = appliance_data_factory(
|
||||
appliance=appliance,
|
||||
details=details,
|
||||
state=state,
|
||||
)
|
||||
|
||||
appliance_data_list.append(appliance_data)
|
||||
|
||||
mock_appliance_client.get_appliance_data.return_value = appliance_data_list
|
||||
|
||||
return mock_appliance_client
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,874 @@
|
||||
{
|
||||
"applianceInfo": {
|
||||
"serialNumber": "11112225",
|
||||
"pnc": "949288049",
|
||||
"brand": "AEG",
|
||||
"deviceType": "BUILT-IN OVEN",
|
||||
"model": "NBX7P631SB",
|
||||
"variant": "PIZZA-HOTAIRFAN+RING",
|
||||
"colour": "BLACK"
|
||||
},
|
||||
"capabilities": {
|
||||
"alerts": {
|
||||
"access": "read",
|
||||
"type": "alert",
|
||||
"values": {
|
||||
"AD_CONVERTER_REFERENCE_ALARM": {},
|
||||
"BACKLIGHT_ALARM": {},
|
||||
"BOARD_TEMPERATURE_ALARM": {},
|
||||
"COMMUNICATION_ALARM_BETWEEN_TWO_CONTROLLERS": {},
|
||||
"COMMUNICATION_ALARM_OUI_OC": {},
|
||||
"CONFIGURATION_CHECKSUM_ALARM": {},
|
||||
"CONFIGURATION_COHERENT_ALARM": {},
|
||||
"CONFIGURATION_COMPATIBILITY_ALARM": {},
|
||||
"COOKING_FAN_CONFIG_ALARM": {},
|
||||
"DATA_FLASH_ALARM_UI": {},
|
||||
"DOOR_LOCK_ACTUATOR": {},
|
||||
"DOOR_LOCK_CONFIGURATION_ALARM": {},
|
||||
"DOOR_LOCK_SENSOR_ALARM": {},
|
||||
"ELECTRONIC_CLIXON_ALARM": {},
|
||||
"FIX_SENSOR_DETECTION_ALARM": {},
|
||||
"FOOD_PROBE_COMMUNICATION_ALARM": {},
|
||||
"FOOD_PROBE_CONFIGURATION_ALARM": {},
|
||||
"FUNCTION_SELECTOR_NOT_CONNECTED": {},
|
||||
"HMI-TOUCH_BOARD_FMEA_ALARM": {},
|
||||
"HMI-TOUCH_BOARD_SERIAL_COMMUNICATION_ALARM": {},
|
||||
"HOB_OVEN_COMMUNICATION_ALARM": {},
|
||||
"HOB_OVEN_POWER_MANAGEMENT_ALARM": {},
|
||||
"HUMIDITY_SENSOR_OUT_OF_RANGE_ALARM": {},
|
||||
"INTERNAL_ERROR": {},
|
||||
"LIB_FMEA_ALARM_ROTARY_GRAB": {},
|
||||
"MACS_COMMNUNICATION_ERROR": {},
|
||||
"MEAT_PROBE_OUT_OF_RANGE_ALARM": {},
|
||||
"NETVM_COMMUNICATION_ALARM": {},
|
||||
"NIUX_COMMUNICATION_ALARM": {},
|
||||
"NIUX_ONBOARDING_FAILED_ALARM": {},
|
||||
"NTC_OUT_OF_RANGE_ALARM": {},
|
||||
"OTA_FAILURE": {},
|
||||
"PERIPHERAL_INIT_ALARM_ROTARY_SERIAL_COMMUNICATION_ALARM": {},
|
||||
"PERIPHERAL_RUNTIME_ALARM_ROTARY_BIT_ENCODER_ALARM": {},
|
||||
"POWER_ALARM": {},
|
||||
"PT500_OUT_OF_RANGE_ALARM": {},
|
||||
"PT500_STEAM_OUT_OF_RANGE_ALARM": {},
|
||||
"PTO_COMMUNICATION_ALARM_OUI_PTO": {},
|
||||
"PTO_CONFIGURATION_CHECKSUM_ALARM": {},
|
||||
"PTO_KEY_ALARM": {},
|
||||
"PTO_LIB_FMEA_ALARM": {},
|
||||
"PYR_HOB_ALARM": {},
|
||||
"ROTARY_TOUCH_ALARM": {},
|
||||
"RTC_ALARM": {},
|
||||
"RTC_OUT_OF_RANGE_ALARM": {},
|
||||
"SMART_AD_CALIBRATION_RUNNING_ERROR": {},
|
||||
"SMART_ERROR_UNKNOWN": {},
|
||||
"SMART_INVALID_CONFIGURATION_ERROR": {},
|
||||
"SMART_NO_START_TEMPERATURE": {},
|
||||
"SMART_READ_FLASH_ERROR": {},
|
||||
"SOFTWARE_COMPATIBILITY_CODE_ALARM": {},
|
||||
"STEAM_MAGNETRON_NTC": {},
|
||||
"TOO_HIGH_TEMPERATURE_ALARM": {},
|
||||
"TOUCHSCREEN_DRIVER_ALARM": {},
|
||||
"TOUCH_KEY_1_ALARM": {},
|
||||
"TOUCH_KEY_ALARM": {},
|
||||
"TRIAC_ALARM": {},
|
||||
"UNKNOWN_STATE_ERROR": {},
|
||||
"USB_CAMERA_DISCONNECTED_ALARM": {},
|
||||
"WATER_LEVEL_SENSOR_IN_STEAMER_OUT_OF_RANGE_ALARM": {},
|
||||
"WATER_LEVEL_SENSOR_IN_STEAM_TANK_DRAWER_OUT_OF_RANGE_ALARM": {},
|
||||
"WIFI_SIGNAL_MISS_ALARM": {}
|
||||
}
|
||||
},
|
||||
"applianceState": {
|
||||
"access": "read",
|
||||
"triggers": [
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"values": {
|
||||
"STOPRESET": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "RUNNING",
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"values": {
|
||||
"STOPRESET": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "PAUSED",
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"values": {
|
||||
"START": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "READY_TO_START",
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"$self": {
|
||||
"access": "read"
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "ALARM",
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"values": {
|
||||
"STOPRESET": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "DELAYED_START",
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"values": {
|
||||
"START": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": "END_OF_CYCLE",
|
||||
"operator": "eq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "string",
|
||||
"values": {
|
||||
"ALARM": {},
|
||||
"DELAYED_START": {},
|
||||
"END_OF_CYCLE": {},
|
||||
"IDLE": {},
|
||||
"OFF": {},
|
||||
"PAUSED": {},
|
||||
"READY_TO_START": {},
|
||||
"RUNNING": {}
|
||||
}
|
||||
},
|
||||
"cavityLight": {
|
||||
"access": "readwrite",
|
||||
"type": "boolean",
|
||||
"values": {
|
||||
"OFF": {},
|
||||
"ON": {}
|
||||
}
|
||||
},
|
||||
"displayTemperatureC": {
|
||||
"access": "read",
|
||||
"type": "temperature"
|
||||
},
|
||||
"doorState": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"CLOSED": {},
|
||||
"OPEN": {}
|
||||
}
|
||||
},
|
||||
"executeCommand": {
|
||||
"access": "write",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"START": {},
|
||||
"STOPRESET": {}
|
||||
}
|
||||
},
|
||||
"hideExecuteCommand": {
|
||||
"access": "constant",
|
||||
"default": 0,
|
||||
"triggers": [
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": 0,
|
||||
"operator": "eq"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": {
|
||||
"executeCommand": {
|
||||
"disabled": true
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"operand_1": "value",
|
||||
"operand_2": 1,
|
||||
"operator": "eq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"type": "int"
|
||||
},
|
||||
"keyModel": {
|
||||
"access": "constant",
|
||||
"default": "PUX_SP_PYR_PE_A++",
|
||||
"type": "string"
|
||||
},
|
||||
"networkInterface": {
|
||||
"command": {
|
||||
"access": "write",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"ABORT": {},
|
||||
"APPLIANCE_AUTHORIZE": {},
|
||||
"DOWNLOAD": {},
|
||||
"START": {},
|
||||
"USER_AUTHORIZE": {},
|
||||
"USER_NOT_AUTHORIZE": {}
|
||||
}
|
||||
},
|
||||
"linkQualityIndicator": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"EXCELLENT": {},
|
||||
"GOOD": {},
|
||||
"POOR": {},
|
||||
"UNDEFINED": {},
|
||||
"VERY_GOOD": {},
|
||||
"VERY_POOR": {}
|
||||
}
|
||||
},
|
||||
"niuSwUpdateCurrentDescription": {
|
||||
"access": "read",
|
||||
"type": "string"
|
||||
},
|
||||
"otaState": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"DESCRIPTION_AVAILABLE": {},
|
||||
"DESCRIPTION_DOWNLOADING": {},
|
||||
"DESCRIPTION_READY": {},
|
||||
"FW_DOWNLOADING": {},
|
||||
"FW_DOWNLOAD_START": {},
|
||||
"FW_SIGNATURE_CHECK": {},
|
||||
"FW_UPDATE_IN_PROGRESS": {},
|
||||
"IDLE": {},
|
||||
"READY_TO_UPDATE": {},
|
||||
"UPDATE_ABORT": {},
|
||||
"UPDATE_CHECK": {},
|
||||
"UPDATE_ERROR": {},
|
||||
"UPDATE_OK": {},
|
||||
"WAITINGFORAUTHORIZATION": {}
|
||||
}
|
||||
},
|
||||
"startUpCommand": {
|
||||
"access": "write",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"UNINSTALL": {}
|
||||
}
|
||||
},
|
||||
"swAncAndRevision": {
|
||||
"access": "read",
|
||||
"type": "string"
|
||||
},
|
||||
"swVersion": {
|
||||
"access": "read",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"pizzaShieldState": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"INSERTED_COLD": {},
|
||||
"INSERTED_HOT": {},
|
||||
"NOT_INSERTED": {},
|
||||
"UNKNOWN": {}
|
||||
}
|
||||
},
|
||||
"preheatComplete": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"OFF": {},
|
||||
"PRE_HEAT_COMPLETED": {},
|
||||
"PRE_HEAT_RUNNING": {},
|
||||
"RE_HEAT_COMPLETED": {},
|
||||
"RE_HEAT_RUNNING": {}
|
||||
}
|
||||
},
|
||||
"program": {
|
||||
"access": "readwrite",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"ASSIST_PIZZA_EXPERT_REHEAT": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 180,
|
||||
"min": 150,
|
||||
"step": 1
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 340,
|
||||
"disabled": false,
|
||||
"max": 340,
|
||||
"min": 320,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"AUGRATIN": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 300,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"BAKE_TRUE_FAN": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 200,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"BOTTOM": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 150,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"BREAD_BAKING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 220,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"CONVENTIONAL_COOKING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 200,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 30,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"DEFROST": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 30,
|
||||
"disabled": false,
|
||||
"max": 30,
|
||||
"min": 30,
|
||||
"step": 0,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"DOUGH_PROVING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 300,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 40,
|
||||
"disabled": false,
|
||||
"max": 40,
|
||||
"min": 40,
|
||||
"step": 0,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"DRYING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 60,
|
||||
"disabled": false,
|
||||
"max": 100,
|
||||
"min": 50,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"FROZEN_FOOD": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 220,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"GRILL": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 300,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"GRILL_FAN": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 180,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"KEEP_WARM": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 80,
|
||||
"disabled": false,
|
||||
"max": 80,
|
||||
"min": 80,
|
||||
"step": 0,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"MOIST_FAN_BAKING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 160,
|
||||
"disabled": false,
|
||||
"max": 230,
|
||||
"min": 90,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PIZZA_EXPERT_EXTENSION": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 15,
|
||||
"min": 15,
|
||||
"step": 0
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 340,
|
||||
"disabled": false,
|
||||
"max": 340,
|
||||
"min": 320,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PIZZA_EXPERT_MAIN": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 180,
|
||||
"min": 150,
|
||||
"step": 1
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 340,
|
||||
"disabled": false,
|
||||
"max": 340,
|
||||
"min": 320,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PIZZA_EXPERT_PREHEAT": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 60,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 340,
|
||||
"disabled": false,
|
||||
"max": 340,
|
||||
"min": 320,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PLATE_WARMING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 70,
|
||||
"disabled": false,
|
||||
"max": 70,
|
||||
"min": 70,
|
||||
"step": 0,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PRESERVING": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 160,
|
||||
"disabled": false,
|
||||
"max": 170,
|
||||
"min": 100,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"PYRO_CLEAN_INTENSE": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 1,
|
||||
"disabled": true
|
||||
}
|
||||
},
|
||||
"PYRO_CLEAN_LIGHT": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 1,
|
||||
"disabled": true
|
||||
}
|
||||
},
|
||||
"PYRO_CLEAN_NORMAL": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 1,
|
||||
"disabled": true
|
||||
}
|
||||
},
|
||||
"SLOW_COOK": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 90,
|
||||
"disabled": false,
|
||||
"max": 150,
|
||||
"min": 80,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
},
|
||||
"TRUE_FAN": {
|
||||
"hideExecuteCommand": {
|
||||
"access": "readwrite",
|
||||
"default": 0,
|
||||
"disabled": true
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"disabled": false,
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"default": 150,
|
||||
"disabled": false,
|
||||
"max": 300,
|
||||
"min": 30,
|
||||
"step": 5,
|
||||
"type": "temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remoteControl": {
|
||||
"access": "read",
|
||||
"type": "string",
|
||||
"values": {
|
||||
"DISABLED": {},
|
||||
"ENABLED": {},
|
||||
"NOT_SAFETY_RELEVANT_ENABLED": {},
|
||||
"TEMPORARY_LOCKED": {}
|
||||
}
|
||||
},
|
||||
"runningTime": {
|
||||
"access": "read",
|
||||
"default": 0,
|
||||
"type": "number"
|
||||
},
|
||||
"targetDuration": {
|
||||
"access": "readwrite",
|
||||
"max": 86340,
|
||||
"min": 0,
|
||||
"step": 60,
|
||||
"type": "number"
|
||||
},
|
||||
"targetTemperatureC": {
|
||||
"access": "readwrite",
|
||||
"type": "temperature"
|
||||
},
|
||||
"timeToEnd": {
|
||||
"access": "read",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"applianceId": "900412569_00:43319382-443E0748CCD4",
|
||||
"connectionState": "connected",
|
||||
"status": "enabled",
|
||||
"properties": {
|
||||
"reported": {
|
||||
"cleaningReminder": false,
|
||||
"doorState": "CLOSED",
|
||||
"remoteControl": "ENABLED",
|
||||
"targetTemperatureF": 356,
|
||||
"targetTemperatureC": 180,
|
||||
"program": "KEY_ERROR",
|
||||
"targetMicrowavePower": 65535,
|
||||
"waterTrayInsertionState": "INSERTED",
|
||||
"waterTankEmpty": "STEAM_TANK_FULL",
|
||||
"targetDuration": 0,
|
||||
"startTime": -1,
|
||||
"applianceInfo": {
|
||||
"capabilityHash": "ab7b74c6ac8a74614980f77811389dfd47206456617a4d698ffcb97da1cc955b",
|
||||
"applianceType": "OV"
|
||||
},
|
||||
"preheatComplete": "OFF",
|
||||
"cpv": "00",
|
||||
"targetFoodProbeTemperatureC": 60,
|
||||
"targetFoodProbeTemperatureF": 140,
|
||||
"runningTime": 0,
|
||||
"applianceState": "READY_TO_START",
|
||||
"alerts": [],
|
||||
"networkInterface": {
|
||||
"swVersion": "v3.0.0S_argo",
|
||||
"otaState": "IDLE",
|
||||
"linkQualityIndicator": "VERY_GOOD",
|
||||
"niuSwUpdateCurrentDescription": "A23642201A-S00007645A",
|
||||
"swAncAndRevision": "S00007645A"
|
||||
},
|
||||
"foodProbeInsertionState": "INSERTED",
|
||||
"cavityLight": false,
|
||||
"processPhase": "NONE",
|
||||
"descalingReminderState": false,
|
||||
"connectivityState": "connected",
|
||||
"timeToEnd": -1,
|
||||
"displayTemperatureC": 30,
|
||||
"displayFoodProbeTemperatureC": 23,
|
||||
"displayTemperatureF": 86,
|
||||
"displayFoodProbeTemperatureF": 73.4
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"applianceId": "949288049_00:11112225-443E076A37D6",
|
||||
"connectionState": "connected",
|
||||
"status": "enabled",
|
||||
"properties": {
|
||||
"reported": {
|
||||
"applianceInfo": {
|
||||
"capabilityHash": "212e55b62f19cfec40ff724301b07f80adef9a0ccf072693d45e38049708ef03",
|
||||
"applianceType": "OV"
|
||||
},
|
||||
"doorState": "CLOSED",
|
||||
"preheatComplete": "OFF",
|
||||
"remoteControl": "NOT_SAFETY_RELEVANT_ENABLED",
|
||||
"cpv": "00",
|
||||
"targetTemperatureF": 302,
|
||||
"targetTemperatureC": 150,
|
||||
"runningTime": 0,
|
||||
"program": "TRUE_FAN",
|
||||
"applianceState": "READY_TO_START",
|
||||
"targetMicrowavePower": 65535,
|
||||
"alerts": [],
|
||||
"pizzaShieldState": "UNKNOWN",
|
||||
"networkInterface": {
|
||||
"autoLocalTimeOffset": -2147483648,
|
||||
"niuSwUpdateCurrentDescription": "A23642205A-S00008458A",
|
||||
"swVersion": "v4.0.0S_argo",
|
||||
"otaState": "IDLE",
|
||||
"timeZoneDatabaseName": "Etc/UTC",
|
||||
"swAncAndRevision": "S00008458A",
|
||||
"linkQualityIndicator": "VERY_GOOD"
|
||||
},
|
||||
"foodProbeInsertionState": "NOT_INSERTED",
|
||||
"cavityLight": false,
|
||||
"waterTrayInsertionState": "INSERTED",
|
||||
"waterTankEmpty": "STEAM_TANK_FULL",
|
||||
"targetDuration": 0,
|
||||
"startTime": -1,
|
||||
"processPhase": "NONE",
|
||||
"connectivityState": "connected",
|
||||
"timeToEnd": -1,
|
||||
"displayTemperatureC": 30,
|
||||
"displayTemperatureF": 86
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"applianceId": "900412569_00:43319382-443E0748CCD4",
|
||||
"applianceName": "Fenix",
|
||||
"applianceType": "OV",
|
||||
"created": "2024-10-30T14:00:00.000+00:00"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"applianceId": "949288049_00:11112225-443E076A37D6",
|
||||
"applianceName": "PUX pizza oven",
|
||||
"applianceType": "OV",
|
||||
"created": "2026-01-15T14:59:00.000+00:00"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_appliances[fenix_oven]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'electrolux',
|
||||
'900412569_00:43319382-443E0748CCD4',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'ELECTROLUX',
|
||||
'model': 'OE9XS',
|
||||
'model_id': None,
|
||||
'name': 'Fenix',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '43319382',
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_appliances[pux_oven]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'electrolux',
|
||||
'949288049_00:11112225-443E076A37D6',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'AEG',
|
||||
'model': 'NBX7P631SB',
|
||||
'model_id': None,
|
||||
'name': 'PUX pizza oven',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': '11112225',
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,515 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor[sensor.fenix_appliance_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'alarm',
|
||||
'delayed_start',
|
||||
'end_of_cycle',
|
||||
'idle',
|
||||
'off',
|
||||
'paused',
|
||||
'ready_to_start',
|
||||
'running',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.fenix_appliance_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Appliance state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': 'mdi:information-outline',
|
||||
'original_name': 'Appliance state',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'appliance_state',
|
||||
'unique_id': '900412569_00:43319382-443E0748CCD4_appliance_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_appliance_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Fenix Appliance state',
|
||||
'icon': 'mdi:information-outline',
|
||||
'options': list([
|
||||
'alarm',
|
||||
'delayed_start',
|
||||
'end_of_cycle',
|
||||
'idle',
|
||||
'off',
|
||||
'paused',
|
||||
'ready_to_start',
|
||||
'running',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.fenix_appliance_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ready_to_start',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_current_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.fenix_current_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Current temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': 'mdi:thermometer',
|
||||
'original_name': 'Current temperature',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'display_temperature',
|
||||
'unique_id': '900412569_00:43319382-443E0748CCD4_display_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_current_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Fenix Current temperature',
|
||||
'icon': 'mdi:thermometer',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.fenix_current_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_food_probe_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'inserted',
|
||||
'not_inserted',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.fenix_food_probe_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Food probe state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': 'mdi:thermometer-probe',
|
||||
'original_name': 'Food probe state',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'food_probe_state',
|
||||
'unique_id': '900412569_00:43319382-443E0748CCD4_food_probe_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_food_probe_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Fenix Food probe state',
|
||||
'icon': 'mdi:thermometer-probe',
|
||||
'options': list([
|
||||
'inserted',
|
||||
'not_inserted',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.fenix_food_probe_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'inserted',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_food_probe_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.fenix_food_probe_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Food probe temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': 'mdi:thermometer-probe',
|
||||
'original_name': 'Food probe temperature',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'food_probe_temperature',
|
||||
'unique_id': '900412569_00:43319382-443E0748CCD4_food_probe_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_food_probe_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Fenix Food probe temperature',
|
||||
'icon': 'mdi:thermometer-probe',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.fenix_food_probe_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '23',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_remote_control-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'disabled',
|
||||
'enabled',
|
||||
'not_safety_relevant_enabled',
|
||||
'temporary_locked',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.fenix_remote_control',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Remote control',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': 'mdi:remote',
|
||||
'original_name': 'Remote control',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'remote_control',
|
||||
'unique_id': '900412569_00:43319382-443E0748CCD4_remote_control',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.fenix_remote_control-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Fenix Remote control',
|
||||
'icon': 'mdi:remote',
|
||||
'options': list([
|
||||
'disabled',
|
||||
'enabled',
|
||||
'not_safety_relevant_enabled',
|
||||
'temporary_locked',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.fenix_remote_control',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'enabled',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_appliance_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'alarm',
|
||||
'delayed_start',
|
||||
'end_of_cycle',
|
||||
'idle',
|
||||
'off',
|
||||
'paused',
|
||||
'ready_to_start',
|
||||
'running',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pux_pizza_oven_appliance_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Appliance state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': 'mdi:information-outline',
|
||||
'original_name': 'Appliance state',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'appliance_state',
|
||||
'unique_id': '949288049_00:11112225-443E076A37D6_appliance_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_appliance_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'PUX pizza oven Appliance state',
|
||||
'icon': 'mdi:information-outline',
|
||||
'options': list([
|
||||
'alarm',
|
||||
'delayed_start',
|
||||
'end_of_cycle',
|
||||
'idle',
|
||||
'off',
|
||||
'paused',
|
||||
'ready_to_start',
|
||||
'running',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pux_pizza_oven_appliance_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'ready_to_start',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_current_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pux_pizza_oven_current_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Current temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': 'mdi:thermometer',
|
||||
'original_name': 'Current temperature',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'display_temperature',
|
||||
'unique_id': '949288049_00:11112225-443E076A37D6_display_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_current_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'PUX pizza oven Current temperature',
|
||||
'icon': 'mdi:thermometer',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pux_pizza_oven_current_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '30',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_remote_control-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'disabled',
|
||||
'enabled',
|
||||
'not_safety_relevant_enabled',
|
||||
'temporary_locked',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pux_pizza_oven_remote_control',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Remote control',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': 'mdi:remote',
|
||||
'original_name': 'Remote control',
|
||||
'platform': 'electrolux',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'remote_control',
|
||||
'unique_id': '949288049_00:11112225-443E076A37D6_remote_control',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor[sensor.pux_pizza_oven_remote_control-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'PUX pizza oven Remote control',
|
||||
'icon': 'mdi:remote',
|
||||
'options': list([
|
||||
'disabled',
|
||||
'enabled',
|
||||
'not_safety_relevant_enabled',
|
||||
'temporary_locked',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pux_pizza_oven_remote_control',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'not_safety_relevant_enabled',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Unit test for Electrolux config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from electrolux_group_developer_sdk.auth.invalid_credentials_exception import (
|
||||
InvalidCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.electrolux.const import CONF_REFRESH_TOKEN, DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
valid_user_input = {
|
||||
CONF_API_KEY: "test_api_key",
|
||||
CONF_ACCESS_TOKEN: "test_access_token",
|
||||
CONF_REFRESH_TOKEN: "test_refresh_token",
|
||||
}
|
||||
|
||||
invalid_user_input = {
|
||||
CONF_API_KEY: "api_key",
|
||||
CONF_ACCESS_TOKEN: "invalid_token",
|
||||
CONF_REFRESH_TOKEN: "invalid_token",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow_success(
|
||||
hass: HomeAssistant, appliances: AsyncMock, mock_token_manager: AsyncMock
|
||||
) -> None:
|
||||
"""Test a successful user config flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=valid_user_input
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Electrolux for mock@email.com"
|
||||
assert result["data"] == valid_user_input
|
||||
assert result["result"].unique_id == "mock_user_id"
|
||||
|
||||
|
||||
async def test_user_flow_invalid_auth(
|
||||
hass: HomeAssistant, appliances: AsyncMock, mock_token_manager: AsyncMock
|
||||
) -> None:
|
||||
"""Test an invalid auth config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
mock_token_manager.ensure_credentials.side_effect = InvalidCredentialsException
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=invalid_user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
mock_token_manager.ensure_credentials.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=valid_user_input
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Electrolux for mock@email.com"
|
||||
assert result["data"] == valid_user_input
|
||||
assert result["result"].unique_id == "mock_user_id"
|
||||
|
||||
|
||||
async def test_user_flow_bad_credentials(
|
||||
hass: HomeAssistant, appliances: AsyncMock, mock_token_manager: AsyncMock
|
||||
) -> None:
|
||||
"""Test an invalid auth config flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
appliances.test_connection.side_effect = BadCredentialsException()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=invalid_user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
appliances.test_connection.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=valid_user_input
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Electrolux for mock@email.com"
|
||||
assert result["data"] == valid_user_input
|
||||
assert result["result"].unique_id == "mock_user_id"
|
||||
|
||||
|
||||
async def test_user_flow_failed_connection(
|
||||
hass: HomeAssistant, appliances: AsyncMock, mock_token_manager: AsyncMock
|
||||
) -> None:
|
||||
"""Test an invalid auth config flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
appliances.test_connection.side_effect = FailedConnectionException()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=invalid_user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
appliances.test_connection.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=valid_user_input
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Electrolux for mock@email.com"
|
||||
assert result["data"] == valid_user_input
|
||||
assert result["result"].unique_id == "mock_user_id"
|
||||
|
||||
|
||||
async def test_user_flow_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
appliances: AsyncMock,
|
||||
mock_token_manager: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test an invalid auth config flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=valid_user_input,
|
||||
)
|
||||
|
||||
assert result["type"] is data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Unit test for Electrolux init flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliance_data_factory import (
|
||||
appliance_data_factory,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.client_exception import (
|
||||
ApplianceClientException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.electrolux import ElectroluxData
|
||||
from homeassistant.components.electrolux.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import (
|
||||
APPLIANCE_FIXTURES,
|
||||
get_fixture_name,
|
||||
load_appliance,
|
||||
load_appliance_details,
|
||||
load_appliance_state,
|
||||
setup_integration,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_async_setup_entry_success(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
appliances: AsyncMock,
|
||||
mock_token_manager: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful setup of the Electrolux integration."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Check integration is loaded
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert mock_config_entry.runtime_data is not None
|
||||
assert isinstance(mock_config_entry.runtime_data, ElectroluxData)
|
||||
|
||||
# Unload the config entry
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
def test_appliance_fixture_data() -> None:
|
||||
"""Test that all appliance fixtures are configured correctly."""
|
||||
appliance_id_set = set()
|
||||
for appliance_fixture in APPLIANCE_FIXTURES:
|
||||
appliance = load_appliance(appliance_fixture)
|
||||
appliance_id = appliance.applianceId
|
||||
assert appliance_id not in appliance_id_set, (
|
||||
f"Duplicate appliance ID {appliance_id} detected in fixture {appliance_fixture}"
|
||||
)
|
||||
|
||||
appliance_state = load_appliance_state(appliance_fixture)
|
||||
assert appliance_id == appliance_state.applianceId, (
|
||||
f"Appliance ID in state {appliance_state.applianceId} does not match appliance ID in appliance object {appliance_id}"
|
||||
)
|
||||
|
||||
appliance_id_set.add(appliance_id)
|
||||
|
||||
|
||||
async def test_all_appliances(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
appliances: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test all entities for all appliance fixtures."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
appliance_list: list[ApplianceData] = await appliances.get_appliance_data()
|
||||
for appliance in appliance_list:
|
||||
appliance_id = appliance.appliance.applianceId
|
||||
|
||||
device = device_registry.async_get_device({(DOMAIN, appliance_id)})
|
||||
|
||||
assert device is not None
|
||||
assert device == snapshot(name=get_fixture_name(appliance_id))
|
||||
|
||||
|
||||
async def test_check_for_dynamic_devices(
|
||||
hass: HomeAssistant,
|
||||
mock_appliance_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test that the integration adds and removes devices correctly."""
|
||||
old_appliance_fixture = "fenix_oven"
|
||||
old_appliance_id = "900412569_00:43319382-443E0748CCD4"
|
||||
|
||||
new_appliance_fixture = "pux_oven"
|
||||
new_appliance_id = "949288049_00:11112225-443E076A37D6"
|
||||
|
||||
def set_appliance_fixture_mock(appliance_fixture: str):
|
||||
|
||||
appliance = load_appliance(appliance_fixture)
|
||||
details = load_appliance_details(appliance_fixture)
|
||||
state = load_appliance_state(appliance_fixture)
|
||||
|
||||
appliance_data = appliance_data_factory(
|
||||
appliance=appliance,
|
||||
details=details,
|
||||
state=state,
|
||||
)
|
||||
|
||||
mock_appliance_client.get_appliance_data.return_value = [appliance_data]
|
||||
|
||||
set_appliance_fixture_mock(old_appliance_fixture)
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert device_registry.async_get_device({(DOMAIN, old_appliance_id)}) is not None
|
||||
assert device_registry.async_get_device({(DOMAIN, new_appliance_id)}) is None
|
||||
|
||||
set_appliance_fixture_mock(new_appliance_fixture)
|
||||
|
||||
event_stream_call_args = mock_appliance_client.start_event_stream.call_args.args
|
||||
assert event_stream_call_args is not None and len(event_stream_call_args) > 0, (
|
||||
"start_event_stream method called without any callbacks specified"
|
||||
)
|
||||
|
||||
callback_list = event_stream_call_args[0]
|
||||
for callback in callback_list:
|
||||
await callback()
|
||||
|
||||
assert device_registry.async_get_device({(DOMAIN, old_appliance_id)}) is None
|
||||
assert device_registry.async_get_device({(DOMAIN, new_appliance_id)}) is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "entry_state"),
|
||||
[
|
||||
(ApplianceClientException(), ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_appliance_client_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_appliance_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
entry_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test for handling errors that occur while getting the data of all appliances."""
|
||||
mock_appliance_client.get_appliance_data.side_effect = exception
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is entry_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "entry_state"),
|
||||
[
|
||||
(BadCredentialsException(), ConfigEntryState.SETUP_ERROR),
|
||||
(FailedConnectionException(), ConfigEntryState.SETUP_RETRY),
|
||||
],
|
||||
)
|
||||
async def test_appliance_client_test_connection_bad_credentials(
|
||||
hass: HomeAssistant,
|
||||
mock_appliance_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
entry_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test for handling errors that occur while testing the connection."""
|
||||
mock_appliance_client.test_connection.side_effect = exception
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is entry_state
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Sensor tests of Electrolux integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def override_platforms() -> Generator[None]:
|
||||
"""Override PLATFORMS."""
|
||||
with patch("homeassistant.components.electrolux.PLATFORMS", [Platform.SENSOR]):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("appliances")
|
||||
async def test_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test states of the sensor."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
Reference in New Issue
Block a user