mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Add vacuum platform to miele (#143757)
* Add vacuum platform * Add comments * Update snapshot * Use class defined in pymiele * Use device class transation * Fix typo * Cleanup consts * Clean up activity property * Address review comments * Address review comments
This commit is contained in:
parent
7bad07ac10
commit
a93bf3c150
@ -26,6 +26,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.LIGHT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.VACUUM,
|
||||
]
|
||||
|
||||
|
||||
|
@ -11,6 +11,7 @@ ACTIONS = "actions"
|
||||
POWER_ON = "powerOn"
|
||||
POWER_OFF = "powerOff"
|
||||
PROCESS_ACTION = "processAction"
|
||||
PROGRAM_ID = "programId"
|
||||
VENTILATION_STEP = "ventilationStep"
|
||||
TARGET_TEMPERATURE = "targetTemperature"
|
||||
AMBIENT_LIGHT = "ambientLight"
|
||||
|
224
homeassistant/components/miele/vacuum.py
Normal file
224
homeassistant/components/miele/vacuum.py
Normal file
@ -0,0 +1,224 @@
|
||||
"""Platform for Miele vacuum integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pymiele import MieleEnum
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
StateVacuumEntity,
|
||||
StateVacuumEntityDescription,
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, PROCESS_ACTION, PROGRAM_ID, MieleActions, MieleAppliance
|
||||
from .coordinator import MieleConfigEntry
|
||||
from .entity import MieleEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# The following const classes define program speeds and programs for the vacuum cleaner.
|
||||
# Miele have used the same and overlapping names for fan_speeds and programs even
|
||||
# if the contexts are different. This is an attempt to make it clearer in the integration.
|
||||
|
||||
|
||||
class FanSpeed(IntEnum):
|
||||
"""Define fan speeds."""
|
||||
|
||||
normal = 0
|
||||
turbo = 1
|
||||
silent = 2
|
||||
|
||||
|
||||
class FanProgram(IntEnum):
|
||||
"""Define fan programs."""
|
||||
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
PROGRAM_MAP = {
|
||||
"normal": FanProgram.auto,
|
||||
"turbo": FanProgram.turbo,
|
||||
"silent": FanProgram.silent,
|
||||
}
|
||||
|
||||
PROGRAM_TO_SPEED: dict[int, str] = {
|
||||
FanProgram.auto: "normal",
|
||||
FanProgram.turbo: "turbo",
|
||||
FanProgram.silent: "silent",
|
||||
FanProgram.spot: "normal",
|
||||
}
|
||||
|
||||
|
||||
class MieleVacuumStateCode(MieleEnum):
|
||||
"""Define vacuum state codes."""
|
||||
|
||||
idle = 0
|
||||
cleaning = 5889
|
||||
returning = 5890
|
||||
paused = 5891
|
||||
going_to_target_area = 5892
|
||||
wheel_lifted = 5893
|
||||
dirty_sensors = 5894
|
||||
dust_box_missing = 5895
|
||||
blocked_drive_wheels = 5896
|
||||
blocked_brushes = 5897
|
||||
check_dust_box_and_filter = 5898
|
||||
internal_fault_reboot = 5899
|
||||
blocked_front_wheel = 5900
|
||||
docked = 5903, 5904
|
||||
remote_controlled = 5910
|
||||
unknown = -9999
|
||||
|
||||
|
||||
SUPPORTED_FEATURES = (
|
||||
VacuumEntityFeature.STATE
|
||||
| VacuumEntityFeature.BATTERY
|
||||
| VacuumEntityFeature.FAN_SPEED
|
||||
| VacuumEntityFeature.START
|
||||
| VacuumEntityFeature.STOP
|
||||
| VacuumEntityFeature.PAUSE
|
||||
| VacuumEntityFeature.CLEAN_SPOT
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MieleVacuumDescription(StateVacuumEntityDescription):
|
||||
"""Class describing Miele vacuum entities."""
|
||||
|
||||
on_value: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class MieleVacuumDefinition:
|
||||
"""Class for defining vacuum entities."""
|
||||
|
||||
types: tuple[MieleAppliance, ...]
|
||||
description: MieleVacuumDescription
|
||||
|
||||
|
||||
VACUUM_TYPES: Final[tuple[MieleVacuumDefinition, ...]] = (
|
||||
MieleVacuumDefinition(
|
||||
types=(MieleAppliance.ROBOT_VACUUM_CLEANER,),
|
||||
description=MieleVacuumDescription(
|
||||
key="vacuum",
|
||||
on_value=14,
|
||||
name=None,
|
||||
translation_key="vacuum",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MieleConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the vacuum platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
MieleVacuum(coordinator, device_id, definition.description)
|
||||
for device_id, device in coordinator.data.devices.items()
|
||||
for definition in VACUUM_TYPES
|
||||
if device.device_type in definition.types
|
||||
)
|
||||
|
||||
|
||||
VACUUM_PHASE_TO_ACTIVITY = {
|
||||
MieleVacuumStateCode.idle: VacuumActivity.IDLE,
|
||||
MieleVacuumStateCode.docked: VacuumActivity.DOCKED,
|
||||
MieleVacuumStateCode.cleaning: VacuumActivity.CLEANING,
|
||||
MieleVacuumStateCode.going_to_target_area: VacuumActivity.CLEANING,
|
||||
MieleVacuumStateCode.returning: VacuumActivity.RETURNING,
|
||||
MieleVacuumStateCode.wheel_lifted: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.dirty_sensors: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.dust_box_missing: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.blocked_drive_wheels: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.blocked_brushes: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.check_dust_box_and_filter: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.internal_fault_reboot: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.blocked_front_wheel: VacuumActivity.ERROR,
|
||||
MieleVacuumStateCode.paused: VacuumActivity.PAUSED,
|
||||
MieleVacuumStateCode.remote_controlled: VacuumActivity.PAUSED,
|
||||
}
|
||||
|
||||
|
||||
class MieleVacuum(MieleEntity, StateVacuumEntity):
|
||||
"""Representation of a Vacuum entity."""
|
||||
|
||||
entity_description: MieleVacuumDescription
|
||||
_attr_supported_features = SUPPORTED_FEATURES
|
||||
_attr_fan_speed_list = [fan_speed.name for fan_speed in FanSpeed]
|
||||
_attr_name = None
|
||||
|
||||
@property
|
||||
def activity(self) -> VacuumActivity | None:
|
||||
"""Return activity."""
|
||||
return VACUUM_PHASE_TO_ACTIVITY.get(
|
||||
MieleVacuumStateCode(self.device.state_program_phase)
|
||||
)
|
||||
|
||||
@property
|
||||
def battery_level(self) -> int | None:
|
||||
"""Return the battery level."""
|
||||
return self.device.state_battery_level
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str | None:
|
||||
"""Return the fan speed."""
|
||||
return PROGRAM_TO_SPEED.get(self.device.state_program_id)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the entity."""
|
||||
|
||||
return (
|
||||
self.action.power_off_enabled or self.action.power_on_enabled
|
||||
) and super().available
|
||||
|
||||
async def send(self, device_id: str, action: dict[str, Any]) -> None:
|
||||
"""Send action to the device."""
|
||||
try:
|
||||
await self.api.send_action(device_id, action)
|
||||
except ClientResponseError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_state_error",
|
||||
translation_placeholders={
|
||||
"entity": self.entity_id,
|
||||
},
|
||||
) from ex
|
||||
|
||||
async def async_clean_spot(self, **kwargs: Any) -> None:
|
||||
"""Clean spot."""
|
||||
await self.send(self._device_id, {PROGRAM_ID: FanProgram.spot})
|
||||
|
||||
async def async_start(self, **kwargs: Any) -> None:
|
||||
"""Start cleaning."""
|
||||
await self.send(self._device_id, {PROCESS_ACTION: MieleActions.START})
|
||||
|
||||
async def async_stop(self, **kwargs: Any) -> None:
|
||||
"""Stop cleaning."""
|
||||
await self.send(self._device_id, {PROCESS_ACTION: MieleActions.STOP})
|
||||
|
||||
async def async_pause(self, **kwargs: Any) -> None:
|
||||
"""Pause cleaning."""
|
||||
await self.send(self._device_id, {PROCESS_ACTION: MieleActions.PAUSE})
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
"""Set fan speed."""
|
||||
await self.send(self._device_id, {PROGRAM_ID: PROGRAM_MAP[fan_speed]})
|
81
tests/components/miele/fixtures/vacuum_device.json
Normal file
81
tests/components/miele/fixtures/vacuum_device.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"Dummy_Vacuum_1": {
|
||||
"ident": {
|
||||
"type": {
|
||||
"key_localized": "Device type",
|
||||
"value_raw": 23,
|
||||
"value_localized": "Robot vacuum cleaner"
|
||||
},
|
||||
"deviceName": "",
|
||||
"protocolVersion": 0,
|
||||
"deviceIdentLabel": {
|
||||
"fabNumber": "161173909",
|
||||
"fabIndex": "32",
|
||||
"techType": "RX3",
|
||||
"matNumber": "11686510",
|
||||
"swids": ["<swid1>", "<swid2>", "<swid3>", "<...>"]
|
||||
},
|
||||
"xkmIdentLabel": { "techType": "", "releaseVersion": "" }
|
||||
},
|
||||
"state": {
|
||||
"ProgramID": {
|
||||
"value_raw": 1,
|
||||
"value_localized": "Auto",
|
||||
"key_localized": "Program name"
|
||||
},
|
||||
"status": {
|
||||
"value_raw": 2,
|
||||
"value_localized": "On",
|
||||
"key_localized": "status"
|
||||
},
|
||||
"programType": {
|
||||
"value_raw": 0,
|
||||
"value_localized": "Program",
|
||||
"key_localized": "Program type"
|
||||
},
|
||||
"programPhase": {
|
||||
"xvalue_raw": 5889,
|
||||
"zvalue_raw": 5904,
|
||||
"value_raw": 5893,
|
||||
"value_localized": "in the base station",
|
||||
"key_localized": "Program phase"
|
||||
},
|
||||
"remainingTime": [0, 0],
|
||||
"startTime": [0, 0],
|
||||
"targetTemperature": [],
|
||||
"temperature": [],
|
||||
"coreTargetTemperature": [],
|
||||
"coreTemperature": [],
|
||||
"signalInfo": false,
|
||||
"signalFailure": false,
|
||||
"signalDoor": false,
|
||||
"remoteEnable": {
|
||||
"fullRemoteControl": true,
|
||||
"smartGrid": false,
|
||||
"mobileStart": false
|
||||
},
|
||||
"ambientLight": null,
|
||||
"light": null,
|
||||
"elapsedTime": [0, 0],
|
||||
"spinningSpeed": {
|
||||
"unit": "rpm",
|
||||
"value_raw": null,
|
||||
"value_localized": null,
|
||||
"key_localized": "Spin speed"
|
||||
},
|
||||
"dryingStep": {
|
||||
"value_raw": 0,
|
||||
"value_localized": "",
|
||||
"key_localized": "Drying level"
|
||||
},
|
||||
"ventilationStep": {
|
||||
"value_raw": null,
|
||||
"value_localized": "",
|
||||
"key_localized": "Fan level"
|
||||
},
|
||||
"plateStep": [],
|
||||
"ecoFeedback": null,
|
||||
"batteryLevel": 65
|
||||
}
|
||||
}
|
||||
}
|
63
tests/components/miele/snapshots/test_vacuum.ambr
Normal file
63
tests/components/miele/snapshots/test_vacuum.ambr
Normal file
@ -0,0 +1,63 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_states[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'fan_speed_list': list([
|
||||
'normal',
|
||||
'turbo',
|
||||
'silent',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'vacuum',
|
||||
'entity_category': None,
|
||||
'entity_id': 'vacuum.robot_vacuum_cleaner',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'miele',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <VacuumEntityFeature: 13420>,
|
||||
'translation_key': 'vacuum',
|
||||
'unique_id': 'Dummy_Vacuum_1-vacuum',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_states[platforms0-vacuum_device.json][vacuum.robot_vacuum_cleaner-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'battery_icon': 'mdi:battery-60',
|
||||
'battery_level': 65,
|
||||
'fan_speed': 'normal',
|
||||
'fan_speed_list': list([
|
||||
'normal',
|
||||
'turbo',
|
||||
'silent',
|
||||
]),
|
||||
'friendly_name': 'robot_vacuum_cleaner',
|
||||
'supported_features': <VacuumEntityFeature: 13420>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'vacuum.robot_vacuum_cleaner',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'paused',
|
||||
})
|
||||
# ---
|
119
tests/components/miele/test_vacuum.py
Normal file
119
tests/components/miele/test_vacuum.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Tests for miele vacuum module."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.miele.const import PROCESS_ACTION, PROGRAM_ID
|
||||
from homeassistant.components.vacuum import (
|
||||
ATTR_FAN_SPEED,
|
||||
DOMAIN as VACUUM_DOMAIN,
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
TEST_PLATFORM = VACUUM_DOMAIN
|
||||
ENTITY_ID = "vacuum.robot_vacuum_cleaner"
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.parametrize("platforms", [(TEST_PLATFORM,)]),
|
||||
pytest.mark.parametrize("load_device_file", ["vacuum_device.json"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_states(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
setup_platform: None,
|
||||
) -> None:
|
||||
"""Test vacuum entity setup."""
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "action_command", "vacuum_power"),
|
||||
[
|
||||
(SERVICE_START, PROCESS_ACTION, 1),
|
||||
(SERVICE_STOP, PROCESS_ACTION, 2),
|
||||
(SERVICE_PAUSE, PROCESS_ACTION, 3),
|
||||
(SERVICE_CLEAN_SPOT, PROGRAM_ID, 2),
|
||||
],
|
||||
)
|
||||
async def test_vacuum_program(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
setup_platform: None,
|
||||
service: str,
|
||||
vacuum_power: int | str,
|
||||
action_command: str,
|
||||
) -> None:
|
||||
"""Test the vacuum can be controlled."""
|
||||
|
||||
await hass.services.async_call(
|
||||
TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True
|
||||
)
|
||||
mock_miele_client.send_action.assert_called_once_with(
|
||||
"Dummy_Vacuum_1", {action_command: vacuum_power}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fan_speed", "expected"), [("normal", 1), ("turbo", 3), ("silent", 4)]
|
||||
)
|
||||
async def test_vacuum_fan_speed(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
setup_platform: None,
|
||||
fan_speed: str,
|
||||
expected: int,
|
||||
) -> None:
|
||||
"""Test the vacuum can be controlled."""
|
||||
|
||||
await hass.services.async_call(
|
||||
TEST_PLATFORM,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: fan_speed},
|
||||
blocking=True,
|
||||
)
|
||||
mock_miele_client.send_action.assert_called_once_with(
|
||||
"Dummy_Vacuum_1", {"programId": expected}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service"),
|
||||
[
|
||||
(SERVICE_START),
|
||||
(SERVICE_STOP),
|
||||
],
|
||||
)
|
||||
async def test_api_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
setup_platform: None,
|
||||
service: str,
|
||||
) -> None:
|
||||
"""Test handling of exception from API."""
|
||||
mock_miele_client.send_action.side_effect = ClientResponseError("test", "Test")
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
TEST_PLATFORM, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True
|
||||
)
|
||||
mock_miele_client.send_action.assert_called_once()
|
Loading…
x
Reference in New Issue
Block a user