mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
497 lines
18 KiB
Python
497 lines
18 KiB
Python
"""Creates the sensor entities for the mower."""
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
import logging
|
|
from operator import attrgetter
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from aioautomower.model import (
|
|
ExternalReasons,
|
|
InactiveReasons,
|
|
MowerAttributes,
|
|
MowerModes,
|
|
RestrictedReasons,
|
|
WorkArea,
|
|
)
|
|
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
SensorStateClass,
|
|
)
|
|
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfTime
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
from homeassistant.helpers.typing import StateType
|
|
|
|
from . import AutomowerConfigEntry
|
|
from .const import ERROR_KEYS, ERROR_STATES
|
|
from .coordinator import AutomowerDataUpdateCoordinator
|
|
from .entity import (
|
|
AutomowerBaseEntity,
|
|
WorkAreaAvailableEntity,
|
|
_work_area_translation_key,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
# Coordinator is used to centralize the data updates
|
|
PARALLEL_UPDATES = 0
|
|
|
|
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
|
|
|
|
ERROR_KEY_LIST = sorted(
|
|
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
|
|
)
|
|
|
|
INACTIVE_REASONS: list = [
|
|
InactiveReasons.NONE,
|
|
InactiveReasons.PLANNING,
|
|
InactiveReasons.SEARCHING_FOR_SATELLITES,
|
|
]
|
|
|
|
|
|
RESTRICTED_REASONS: list = [
|
|
RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
|
|
RestrictedReasons.DAILY_LIMIT,
|
|
RestrictedReasons.EXTERNAL,
|
|
RestrictedReasons.FOTA,
|
|
RestrictedReasons.FROST,
|
|
RestrictedReasons.NONE,
|
|
RestrictedReasons.NOT_APPLICABLE,
|
|
RestrictedReasons.PARK_OVERRIDE,
|
|
RestrictedReasons.SENSOR,
|
|
RestrictedReasons.WEEK_SCHEDULE,
|
|
ExternalReasons.AMAZON_ALEXA,
|
|
ExternalReasons.DEVELOPER_PORTAL,
|
|
ExternalReasons.GARDENA_SMART_SYSTEM,
|
|
ExternalReasons.GOOGLE_ASSISTANT,
|
|
ExternalReasons.HOME_ASSISTANT,
|
|
ExternalReasons.IFTTT,
|
|
ExternalReasons.IFTTT_APPLETS,
|
|
ExternalReasons.IFTTT_CALENDAR_CONNECTION,
|
|
ExternalReasons.SMART_ROUTINE,
|
|
ExternalReasons.SMART_ROUTINE_FROST_GUARD,
|
|
ExternalReasons.SMART_ROUTINE_RAIN_GUARD,
|
|
ExternalReasons.SMART_ROUTINE_WILDLIFE_PROTECTION,
|
|
]
|
|
|
|
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
|
|
|
|
|
|
@callback
|
|
def _get_restricted_reason(data: MowerAttributes) -> str:
|
|
"""Return the restricted reason.
|
|
|
|
If there is an external reason, return that instead, if it's available.
|
|
"""
|
|
if (
|
|
data.planner.restricted_reason == RestrictedReasons.EXTERNAL
|
|
and data.planner.external_reason is not None
|
|
):
|
|
return data.planner.external_reason
|
|
return data.planner.restricted_reason
|
|
|
|
|
|
@callback
|
|
def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
|
"""Return a list with all work area names."""
|
|
if TYPE_CHECKING:
|
|
# Sensor does not get created if it is None
|
|
assert data.work_areas is not None
|
|
work_area_list = [
|
|
data.work_areas[work_area_id].name for work_area_id in data.work_areas
|
|
]
|
|
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
|
|
return work_area_list
|
|
|
|
|
|
@callback
|
|
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
|
"""Return the name of the current work area."""
|
|
if TYPE_CHECKING:
|
|
# Sensor does not get created if values are None
|
|
assert data.work_areas is not None
|
|
if (
|
|
data.mower.work_area_id is not None
|
|
and data.mower.work_area_id in data.work_areas
|
|
):
|
|
return data.work_areas[data.mower.work_area_id].name
|
|
|
|
return STATE_NO_WORK_AREA_ACTIVE
|
|
|
|
|
|
@callback
|
|
def _get_remaining_charging_time(data: MowerAttributes) -> int | None:
|
|
if data.battery.remaining_charging_time is not None:
|
|
return int(data.battery.remaining_charging_time.total_seconds())
|
|
return None
|
|
|
|
|
|
@callback
|
|
def _get_current_work_area_dict(data: MowerAttributes) -> Mapping[str, Any]:
|
|
"""Return the name of the current work area."""
|
|
if TYPE_CHECKING:
|
|
# Sensor does not get created if it is None
|
|
assert data.work_areas is not None
|
|
return {ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict}
|
|
|
|
|
|
@callback
|
|
def _get_error_string(data: MowerAttributes) -> str:
|
|
"""Return the error key, if not provided the mower state or `no error`."""
|
|
if data.mower.error_key is not None:
|
|
return data.mower.error_key
|
|
if data.mower.state in ERROR_STATES:
|
|
return data.mower.state.lower()
|
|
return "no_error"
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class AutomowerSensorEntityDescription(SensorEntityDescription):
|
|
"""Describes Automower sensor entity."""
|
|
|
|
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
|
|
extra_state_attributes_fn: Callable[[MowerAttributes], Mapping[str, Any] | None] = (
|
|
lambda _: None
|
|
)
|
|
option_fn: Callable[[MowerAttributes], list[str] | None] = lambda _: None
|
|
value_fn: Callable[[MowerAttributes], StateType | datetime]
|
|
|
|
|
|
MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
|
AutomowerSensorEntityDescription(
|
|
key="battery_percent",
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
device_class=SensorDeviceClass.BATTERY,
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
value_fn=attrgetter("battery.battery_percent"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="mode",
|
|
translation_key="mode",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
option_fn=lambda data: list(MowerModes),
|
|
value_fn=(
|
|
lambda data: data.mower.mode
|
|
if data.mower.mode != MowerModes.UNKNOWN
|
|
else None
|
|
),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="cutting_blade_usage_time",
|
|
translation_key="cutting_blade_usage_time",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
|
|
value_fn=attrgetter("statistics.cutting_blade_usage_time"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="downtime",
|
|
translation_key="downtime",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_display_precision=0,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.downtime is not None,
|
|
value_fn=attrgetter("statistics.downtime"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="total_charging_time",
|
|
translation_key="total_charging_time",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.total_charging_time is not None,
|
|
value_fn=attrgetter("statistics.total_charging_time"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="total_cutting_time",
|
|
translation_key="total_cutting_time",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.total_cutting_time is not None,
|
|
value_fn=attrgetter("statistics.total_cutting_time"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="total_running_time",
|
|
translation_key="total_running_time",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.total_running_time is not None,
|
|
value_fn=attrgetter("statistics.total_running_time"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="total_searching_time",
|
|
translation_key="total_searching_time",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.total_searching_time is not None,
|
|
value_fn=attrgetter("statistics.total_searching_time"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="number_of_charging_cycles",
|
|
translation_key="number_of_charging_cycles",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None,
|
|
value_fn=attrgetter("statistics.number_of_charging_cycles"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="number_of_collisions",
|
|
translation_key="number_of_collisions",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
entity_registry_enabled_default=False,
|
|
state_class=SensorStateClass.TOTAL,
|
|
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
|
|
value_fn=attrgetter("statistics.number_of_collisions"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="total_drive_distance",
|
|
translation_key="total_drive_distance",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DISTANCE,
|
|
native_unit_of_measurement=UnitOfLength.METERS,
|
|
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
|
|
exists_fn=lambda data: data.statistics.total_drive_distance is not None,
|
|
value_fn=attrgetter("statistics.total_drive_distance"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="uptime",
|
|
translation_key="uptime",
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
state_class=SensorStateClass.TOTAL,
|
|
device_class=SensorDeviceClass.DURATION,
|
|
entity_registry_enabled_default=False,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_display_precision=0,
|
|
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
exists_fn=lambda data: data.statistics.uptime is not None,
|
|
value_fn=attrgetter("statistics.uptime"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="next_start_timestamp",
|
|
translation_key="next_start_timestamp",
|
|
device_class=SensorDeviceClass.TIMESTAMP,
|
|
value_fn=attrgetter("planner.next_start_datetime"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="error",
|
|
translation_key="error",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
option_fn=lambda data: ERROR_KEY_LIST,
|
|
value_fn=_get_error_string,
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="restricted_reason",
|
|
translation_key="restricted_reason",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
option_fn=lambda data: RESTRICTED_REASONS,
|
|
value_fn=_get_restricted_reason,
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="inactive_reason",
|
|
translation_key="inactive_reason",
|
|
exists_fn=lambda data: data.capabilities.work_areas,
|
|
device_class=SensorDeviceClass.ENUM,
|
|
option_fn=lambda data: INACTIVE_REASONS,
|
|
value_fn=attrgetter("mower.inactive_reason"),
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="work_area",
|
|
translation_key="work_area",
|
|
device_class=SensorDeviceClass.ENUM,
|
|
exists_fn=lambda data: data.capabilities.work_areas,
|
|
extra_state_attributes_fn=_get_current_work_area_dict,
|
|
option_fn=_get_work_area_names,
|
|
value_fn=_get_current_work_area_name,
|
|
),
|
|
AutomowerSensorEntityDescription(
|
|
key="remaining_charging_time",
|
|
translation_key="remaining_charging_time",
|
|
device_class=SensorDeviceClass.DURATION,
|
|
value_fn=_get_remaining_charging_time,
|
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
|
suggested_display_precision=0,
|
|
),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class WorkAreaSensorEntityDescription(SensorEntityDescription):
|
|
"""Describes the work area sensor entities."""
|
|
|
|
exists_fn: Callable[[WorkArea], bool] = lambda _: True
|
|
value_fn: Callable[[WorkArea], StateType | datetime]
|
|
translation_key_fn: Callable[[int, str], str]
|
|
|
|
|
|
WORK_AREA_SENSOR_TYPES: tuple[WorkAreaSensorEntityDescription, ...] = (
|
|
WorkAreaSensorEntityDescription(
|
|
key="progress",
|
|
translation_key_fn=_work_area_translation_key,
|
|
exists_fn=lambda data: data.progress is not None,
|
|
state_class=SensorStateClass.MEASUREMENT,
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
value_fn=attrgetter("progress"),
|
|
),
|
|
WorkAreaSensorEntityDescription(
|
|
key="last_time_completed",
|
|
translation_key_fn=_work_area_translation_key,
|
|
exists_fn=lambda data: data.last_time_completed is not None,
|
|
device_class=SensorDeviceClass.TIMESTAMP,
|
|
value_fn=attrgetter("last_time_completed"),
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: AutomowerConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up sensor platform."""
|
|
coordinator = entry.runtime_data
|
|
entities: list[SensorEntity] = []
|
|
for mower_id in coordinator.data:
|
|
if coordinator.data[mower_id].capabilities.work_areas:
|
|
_work_areas = coordinator.data[mower_id].work_areas
|
|
if _work_areas is not None:
|
|
entities.extend(
|
|
WorkAreaSensorEntity(
|
|
mower_id, coordinator, description, work_area_id
|
|
)
|
|
for description in WORK_AREA_SENSOR_TYPES
|
|
for work_area_id in _work_areas
|
|
if description.exists_fn(_work_areas[work_area_id])
|
|
)
|
|
entities.extend(
|
|
AutomowerSensorEntity(mower_id, coordinator, description)
|
|
for description in MOWER_SENSOR_TYPES
|
|
if description.exists_fn(coordinator.data[mower_id])
|
|
)
|
|
async_add_entities(entities)
|
|
|
|
def _async_add_new_work_areas(mower_id: str, work_area_ids: set[int]) -> None:
|
|
mower_data = coordinator.data[mower_id]
|
|
if mower_data.work_areas is None:
|
|
return
|
|
|
|
async_add_entities(
|
|
WorkAreaSensorEntity(mower_id, coordinator, description, work_area_id)
|
|
for description in WORK_AREA_SENSOR_TYPES
|
|
for work_area_id in work_area_ids
|
|
if work_area_id in mower_data.work_areas
|
|
and description.exists_fn(mower_data.work_areas[work_area_id])
|
|
)
|
|
|
|
def _async_add_new_devices(mower_ids: set[str]) -> None:
|
|
async_add_entities(
|
|
AutomowerSensorEntity(mower_id, coordinator, description)
|
|
for mower_id in mower_ids
|
|
for description in MOWER_SENSOR_TYPES
|
|
if description.exists_fn(coordinator.data[mower_id])
|
|
)
|
|
for mower_id in mower_ids:
|
|
mower_data = coordinator.data[mower_id]
|
|
if mower_data.capabilities.work_areas and mower_data.work_areas is not None:
|
|
_async_add_new_work_areas(
|
|
mower_id,
|
|
set(mower_data.work_areas.keys()),
|
|
)
|
|
|
|
coordinator.new_devices_callbacks.append(_async_add_new_devices)
|
|
coordinator.new_areas_callbacks.append(_async_add_new_work_areas)
|
|
|
|
|
|
class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
|
|
"""Defining the Automower Sensors with AutomowerSensorEntityDescription."""
|
|
|
|
entity_description: AutomowerSensorEntityDescription
|
|
_unrecorded_attributes = frozenset({ATTR_WORK_AREA_ID_ASSIGNMENT})
|
|
|
|
def __init__(
|
|
self,
|
|
mower_id: str,
|
|
coordinator: AutomowerDataUpdateCoordinator,
|
|
description: AutomowerSensorEntityDescription,
|
|
) -> None:
|
|
"""Set up AutomowerSensors."""
|
|
super().__init__(mower_id, coordinator)
|
|
self.entity_description = description
|
|
self._attr_unique_id = f"{mower_id}_{description.key}"
|
|
|
|
@property
|
|
def native_value(self) -> StateType | datetime:
|
|
"""Return the state of the sensor."""
|
|
return self.entity_description.value_fn(self.mower_attributes)
|
|
|
|
@property
|
|
def options(self) -> list[str] | None:
|
|
"""Return the option of the sensor."""
|
|
return self.entity_description.option_fn(self.mower_attributes)
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
|
"""Return the state attributes."""
|
|
return self.entity_description.extra_state_attributes_fn(self.mower_attributes)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return the available attribute of the entity."""
|
|
return super().available and self.native_value is not None
|
|
|
|
|
|
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
|
|
"""Defining the Work area sensors with WorkAreaSensorEntityDescription."""
|
|
|
|
entity_description: WorkAreaSensorEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
mower_id: str,
|
|
coordinator: AutomowerDataUpdateCoordinator,
|
|
description: WorkAreaSensorEntityDescription,
|
|
work_area_id: int,
|
|
) -> None:
|
|
"""Set up AutomowerSensors."""
|
|
super().__init__(mower_id, coordinator, work_area_id)
|
|
self.entity_description = description
|
|
self._attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}"
|
|
self._attr_translation_placeholders = {
|
|
"work_area": self.work_area_attributes.name
|
|
}
|
|
|
|
@property
|
|
def native_value(self) -> StateType | datetime:
|
|
"""Return the state of the sensor."""
|
|
return self.entity_description.value_fn(self.work_area_attributes)
|
|
|
|
@property
|
|
def translation_key(self) -> str:
|
|
"""Return the translation key of the work area."""
|
|
return self.entity_description.translation_key_fn(
|
|
self.work_area_id, self.entity_description.key
|
|
)
|