1 """Creates the sensor entities for the mower."""
3 from collections.abc
import Callable, Mapping
4 from dataclasses
import dataclass
5 from datetime
import datetime
7 from operator
import attrgetter
8 from typing
import TYPE_CHECKING, Any
10 from aioautomower.model
import (
21 SensorEntityDescription,
29 from .
import AutomowerConfigEntry
30 from .coordinator
import AutomowerDataUpdateCoordinator
33 WorkAreaAvailableEntity,
34 _work_area_translation_key,
37 _LOGGER = logging.getLogger(__name__)
39 ATTR_WORK_AREA_ID_ASSIGNMENT =
"work_area_id_assignment"
43 "alarm_mower_in_motion",
45 "alarm_mower_stopped",
46 "alarm_mower_switched_off",
48 "alarm_outside_geofence",
49 "angular_sensor_problem",
52 "battery_restriction_due_to_ambient_temperature",
54 "charging_current_too_high",
55 "charging_station_blocked",
56 "charging_system_problem",
57 "charging_system_problem",
58 "collision_sensor_defect",
59 "collision_sensor_error",
60 "collision_sensor_problem_front",
61 "collision_sensor_problem_rear",
62 "com_board_not_available",
63 "communication_circuit_board_sw_must_be_updated",
64 "complex_working_area",
66 "connection_not_changed",
67 "connectivity_problem",
68 "connectivity_problem",
69 "connectivity_problem",
70 "connectivity_problem",
71 "connectivity_problem",
72 "connectivity_problem",
73 "connectivity_settings_restored",
74 "cutting_drive_motor_1_defect",
75 "cutting_drive_motor_2_defect",
76 "cutting_drive_motor_3_defect",
77 "cutting_height_blocked",
78 "cutting_height_problem",
79 "cutting_height_problem_curr",
80 "cutting_height_problem_dir",
81 "cutting_height_problem_drive",
82 "cutting_motor_problem",
83 "cutting_stopped_slope_too_steep",
84 "cutting_system_blocked",
85 "cutting_system_blocked",
86 "cutting_system_imbalance_warning",
87 "cutting_system_major_imbalance",
88 "destination_not_reachable",
89 "difficult_finding_home",
90 "docking_sensor_defect",
93 MowerStates.ERROR.lower(),
94 MowerStates.ERROR_AT_POWER_UP.lower(),
95 MowerStates.FATAL_ERROR.lower(),
96 "folding_cutting_deck_sensor_defect",
97 "folding_sensor_activated",
100 "gps_navigation_problem",
104 "guide_calibration_accomplished",
105 "guide_calibration_failed",
106 "high_charging_power_loss",
107 "high_internal_power_loss",
108 "high_internal_temperature",
109 "internal_voltage_error",
110 "invalid_battery_combination_invalid_combination_of_different_battery_types",
111 "invalid_sub_device_combination",
112 "invalid_system_configuration",
113 "left_brush_motor_overloaded",
114 "lift_sensor_defect",
116 "limited_cutting_height_range",
117 "limited_cutting_height_range",
118 "loop_sensor_defect",
119 "loop_sensor_problem_front",
120 "loop_sensor_problem_left",
121 "loop_sensor_problem_rear",
122 "loop_sensor_problem_right",
124 "memory_circuit_problem",
127 "no_accurate_position_from_satellites",
128 "no_confirmed_position",
131 "no_power_in_charging_station",
132 "no_response_from_charger",
133 "outside_working_area",
134 "poor_signal_quality",
135 "reference_station_communication_problem",
136 "right_brush_motor_overloaded",
137 "safety_function_faulty",
143 "sim_card_not_found",
144 "sim_card_requires_pin",
145 "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
147 "sms_could_not_be_sent",
148 "stop_button_problem",
149 "stuck_in_charging_station",
150 "switch_cord_problem",
151 "temporary_battery_problem",
152 "temporary_battery_problem",
153 "temporary_battery_problem",
154 "temporary_battery_problem",
155 "temporary_battery_problem",
156 "temporary_battery_problem",
157 "temporary_battery_problem",
158 "temporary_battery_problem",
159 "tilt_sensor_problem",
160 "too_high_discharge_current",
161 "too_high_internal_current",
163 "ultrasonic_problem",
164 "ultrasonic_sensor_1_defect",
165 "ultrasonic_sensor_2_defect",
166 "ultrasonic_sensor_3_defect",
167 "ultrasonic_sensor_4_defect",
168 "unexpected_cutting_height_adj",
172 "wheel_drive_problem_left",
173 "wheel_drive_problem_rear_left",
174 "wheel_drive_problem_rear_right",
175 "wheel_drive_problem_right",
176 "wheel_motor_blocked_left",
177 "wheel_motor_blocked_rear_left",
178 "wheel_motor_blocked_rear_right",
179 "wheel_motor_blocked_right",
180 "wheel_motor_overloaded_left",
181 "wheel_motor_overloaded_rear_left",
182 "wheel_motor_overloaded_rear_right",
183 "wheel_motor_overloaded_right",
184 "work_area_not_valid",
187 "zone_generator_problem",
192 MowerStates.ERROR_AT_POWER_UP,
193 MowerStates.FATAL_ERROR,
196 RESTRICTED_REASONS: list = [
197 RestrictedReasons.ALL_WORK_AREAS_COMPLETED,
198 RestrictedReasons.DAILY_LIMIT,
199 RestrictedReasons.EXTERNAL,
200 RestrictedReasons.FOTA,
201 RestrictedReasons.FROST,
202 RestrictedReasons.NONE,
203 RestrictedReasons.NOT_APPLICABLE,
204 RestrictedReasons.PARK_OVERRIDE,
205 RestrictedReasons.SENSOR,
206 RestrictedReasons.WEEK_SCHEDULE,
209 STATE_NO_WORK_AREA_ACTIVE =
"no_work_area_active"
214 """Return a list with all work area names."""
217 assert data.work_areas
is not None
219 data.work_areas[work_area_id].name
for work_area_id
in data.work_areas
221 work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
222 return work_area_list
227 """Return the name of the current work area."""
228 if data.mower.work_area_id
is None:
229 return STATE_NO_WORK_AREA_ACTIVE
232 assert data.work_areas
is not None
233 return data.work_areas[data.mower.work_area_id].name
238 """Return the name of the current work area."""
241 assert data.work_areas
is not None
242 return {ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict}
247 """Return the error key, if not provided the mower state or `no error`."""
248 if data.mower.error_key
is not None:
249 return data.mower.error_key
250 if data.mower.state
in ERROR_STATES:
251 return data.mower.state.lower()
255 @dataclass(frozen=True, kw_only=True)
257 """Describes Automower sensor entity."""
259 exists_fn: Callable[[MowerAttributes], bool] =
lambda _:
True
260 extra_state_attributes_fn: Callable[[MowerAttributes], Mapping[str, Any] |
None] = (
263 option_fn: Callable[[MowerAttributes], list[str] |
None] =
lambda _:
None
264 value_fn: Callable[[MowerAttributes], StateType | datetime]
267 MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
269 key=
"battery_percent",
270 state_class=SensorStateClass.MEASUREMENT,
271 device_class=SensorDeviceClass.BATTERY,
272 native_unit_of_measurement=PERCENTAGE,
273 value_fn=attrgetter(
"battery.battery_percent"),
277 translation_key=
"mode",
278 device_class=SensorDeviceClass.ENUM,
279 option_fn=
lambda data:
list(MowerModes),
281 lambda data: data.mower.mode
282 if data.mower.mode != MowerModes.UNKNOWN
287 key=
"cutting_blade_usage_time",
288 translation_key=
"cutting_blade_usage_time",
289 state_class=SensorStateClass.TOTAL,
290 device_class=SensorDeviceClass.DURATION,
291 native_unit_of_measurement=UnitOfTime.SECONDS,
292 suggested_unit_of_measurement=UnitOfTime.HOURS,
293 exists_fn=
lambda data: data.statistics.cutting_blade_usage_time
is not None,
294 value_fn=attrgetter(
"statistics.cutting_blade_usage_time"),
297 key=
"total_charging_time",
298 translation_key=
"total_charging_time",
299 entity_category=EntityCategory.DIAGNOSTIC,
300 state_class=SensorStateClass.TOTAL,
301 device_class=SensorDeviceClass.DURATION,
302 native_unit_of_measurement=UnitOfTime.SECONDS,
303 suggested_unit_of_measurement=UnitOfTime.HOURS,
304 exists_fn=
lambda data: data.statistics.total_charging_time
is not None,
305 value_fn=attrgetter(
"statistics.total_charging_time"),
308 key=
"total_cutting_time",
309 translation_key=
"total_cutting_time",
310 entity_category=EntityCategory.DIAGNOSTIC,
311 state_class=SensorStateClass.TOTAL,
312 device_class=SensorDeviceClass.DURATION,
313 native_unit_of_measurement=UnitOfTime.SECONDS,
314 suggested_unit_of_measurement=UnitOfTime.HOURS,
315 exists_fn=
lambda data: data.statistics.total_cutting_time
is not None,
316 value_fn=attrgetter(
"statistics.total_cutting_time"),
319 key=
"total_running_time",
320 translation_key=
"total_running_time",
321 entity_category=EntityCategory.DIAGNOSTIC,
322 state_class=SensorStateClass.TOTAL,
323 device_class=SensorDeviceClass.DURATION,
324 native_unit_of_measurement=UnitOfTime.SECONDS,
325 suggested_unit_of_measurement=UnitOfTime.HOURS,
326 exists_fn=
lambda data: data.statistics.total_running_time
is not None,
327 value_fn=attrgetter(
"statistics.total_running_time"),
330 key=
"total_searching_time",
331 translation_key=
"total_searching_time",
332 entity_category=EntityCategory.DIAGNOSTIC,
333 state_class=SensorStateClass.TOTAL,
334 device_class=SensorDeviceClass.DURATION,
335 native_unit_of_measurement=UnitOfTime.SECONDS,
336 suggested_unit_of_measurement=UnitOfTime.HOURS,
337 exists_fn=
lambda data: data.statistics.total_searching_time
is not None,
338 value_fn=attrgetter(
"statistics.total_searching_time"),
341 key=
"number_of_charging_cycles",
342 translation_key=
"number_of_charging_cycles",
343 entity_category=EntityCategory.DIAGNOSTIC,
344 state_class=SensorStateClass.TOTAL,
345 exists_fn=
lambda data: data.statistics.number_of_charging_cycles
is not None,
346 value_fn=attrgetter(
"statistics.number_of_charging_cycles"),
349 key=
"number_of_collisions",
350 translation_key=
"number_of_collisions",
351 entity_category=EntityCategory.DIAGNOSTIC,
352 entity_registry_enabled_default=
False,
353 state_class=SensorStateClass.TOTAL,
354 exists_fn=
lambda data: data.statistics.number_of_collisions
is not None,
355 value_fn=attrgetter(
"statistics.number_of_collisions"),
358 key=
"total_drive_distance",
359 translation_key=
"total_drive_distance",
360 entity_category=EntityCategory.DIAGNOSTIC,
361 state_class=SensorStateClass.TOTAL,
362 device_class=SensorDeviceClass.DISTANCE,
363 native_unit_of_measurement=UnitOfLength.METERS,
364 suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
365 exists_fn=
lambda data: data.statistics.total_drive_distance
is not None,
366 value_fn=attrgetter(
"statistics.total_drive_distance"),
369 key=
"next_start_timestamp",
370 translation_key=
"next_start_timestamp",
371 device_class=SensorDeviceClass.TIMESTAMP,
372 value_fn=attrgetter(
"planner.next_start_datetime"),
376 translation_key=
"error",
377 device_class=SensorDeviceClass.ENUM,
378 option_fn=
lambda data: ERROR_KEY_LIST,
379 value_fn=_get_error_string,
382 key=
"restricted_reason",
383 translation_key=
"restricted_reason",
384 device_class=SensorDeviceClass.ENUM,
385 option_fn=
lambda data: RESTRICTED_REASONS,
386 value_fn=attrgetter(
"planner.restricted_reason"),
390 translation_key=
"work_area",
391 device_class=SensorDeviceClass.ENUM,
392 exists_fn=
lambda data: data.capabilities.work_areas,
393 extra_state_attributes_fn=_get_current_work_area_dict,
394 option_fn=_get_work_area_names,
395 value_fn=_get_current_work_area_name,
400 @dataclass(frozen=True, kw_only=True)
402 """Describes the work area sensor entities."""
404 exists_fn: Callable[[WorkArea], bool] =
lambda _:
True
405 value_fn: Callable[[WorkArea], StateType | datetime]
406 translation_key_fn: Callable[[int, str], str]
409 WORK_AREA_SENSOR_TYPES: tuple[WorkAreaSensorEntityDescription, ...] = (
412 translation_key_fn=_work_area_translation_key,
413 exists_fn=
lambda data: data.progress
is not None,
414 state_class=SensorStateClass.MEASUREMENT,
415 native_unit_of_measurement=PERCENTAGE,
416 value_fn=attrgetter(
"progress"),
419 key=
"last_time_completed",
420 translation_key_fn=_work_area_translation_key,
421 exists_fn=
lambda data: data.last_time_completed
is not None,
422 device_class=SensorDeviceClass.TIMESTAMP,
423 value_fn=attrgetter(
"last_time_completed"),
430 entry: AutomowerConfigEntry,
431 async_add_entities: AddEntitiesCallback,
433 """Set up sensor platform."""
434 coordinator = entry.runtime_data
435 current_work_areas: dict[str, set[int]] = {}
439 for mower_id, data
in coordinator.data.items()
440 for description
in MOWER_SENSOR_TYPES
441 if description.exists_fn(data)
444 def _async_work_area_listener() -> None:
445 """Listen for new work areas and add sensor entities if they did not exist.
447 Listening for deletable work areas is managed in the number platform.
449 for mower_id
in coordinator.data:
451 coordinator.data[mower_id].capabilities.work_areas
452 and (_work_areas := coordinator.data[mower_id].work_areas)
is not None
454 received_work_areas = set(_work_areas.keys())
455 new_work_areas = received_work_areas - current_work_areas.get(
459 current_work_areas.setdefault(mower_id, set()).
update(
464 mower_id, coordinator, description, work_area_id
466 for description
in WORK_AREA_SENSOR_TYPES
467 for work_area_id
in new_work_areas
468 if description.exists_fn(_work_areas[work_area_id])
471 coordinator.async_add_listener(_async_work_area_listener)
472 _async_work_area_listener()
476 """Defining the Automower Sensors with AutomowerSensorEntityDescription."""
478 entity_description: AutomowerSensorEntityDescription
479 _unrecorded_attributes = frozenset({ATTR_WORK_AREA_ID_ASSIGNMENT})
484 coordinator: AutomowerDataUpdateCoordinator,
485 description: AutomowerSensorEntityDescription,
487 """Set up AutomowerSensors."""
488 super().
__init__(mower_id, coordinator)
494 """Return the state of the sensor."""
499 """Return the option of the sensor."""
504 """Return the state attributes."""
509 """Defining the Work area sensors with WorkAreaSensorEntityDescription."""
511 entity_description: WorkAreaSensorEntityDescription
516 coordinator: AutomowerDataUpdateCoordinator,
517 description: WorkAreaSensorEntityDescription,
520 """Set up AutomowerSensors."""
521 super().
__init__(mower_id, coordinator, work_area_id)
530 """Return the state of the sensor."""
535 """Return the translation key of the work area."""
MowerAttributes mower_attributes(self)
WorkArea work_area_attributes(self)
StateType|datetime native_value(self)
list[str]|None options(self)
None __init__(self, str mower_id, AutomowerDataUpdateCoordinator coordinator, AutomowerSensorEntityDescription description)
Mapping[str, Any]|None extra_state_attributes(self)
str translation_key(self)
None __init__(self, str mower_id, AutomowerDataUpdateCoordinator coordinator, WorkAreaSensorEntityDescription description, int work_area_id)
_attr_translation_placeholders
StateType|datetime native_value(self)
None async_setup_entry(HomeAssistant hass, AutomowerConfigEntry entry, AddEntitiesCallback async_add_entities)
Mapping[str, Any] _get_current_work_area_dict(MowerAttributes data)
list[str] _get_work_area_names(MowerAttributes data)
str _get_current_work_area_name(MowerAttributes data)
str _get_error_string(MowerAttributes data)
IssData update(pyiss.ISS iss)