736 lines
30 KiB
Python
736 lines
30 KiB
Python
"""Support for Dreame Vacuum selects."""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import voluptuous as vol
|
|
from typing import Any
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
|
|
from homeassistant.components.select import (
|
|
SelectEntity,
|
|
SelectEntityDescription,
|
|
)
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.entity import EntityCategory
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers import entity_platform, entity_registry
|
|
|
|
from .const import (
|
|
DOMAIN,
|
|
UNIT_HOURS,
|
|
UNIT_TIMES,
|
|
INPUT_CYCLE,
|
|
SERVICE_SELECT_NEXT,
|
|
SERVICE_SELECT_PREVIOUS,
|
|
SERVICE_SELECT_FIRST,
|
|
SERVICE_SELECT_LAST,
|
|
)
|
|
|
|
from .coordinator import DreameVacuumDataUpdateCoordinator
|
|
from .entity import (
|
|
DreameVacuumEntity,
|
|
DreameVacuumEntityDescription,
|
|
)
|
|
|
|
from .dreame import (
|
|
DreameVacuumProperty,
|
|
DreameVacuumSuctionLevel,
|
|
DreameVacuumCleaningMode,
|
|
DreameVacuumWaterVolume,
|
|
DreameVacuumSelfCleanArea,
|
|
DreameVacuumMopPadHumidity,
|
|
DreameVacuumCarpetSensitivity,
|
|
DreameVacuumMopWashLevel,
|
|
DreameVacuumMoppingType,
|
|
SUCTION_LEVEL_CODE_TO_NAME,
|
|
WATER_VOLUME_CODE_TO_NAME,
|
|
MOP_PAD_HUMIDITY_CODE_TO_NAME,
|
|
)
|
|
|
|
SUCTION_LEVEL_TO_ICON = {
|
|
DreameVacuumSuctionLevel.QUIET: "mdi:fan-speed-1",
|
|
DreameVacuumSuctionLevel.STANDARD: "mdi:fan-speed-2",
|
|
DreameVacuumSuctionLevel.STRONG: "mdi:fan-speed-3",
|
|
DreameVacuumSuctionLevel.TURBO: "mdi:weather-windy",
|
|
}
|
|
|
|
WATER_VOLUME_TO_ICON = {
|
|
DreameVacuumWaterVolume.LOW: "mdi:water-minus",
|
|
DreameVacuumWaterVolume.MEDIUM: "mdi:water",
|
|
DreameVacuumWaterVolume.HIGH: "mdi:water-plus",
|
|
}
|
|
|
|
MOP_PAD_HUMIDITY_TO_ICON = {
|
|
DreameVacuumMopPadHumidity.SLIGHTLY_DRY: "mdi:water-minus",
|
|
DreameVacuumMopPadHumidity.MOIST: "mdi:water",
|
|
DreameVacuumMopPadHumidity.WET: "mdi:water-plus",
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class DreameVacuumSelectEntityDescription(
|
|
DreameVacuumEntityDescription, SelectEntityDescription
|
|
):
|
|
"""Describes Dreame Vacuum Select entity."""
|
|
|
|
set_fn: Callable[[object, int, int]] = None
|
|
options: Callable[[object, object], list[str]] = None
|
|
value_int_fn: Callable[[object, str], int] = None
|
|
|
|
|
|
SELECTS: tuple[DreameVacuumSelectEntityDescription, ...] = (
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.SUCTION_LEVEL,
|
|
device_class=f"{DOMAIN}__suction_level",
|
|
icon_fn=lambda value, device: "mdi:fan-off"
|
|
if device.status.cleaning_mode is DreameVacuumCleaningMode.MOPPING
|
|
else SUCTION_LEVEL_TO_ICON.get(device.status.suction_level, "mdi:fan"),
|
|
options=lambda device, segment: list(device.status.suction_level_list),
|
|
value_int_fn=lambda value, device: DreameVacuumSuctionLevel[value.upper()],
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.WATER_VOLUME,
|
|
device_class=f"{DOMAIN}__water_volume",
|
|
icon_fn=lambda value, device: "mdi:water-off"
|
|
if (
|
|
not device.status.water_tank_or_mop_installed
|
|
or device.status.cleaning_mode is DreameVacuumCleaningMode.SWEEPING
|
|
)
|
|
else WATER_VOLUME_TO_ICON.get(device.status.water_volume, "mdi:water"),
|
|
options=lambda device, segment: list(device.status.water_volume_list),
|
|
value_int_fn=lambda value, device: DreameVacuumWaterVolume[value.upper()],
|
|
exists_fn=lambda description, device: bool(
|
|
not device.status.self_wash_base_available and
|
|
DreameVacuumEntityDescription().exists_fn(description, device)
|
|
),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.CLEANING_MODE,
|
|
device_class=f"{DOMAIN}__cleaning_mode",
|
|
icon_fn=lambda value, device: "mdi:hydro-power"
|
|
if device.status.cleaning_mode is DreameVacuumCleaningMode.SWEEPING_AND_MOPPING
|
|
else "mdi:cup-water"
|
|
if device.status.cleaning_mode is DreameVacuumCleaningMode.MOPPING
|
|
else "mdi:broom",
|
|
options=lambda device, segment: list(device.status.cleaning_mode_list),
|
|
value_fn=lambda value, device: device.status.cleaning_mode_name,
|
|
value_int_fn=lambda value, device: DreameVacuumCleaningMode[value.upper()],
|
|
set_fn=lambda device, map_id, value: device.set_cleaning_mode(value),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.CARPET_SENSITIVITY,
|
|
device_class=f"{DOMAIN}__carpet_sensitivity",
|
|
icon="mdi:rug",
|
|
options=lambda device, segment: list(device.status.carpet_sensitivity_list),
|
|
value_int_fn=lambda value, device: DreameVacuumCarpetSensitivity[value.upper()],
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.AUTO_EMPTY_FREQUENCY,
|
|
icon_fn=lambda value, device: f"mdi:numeric-{value[0]}-box-multiple-outline",
|
|
options=lambda device, segment: [f"{i}{UNIT_TIMES}" for i in range(1, 4)],
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: f"{value}{UNIT_TIMES}",
|
|
value_int_fn=lambda value, device: int(value[0]),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.DRYING_TIME,
|
|
icon="mdi:hair-dryer",
|
|
options=lambda device, segment: [f"{i}h" for i in range(2, 5)],
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: f"{value}h",
|
|
value_int_fn=lambda value, device: int(value[0]),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
property_key=DreameVacuumProperty.MOP_WASH_LEVEL,
|
|
device_class=f"{DOMAIN}__mop_wash_level",
|
|
icon="mdi:water-opacity",
|
|
options=lambda device, segment: list(device.status.mop_wash_level_list),
|
|
value_int_fn=lambda value, device: DreameVacuumMopWashLevel[value.upper()],
|
|
entity_category=EntityCategory.CONFIG,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="mop_pad_humidity",
|
|
device_class=f"{DOMAIN}__mop_pad_humidity",
|
|
icon_fn=lambda value, device: "mdi:water-off"
|
|
if (
|
|
not device.status.water_tank_or_mop_installed
|
|
or device.status.cleaning_mode is DreameVacuumCleaningMode.SWEEPING
|
|
)
|
|
else MOP_PAD_HUMIDITY_TO_ICON.get(device.status.mop_pad_humidity, "mdi:water-percent"),
|
|
options=lambda device, segment: list(device.status.mop_pad_humidity_list),
|
|
value_fn=lambda value, device: device.status.mop_pad_humidity_name,
|
|
value_int_fn=lambda value, device: DreameVacuumMopPadHumidity[value.upper()],
|
|
exists_fn=lambda description, device: device.status.self_wash_base_available,
|
|
available_fn=lambda device: device.status.water_tank_or_mop_installed and not device.status.sweeping and not (device.status.customized_cleaning and not (device.status.zone_cleaning or device.status.spot_cleaning)) and not device.status.fast_mapping and not device.status.started,
|
|
set_fn=lambda device, map_id, value: device.set_mop_pad_humidity(value),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="self_clean_area",
|
|
device_class=f"{DOMAIN}__self_clean_area",
|
|
icon="mdi:texture-box",
|
|
options=lambda device, segment: list(device.status.self_clean_area_list),
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: device.status.self_clean_area_name,
|
|
value_int_fn=lambda value, device: DreameVacuumSelfCleanArea[value.upper()],
|
|
exists_fn=lambda description, device: device.status.self_wash_base_available,
|
|
available_fn=lambda device: device.status.self_clean and not device.status.started and not device.status.fast_mapping and not device.status.cleaning_paused,
|
|
set_fn=lambda device, map_id, value: device.set_self_clean_area(value),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="mopping_type",
|
|
device_class=f"{DOMAIN}__mopping_type",
|
|
icon="mdi:spray-bottle",
|
|
options=lambda device, segment: list(device.status.mopping_type_list),
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: device.status.mopping_type_name,
|
|
value_int_fn=lambda value, device: DreameVacuumMoppingType[value.upper()],
|
|
exists_fn=lambda description, device: device.status.auto_switch_settings_available and device.status.mopping_type is not None,
|
|
available_fn=lambda device: not device.status.started and not device.status.fast_mapping and not device.status.cleaning_paused,
|
|
set_fn=lambda device, map_id, value: device.set_mopping_type(value),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="map_rotation",
|
|
icon="mdi:crop-rotate",
|
|
options=lambda device, segment: ["0", "90", "180", "270"],
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: str(device.status.selected_map.rotation)
|
|
if device.status.selected_map
|
|
and device.status.selected_map.rotation is not None
|
|
else "",
|
|
exists_fn=lambda description, device: device.status.map_available,
|
|
available_fn=lambda device: bool(
|
|
device.status.selected_map is not None
|
|
and device.status.selected_map.rotation is not None
|
|
and not device.status.fast_mapping
|
|
and device.status.has_saved_map
|
|
),
|
|
set_fn=lambda device, map_id, value: device.set_map_rotation(
|
|
device.status.selected_map.map_id, value
|
|
),
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="selected_map",
|
|
icon="mdi:map-check",
|
|
options=lambda device, segment: [
|
|
v.map_name for k, v in device.status.map_data_list.items()
|
|
],
|
|
entity_category=EntityCategory.CONFIG,
|
|
value_fn=lambda value, device: device.status.selected_map.map_name
|
|
if device.status.selected_map and device.status.selected_map.map_name
|
|
else "",
|
|
exists_fn=lambda description, device: device.status.map_available,# and device.status.lidar_navigation,
|
|
available_fn=lambda device: bool(
|
|
device.status.multi_map
|
|
and not device.status.fast_mapping
|
|
and device.status.map_list
|
|
and device.status.selected_map
|
|
and device.status.selected_map.map_name
|
|
and device.status.selected_map.map_id in device.status.map_list
|
|
),
|
|
value_int_fn=lambda value, device: next(
|
|
(k for k, v in device.status.map_data_list.items() if v.map_name == value),
|
|
None,
|
|
),
|
|
set_fn=lambda device, map_id, value: device.select_map(value),
|
|
attrs_fn=lambda device: {"map_id": device.status.selected_map.map_id, "map_index": device.status.selected_map.map_index}
|
|
if device.status.selected_map
|
|
else None,
|
|
),
|
|
)
|
|
|
|
SEGMENT_SELECTS: tuple[DreameVacuumSelectEntityDescription, ...] = (
|
|
DreameVacuumSelectEntityDescription(
|
|
key="suction_level",
|
|
device_class=f"{DOMAIN}__suction_level",
|
|
icon_fn=lambda value, segment: SUCTION_LEVEL_TO_ICON.get(
|
|
segment.suction_level, "mdi:fan"
|
|
)
|
|
if segment
|
|
else "mdi:fan-off",
|
|
options=lambda device, segment: list(device.status.suction_level_list),
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and next(iter(device.status.segments.values())).suction_level is not None
|
|
and device.status.customized_cleaning
|
|
and not (device.status.zone_cleaning or device.status.spot_cleaning)
|
|
and not device.status.fast_mapping
|
|
),
|
|
value_fn=lambda device, segment: SUCTION_LEVEL_CODE_TO_NAME.get(
|
|
segment.suction_level, STATE_UNKNOWN
|
|
),
|
|
value_int_fn=lambda value, self: DreameVacuumSuctionLevel[value.upper()],
|
|
set_fn=lambda device, segment_id, value: device.set_segment_suction_level(
|
|
segment_id, value
|
|
),
|
|
exists_fn=lambda description, device: device.status.customized_cleaning_available,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="water_volume",
|
|
device_class=f"{DOMAIN}__water_volume",
|
|
icon_fn=lambda value, segment: WATER_VOLUME_TO_ICON.get(
|
|
segment.water_volume, "mdi:water"
|
|
)
|
|
if segment
|
|
else "mdi:water-off",
|
|
options=lambda device, segment: list(device.status.water_volume_list),
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and next(iter(device.status.segments.values())).water_volume is not None
|
|
and device.status.customized_cleaning
|
|
and not (device.status.zone_cleaning or device.status.spot_cleaning)
|
|
and not device.status.fast_mapping
|
|
),
|
|
value_fn=lambda device, segment: WATER_VOLUME_CODE_TO_NAME.get(
|
|
segment.water_volume, STATE_UNKNOWN
|
|
),
|
|
value_int_fn=lambda value, self: DreameVacuumWaterVolume[value.upper()],
|
|
set_fn=lambda device, segment_id, value: device.set_segment_water_volume(
|
|
segment_id, value
|
|
),
|
|
exists_fn=lambda description, device: device.status.customized_cleaning_available and not device.status.self_wash_base_available,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="mop_pad_humidity",
|
|
device_class=f"{DOMAIN}__mop_pad_humidity",
|
|
icon_fn=lambda value, segment: MOP_PAD_HUMIDITY_TO_ICON.get(
|
|
segment.water_volume, "mdi:water-percent"
|
|
)
|
|
if segment
|
|
else "mdi:water-off",
|
|
options=lambda device, segment: list(device.status.mop_pad_humidity_list),
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and next(iter(device.status.segments.values())).mop_pad_humidity is not None
|
|
and device.status.customized_cleaning
|
|
and not (device.status.zone_cleaning or device.status.spot_cleaning)
|
|
and not device.status.fast_mapping
|
|
),
|
|
value_fn=lambda device, segment: MOP_PAD_HUMIDITY_CODE_TO_NAME.get(
|
|
segment.mop_pad_humidity, STATE_UNKNOWN
|
|
),
|
|
value_int_fn=lambda value, self: DreameVacuumMopPadHumidity[value.upper()],
|
|
set_fn=lambda device, segment_id, value: device.set_segment_mop_pad_humidity(
|
|
segment_id, value
|
|
),
|
|
exists_fn=lambda description, device: device.status.customized_cleaning_available and device.status.self_wash_base_available,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="cleaning_times",
|
|
icon_fn=lambda value, segment: "mdi:home-floor-" + str(segment.cleaning_times)
|
|
if segment and segment.cleaning_times and segment.cleaning_times < 4
|
|
else "mdi:home-floor-0",
|
|
options=lambda device, segment: [f"{i}{UNIT_TIMES}" for i in range(1, 4)],
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and next(iter(device.status.segments.values())).cleaning_times is not None
|
|
and device.status.customized_cleaning
|
|
and not (device.status.zone_cleaning or device.status.spot_cleaning)
|
|
and not device.status.started
|
|
and not device.status.fast_mapping
|
|
),
|
|
value_fn=lambda device, segment: f"{segment.cleaning_times}{UNIT_TIMES}",
|
|
value_int_fn=lambda value, self: int(value[0]),
|
|
set_fn=lambda device, segment_id, value: device.set_segment_cleaning_times(
|
|
segment_id, value
|
|
),
|
|
exists_fn=lambda description, device: device.status.customized_cleaning_available,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
key="order",
|
|
options=lambda device, segment: [
|
|
str(i) for i in range(1, len(device.status.segments.values()) + 1)
|
|
]
|
|
if device.status.segments
|
|
else [STATE_UNAVAILABLE],
|
|
entity_category=EntityCategory.CONFIG,
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and next(iter(device.status.segments.values())).order is not None
|
|
and not device.status.started
|
|
and device.status.custom_order
|
|
and device.status.has_saved_map
|
|
and not device.status.fast_mapping
|
|
),
|
|
value_fn=lambda device, segment: str(segment.order) if segment.order else STATE_UNAVAILABLE,
|
|
set_fn=lambda device, segment_id, value: device.set_segment_order(
|
|
segment_id, value
|
|
)
|
|
if value > 0
|
|
else None,
|
|
exists_fn=lambda description, device: device.status.customized_cleaning_available,
|
|
),
|
|
DreameVacuumSelectEntityDescription(
|
|
name="",
|
|
key="name",
|
|
options=lambda device, segment: list(segment.name_list(device.status.segments)),
|
|
entity_category=EntityCategory.CONFIG,
|
|
available_fn=lambda device: bool(
|
|
device.status.segments
|
|
and not device.status.fast_mapping
|
|
and not device.status.has_temporary_map
|
|
),
|
|
value_fn=lambda device, segment: device.status.segments[segment.segment_id].name
|
|
if segment.segment_id in device.status.segments
|
|
else None,
|
|
value_int_fn=lambda value, self: next(
|
|
(
|
|
type
|
|
for name, type in self.segment.name_list(
|
|
self.device.status.segments
|
|
).items()
|
|
if name == value
|
|
),
|
|
None,
|
|
),
|
|
set_fn=lambda device, segment_id, value: device.set_segment_name(
|
|
segment_id, value
|
|
),
|
|
attrs_fn=lambda segment: {
|
|
"room_id": segment.segment_id,
|
|
"index": segment.index,
|
|
"type": segment.type,
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up Dreame Vacuum select based on a config entry."""
|
|
coordinator: DreameVacuumDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
|
async_add_entities(
|
|
DreameVacuumSelectEntity(coordinator, description)
|
|
for description in SELECTS
|
|
if description.exists_fn(description, coordinator.device)
|
|
)
|
|
platform = entity_platform.current_platform.get()
|
|
platform.async_register_entity_service(
|
|
SERVICE_SELECT_NEXT,
|
|
{vol.Optional(INPUT_CYCLE, default=True): bool},
|
|
DreameVacuumSelectEntity.async_next.__name__,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SELECT_PREVIOUS,
|
|
{vol.Optional(INPUT_CYCLE, default=True): bool},
|
|
DreameVacuumSelectEntity.async_previous.__name__,
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SELECT_FIRST, {}, DreameVacuumSelectEntity.async_first.__name__
|
|
)
|
|
platform.async_register_entity_service(
|
|
SERVICE_SELECT_LAST, {}, DreameVacuumSelectEntity.async_last.__name__
|
|
)
|
|
|
|
update_segment_selects = partial(
|
|
async_update_segment_selects, coordinator, {}, async_add_entities
|
|
)
|
|
coordinator.async_add_listener(update_segment_selects)
|
|
update_segment_selects()
|
|
|
|
|
|
@callback
|
|
def async_update_segment_selects(
|
|
coordinator: DreameVacuumDataUpdateCoordinator,
|
|
current: dict[str, list[DreameVacuumSegmentSelectEntity]],
|
|
async_add_entities,
|
|
) -> None:
|
|
new_ids = []
|
|
if coordinator.device and coordinator.device.status.map_list:
|
|
for (k, v) in coordinator.device.status.map_data_list.items():
|
|
for (j, s) in v.segments.items():
|
|
if j not in new_ids:
|
|
new_ids.append(j)
|
|
|
|
new_ids = set(new_ids)
|
|
current_ids = set(current)
|
|
|
|
for segment_id in current_ids - new_ids:
|
|
async_remove_segment_selects(segment_id, coordinator, current)
|
|
|
|
new_entities = []
|
|
for segment_id in new_ids - current_ids:
|
|
current[segment_id] = [
|
|
DreameVacuumSegmentSelectEntity(coordinator, description, segment_id)
|
|
for description in SEGMENT_SELECTS
|
|
if description.exists_fn(description, coordinator.device)
|
|
]
|
|
new_entities = new_entities + current[segment_id]
|
|
|
|
if new_entities:
|
|
async_add_entities(new_entities)
|
|
|
|
|
|
def async_remove_segment_selects(
|
|
segment_id: str,
|
|
coordinator: DreameVacuumDataUpdateCoordinator,
|
|
current: dict[str, DreameVacuumSegmentSelectEntity],
|
|
) -> None:
|
|
registry = entity_registry.async_get(coordinator.hass)
|
|
entities = current[segment_id]
|
|
for entity in entities:
|
|
if entity.entity_id in registry.entities:
|
|
registry.async_remove(entity.entity_id)
|
|
del current[segment_id]
|
|
|
|
|
|
class DreameVacuumSelectEntity(DreameVacuumEntity, SelectEntity):
|
|
"""Defines a Dreame Vacuum select."""
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: DreameVacuumDataUpdateCoordinator,
|
|
description: SelectEntityDescription,
|
|
) -> None:
|
|
"""Initialize Dreame Vacuum select."""
|
|
super().__init__(coordinator, description)
|
|
if description.property_key is not None and description.value_fn is None:
|
|
prop = f'{description.property_key.name.lower()}_name'
|
|
if hasattr(coordinator.device.status, prop):
|
|
description.value_fn = lambda value, device: getattr(device.status, prop)
|
|
|
|
self._attr_options = description.options(coordinator.device, None)
|
|
self._attr_current_option = self.native_value
|
|
|
|
@callback
|
|
def _handle_coordinator_update(self) -> None:
|
|
self._attr_options = self.entity_description.options(self.device, None)
|
|
self._attr_current_option = self.native_value
|
|
super()._handle_coordinator_update()
|
|
|
|
@callback
|
|
async def async_select_index(self, idx: int) -> None:
|
|
"""Select new option by index."""
|
|
new_index = idx % len(self._attr_options)
|
|
await self.async_select_option(self._attr_options[new_index])
|
|
|
|
@callback
|
|
async def async_offset_index(self, offset: int, cycle: bool) -> None:
|
|
"""Offset current index."""
|
|
current_index = (self._attr_options.index(self._attr_current_option))
|
|
new_index = current_index + offset
|
|
if cycle:
|
|
new_index = new_index % len(self._attr_options)
|
|
elif new_index < 0:
|
|
new_index = 0
|
|
elif new_index >= len(self._attr_options):
|
|
new_index = len(self._attr_options) - 1
|
|
|
|
if cycle or current_index != new_index:
|
|
await self.async_select_option(self._attr_options[new_index])
|
|
|
|
@callback
|
|
async def async_first(self) -> None:
|
|
"""Select first option."""
|
|
await self.async_select_index(0)
|
|
|
|
@callback
|
|
async def async_last(self) -> None:
|
|
"""Select last option."""
|
|
await self.async_select_index(-1)
|
|
|
|
@callback
|
|
async def async_next(self, cycle: bool) -> None:
|
|
"""Select next option."""
|
|
await self.async_offset_index(1, cycle)
|
|
|
|
@callback
|
|
async def async_previous(self, cycle: bool) -> None:
|
|
"""Select previous option."""
|
|
await self.async_offset_index(-1, cycle)
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
"""Change the selected option."""
|
|
if not self.available:
|
|
raise HomeAssistantError("Entity unavailable")
|
|
|
|
if option not in self._attr_options:
|
|
raise HomeAssistantError(
|
|
f"Invalid option for {self.entity_description.name} {option}. Valid options: {self._attr_options}"
|
|
)
|
|
|
|
value = option
|
|
if self.entity_description.value_int_fn is not None:
|
|
value = self.entity_description.value_int_fn(option, self.device)
|
|
|
|
if value is None:
|
|
raise HomeAssistantError(
|
|
f"Invalid option for {self.entity_description.name} {option}. Valid options: {self._attr_options}"
|
|
)
|
|
|
|
if self.entity_description.set_fn is not None:
|
|
await self._try_command(
|
|
"Unable to call %s",
|
|
self.entity_description.set_fn,
|
|
self.device,
|
|
0,
|
|
int(value),
|
|
)
|
|
elif self.entity_description.property_key is not None:
|
|
await self._try_command(
|
|
"Unable to call %s",
|
|
self.device.set_property,
|
|
self.entity_description.property_key,
|
|
int(value),
|
|
)
|
|
|
|
class DreameVacuumSegmentSelectEntity(DreameVacuumEntity, SelectEntity):
|
|
"""Defines a Dreame Vacuum Segment select."""
|
|
|
|
def __init__(
|
|
self,
|
|
coordinator: DreameVacuumDataUpdateCoordinator,
|
|
description: DreameVacuumSelectEntityDescription,
|
|
segment_id: int,
|
|
) -> None:
|
|
"""Initialize Dreame Vacuum Segment Select."""
|
|
self.segment_id = segment_id
|
|
self.segment = None
|
|
self.segments = None
|
|
if coordinator.device:
|
|
self.segments = copy.deepcopy(coordinator.device.status.segments)
|
|
if segment_id in self.segments:
|
|
self.segment = self.segments[segment_id]
|
|
|
|
super().__init__(coordinator, description)
|
|
self._attr_unique_id = f"{self.device.mac}_room_{segment_id}_{description.key.lower()}"
|
|
self.entity_id = f"select.{self.device.name.lower()}_room_{segment_id}_{description.key.lower()}"
|
|
self._attr_options = []
|
|
self._attr_current_option = "unavailable"
|
|
if self.segment:
|
|
self._attr_options = description.options(coordinator.device, self.segment)
|
|
self._attr_current_option = self.native_value
|
|
|
|
def _set_id(self) -> None:
|
|
"""Set name, unique id and icon of the entity"""
|
|
if self.entity_description.name == "":
|
|
name = f"room_{self.segment_id}_{self.entity_description.key}"
|
|
elif self.segment:
|
|
name = f"{self.entity_description.key}_{self.segment.name}"
|
|
else:
|
|
name = f"{self.entity_description.key}_room_unavailable"
|
|
|
|
self._attr_name = f"{self.device.name} {name.replace('_', ' ').title()}"
|
|
|
|
if self.entity_description.icon_fn is not None:
|
|
self._attr_icon = self.entity_description.icon_fn(
|
|
self.native_value, self.segment
|
|
)
|
|
elif self.segment:
|
|
self._attr_icon = self.segment.icon
|
|
else:
|
|
self._attr_icon = "mdi:home-off-outline"
|
|
|
|
@callback
|
|
def _handle_coordinator_update(self) -> None:
|
|
if self.segments != self.device.status.segments:
|
|
self.segments = copy.deepcopy(self.device.status.segments)
|
|
if self.segments and self.segment_id in self.segments:
|
|
if self.segment != self.segments[self.segment_id]:
|
|
self.segment = self.segments[self.segment_id]
|
|
self._attr_current_option = self.native_value
|
|
self._set_id()
|
|
self._attr_options = self.entity_description.options(
|
|
self.device, self.segment
|
|
)
|
|
elif self.segment:
|
|
self._attr_options = []
|
|
self.segment = None
|
|
self._set_id()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
async def async_select_index(self, idx: int) -> None:
|
|
"""Select new option by index."""
|
|
new_index = idx % len(self._attr_options)
|
|
await self.async_select_option(self._attr_options[new_index])
|
|
|
|
@callback
|
|
async def async_offset_index(self, offset: int, cycle: bool) -> None:
|
|
"""Offset current index."""
|
|
current_index = (self._attr_options.index(self._attr_current_option))
|
|
new_index = current_index + offset
|
|
if cycle:
|
|
new_index = new_index % len(self._attr_options)
|
|
elif new_index < 0:
|
|
new_index = 0
|
|
elif new_index >= len(self._attr_options):
|
|
new_index = len(self._attr_options) - 1
|
|
|
|
if cycle or current_index != new_index:
|
|
await self.async_select_option(self._attr_options[new_index])
|
|
|
|
@callback
|
|
async def async_first(self) -> None:
|
|
"""Select first option."""
|
|
await self.async_select_index(0)
|
|
|
|
@callback
|
|
async def async_last(self) -> None:
|
|
"""Select last option."""
|
|
await self.async_select_index(-1)
|
|
|
|
@callback
|
|
async def async_next(self, cycle: bool) -> None:
|
|
"""Select next option."""
|
|
await self.async_offset_index(1, cycle)
|
|
|
|
@callback
|
|
async def async_previous(self, cycle: bool) -> None:
|
|
"""Select previous option."""
|
|
await self.async_offset_index(-1, cycle)
|
|
|
|
async def async_select_option(self, option: str) -> None:
|
|
"""Set the Dreame Vacuum Segment Select value."""
|
|
if not self.available:
|
|
raise HomeAssistantError("Entity unavailable")
|
|
|
|
value = option
|
|
if self.entity_description.value_int_fn is not None:
|
|
value = self.entity_description.value_int_fn(value, self)
|
|
|
|
if value is None:
|
|
raise HomeAssistantError(
|
|
"(%s) Invalid option (%s). Valid options: %s",
|
|
self.entity_description.name,
|
|
option,
|
|
self._attr_options,
|
|
)
|
|
|
|
await self._try_command(
|
|
"Unable to call %s",
|
|
self.entity_description.set_fn,
|
|
self.device,
|
|
self.segment_id,
|
|
int(value),
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return True if entity is available."""
|
|
if super().available:
|
|
return bool(self.segment is not None)
|
|
return False
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict[str, Any] | None:
|
|
"""Return the extra state attributes of the entity."""
|
|
if self.entity_description.attrs_fn is not None and self.segment:
|
|
return self.entity_description.attrs_fn(self.segment)
|
|
return None
|
|
|
|
@property
|
|
def native_value(self) -> str | None:
|
|
"""Return the current Dreame Vacuum number value."""
|
|
if self.segment:
|
|
return self.entity_description.value_fn(self.device, self.segment) |