Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Creates the sensor entities for the mower."""
2 
3 from collections.abc import Callable, Mapping
4 from dataclasses import dataclass
5 from datetime import datetime
6 import logging
7 from operator import attrgetter
8 from typing import TYPE_CHECKING, Any
9 
10 from aioautomower.model import (
11  MowerAttributes,
12  MowerModes,
13  MowerStates,
14  RestrictedReasons,
15  WorkArea,
16 )
17 
19  SensorDeviceClass,
20  SensorEntity,
21  SensorEntityDescription,
22  SensorStateClass,
23 )
24 from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfLength, UnitOfTime
25 from homeassistant.core import HomeAssistant, callback
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.typing import StateType
28 
29 from . import AutomowerConfigEntry
30 from .coordinator import AutomowerDataUpdateCoordinator
31 from .entity import (
32  AutomowerBaseEntity,
33  WorkAreaAvailableEntity,
34  _work_area_translation_key,
35 )
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
40 
41 ERROR_KEY_LIST = [
42  "no_error",
43  "alarm_mower_in_motion",
44  "alarm_mower_lifted",
45  "alarm_mower_stopped",
46  "alarm_mower_switched_off",
47  "alarm_mower_tilted",
48  "alarm_outside_geofence",
49  "angular_sensor_problem",
50  "battery_problem",
51  "battery_problem",
52  "battery_restriction_due_to_ambient_temperature",
53  "can_error",
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",
65  "connection_changed",
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",
91  "electronic_problem",
92  "empty_battery",
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",
98  "geofence_problem",
99  "geofence_problem",
100  "gps_navigation_problem",
101  "guide_1_not_found",
102  "guide_2_not_found",
103  "guide_3_not_found",
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",
115  "lifted",
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",
123  "low_battery",
124  "memory_circuit_problem",
125  "mower_lifted",
126  "mower_tilted",
127  "no_accurate_position_from_satellites",
128  "no_confirmed_position",
129  "no_drive",
130  "no_loop_signal",
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",
138  "settings_restored",
139  "sim_card_locked",
140  "sim_card_locked",
141  "sim_card_locked",
142  "sim_card_locked",
143  "sim_card_not_found",
144  "sim_card_requires_pin",
145  "slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
146  "slope_too_steep",
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",
162  "trapped",
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",
169  "unexpected_error",
170  "upside_down",
171  "weak_gps_signal",
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",
185  "wrong_loop_signal",
186  "wrong_pin_code",
187  "zone_generator_problem",
188 ]
189 
190 ERROR_STATES = {
191  MowerStates.ERROR,
192  MowerStates.ERROR_AT_POWER_UP,
193  MowerStates.FATAL_ERROR,
194 }
195 
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,
207 ]
208 
209 STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
210 
211 
212 @callback
213 def _get_work_area_names(data: MowerAttributes) -> list[str]:
214  """Return a list with all work area names."""
215  if TYPE_CHECKING:
216  # Sensor does not get created if it is None
217  assert data.work_areas is not None
218  work_area_list = [
219  data.work_areas[work_area_id].name for work_area_id in data.work_areas
220  ]
221  work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
222  return work_area_list
223 
224 
225 @callback
226 def _get_current_work_area_name(data: MowerAttributes) -> str:
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
230  if TYPE_CHECKING:
231  # Sensor does not get created if values are None
232  assert data.work_areas is not None
233  return data.work_areas[data.mower.work_area_id].name
234 
235 
236 @callback
237 def _get_current_work_area_dict(data: MowerAttributes) -> Mapping[str, Any]:
238  """Return the name of the current work area."""
239  if TYPE_CHECKING:
240  # Sensor does not get created if it is None
241  assert data.work_areas is not None
242  return {ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict}
243 
244 
245 @callback
246 def _get_error_string(data: MowerAttributes) -> str:
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()
252  return "no_error"
253 
254 
255 @dataclass(frozen=True, kw_only=True)
257  """Describes Automower sensor entity."""
258 
259  exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
260  extra_state_attributes_fn: Callable[[MowerAttributes], Mapping[str, Any] | None] = (
261  lambda _: None
262  )
263  option_fn: Callable[[MowerAttributes], list[str] | None] = lambda _: None
264  value_fn: Callable[[MowerAttributes], StateType | datetime]
265 
266 
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"),
274  ),
276  key="mode",
277  translation_key="mode",
278  device_class=SensorDeviceClass.ENUM,
279  option_fn=lambda data: list(MowerModes),
280  value_fn=(
281  lambda data: data.mower.mode
282  if data.mower.mode != MowerModes.UNKNOWN
283  else None
284  ),
285  ),
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"),
295  ),
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"),
306  ),
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"),
317  ),
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"),
328  ),
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"),
339  ),
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"),
347  ),
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"),
356  ),
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"),
367  ),
369  key="next_start_timestamp",
370  translation_key="next_start_timestamp",
371  device_class=SensorDeviceClass.TIMESTAMP,
372  value_fn=attrgetter("planner.next_start_datetime"),
373  ),
375  key="error",
376  translation_key="error",
377  device_class=SensorDeviceClass.ENUM,
378  option_fn=lambda data: ERROR_KEY_LIST,
379  value_fn=_get_error_string,
380  ),
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"),
387  ),
389  key="work_area",
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,
396  ),
397 )
398 
399 
400 @dataclass(frozen=True, kw_only=True)
402  """Describes the work area sensor entities."""
403 
404  exists_fn: Callable[[WorkArea], bool] = lambda _: True
405  value_fn: Callable[[WorkArea], StateType | datetime]
406  translation_key_fn: Callable[[int, str], str]
407 
408 
409 WORK_AREA_SENSOR_TYPES: tuple[WorkAreaSensorEntityDescription, ...] = (
411  key="progress",
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"),
417  ),
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"),
424  ),
425 )
426 
427 
429  hass: HomeAssistant,
430  entry: AutomowerConfigEntry,
431  async_add_entities: AddEntitiesCallback,
432 ) -> None:
433  """Set up sensor platform."""
434  coordinator = entry.runtime_data
435  current_work_areas: dict[str, set[int]] = {}
436 
438  AutomowerSensorEntity(mower_id, coordinator, description)
439  for mower_id, data in coordinator.data.items()
440  for description in MOWER_SENSOR_TYPES
441  if description.exists_fn(data)
442  )
443 
444  def _async_work_area_listener() -> None:
445  """Listen for new work areas and add sensor entities if they did not exist.
446 
447  Listening for deletable work areas is managed in the number platform.
448  """
449  for mower_id in coordinator.data:
450  if (
451  coordinator.data[mower_id].capabilities.work_areas
452  and (_work_areas := coordinator.data[mower_id].work_areas) is not None
453  ):
454  received_work_areas = set(_work_areas.keys())
455  new_work_areas = received_work_areas - current_work_areas.get(
456  mower_id, set()
457  )
458  if new_work_areas:
459  current_work_areas.setdefault(mower_id, set()).update(
460  new_work_areas
461  )
464  mower_id, coordinator, description, work_area_id
465  )
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])
469  )
470 
471  coordinator.async_add_listener(_async_work_area_listener)
472  _async_work_area_listener()
473 
474 
476  """Defining the Automower Sensors with AutomowerSensorEntityDescription."""
477 
478  entity_description: AutomowerSensorEntityDescription
479  _unrecorded_attributes = frozenset({ATTR_WORK_AREA_ID_ASSIGNMENT})
480 
481  def __init__(
482  self,
483  mower_id: str,
484  coordinator: AutomowerDataUpdateCoordinator,
485  description: AutomowerSensorEntityDescription,
486  ) -> None:
487  """Set up AutomowerSensors."""
488  super().__init__(mower_id, coordinator)
489  self.entity_descriptionentity_description = description
490  self._attr_unique_id_attr_unique_id = f"{mower_id}_{description.key}"
491 
492  @property
493  def native_value(self) -> StateType | datetime:
494  """Return the state of the sensor."""
495  return self.entity_descriptionentity_description.value_fn(self.mower_attributesmower_attributes)
496 
497  @property
498  def options(self) -> list[str] | None:
499  """Return the option of the sensor."""
500  return self.entity_descriptionentity_description.option_fn(self.mower_attributesmower_attributes)
501 
502  @property
503  def extra_state_attributes(self) -> Mapping[str, Any] | None:
504  """Return the state attributes."""
505  return self.entity_descriptionentity_description.extra_state_attributes_fn(self.mower_attributesmower_attributes)
506 
507 
509  """Defining the Work area sensors with WorkAreaSensorEntityDescription."""
510 
511  entity_description: WorkAreaSensorEntityDescription
512 
513  def __init__(
514  self,
515  mower_id: str,
516  coordinator: AutomowerDataUpdateCoordinator,
517  description: WorkAreaSensorEntityDescription,
518  work_area_id: int,
519  ) -> None:
520  """Set up AutomowerSensors."""
521  super().__init__(mower_id, coordinator, work_area_id)
522  self.entity_descriptionentity_description = description
523  self._attr_unique_id_attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}"
524  self._attr_translation_placeholders_attr_translation_placeholders = {
525  "work_area": self.work_area_attributeswork_area_attributes.name
526  }
527 
528  @property
529  def native_value(self) -> StateType | datetime:
530  """Return the state of the sensor."""
531  return self.entity_descriptionentity_description.value_fn(self.work_area_attributeswork_area_attributes)
532 
533  @property
534  def translation_key(self) -> str:
535  """Return the translation key of the work area."""
536  return self.entity_descriptionentity_description.translation_key_fn(
537  self.work_area_idwork_area_id, self.entity_descriptionentity_description.key
538  )
None __init__(self, str mower_id, AutomowerDataUpdateCoordinator coordinator, AutomowerSensorEntityDescription description)
Definition: sensor.py:486
None __init__(self, str mower_id, AutomowerDataUpdateCoordinator coordinator, WorkAreaSensorEntityDescription description, int work_area_id)
Definition: sensor.py:519
None async_setup_entry(HomeAssistant hass, AutomowerConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:432
Mapping[str, Any] _get_current_work_area_dict(MowerAttributes data)
Definition: sensor.py:237
list[str] _get_work_area_names(MowerAttributes data)
Definition: sensor.py:213
str _get_current_work_area_name(MowerAttributes data)
Definition: sensor.py:226
str _get_error_string(MowerAttributes data)
Definition: sensor.py:246
IssData update(pyiss.ISS iss)
Definition: __init__.py:33