Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """The Meater Temperature Probe integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import datetime, timedelta
8 
9 from meater.MeaterApi import MeaterProbe
10 
12  SensorDeviceClass,
13  SensorEntity,
14  SensorEntityDescription,
15  SensorStateClass,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import UnitOfTemperature
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.helpers.device_registry import DeviceInfo
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23  CoordinatorEntity,
24  DataUpdateCoordinator,
25 )
26 from homeassistant.util import dt as dt_util
27 
28 from .const import DOMAIN
29 
30 
31 @dataclass(frozen=True, kw_only=True)
33  """Describes meater sensor entity."""
34 
35  available: Callable[[MeaterProbe | None], bool]
36  value: Callable[[MeaterProbe], datetime | float | str | None]
37 
38 
39 def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
40  """Convert elapsed time to timestamp."""
41  if not probe.cook or not hasattr(probe.cook, "time_elapsed"):
42  return None
43  return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed)
44 
45 
46 def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
47  """Convert remaining time to timestamp."""
48  if (
49  not probe.cook
50  or not hasattr(probe.cook, "time_remaining")
51  or probe.cook.time_remaining < 0
52  ):
53  return None
54  return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining)
55 
56 
57 SENSOR_TYPES = (
58  # Ambient temperature
60  key="ambient",
61  translation_key="ambient",
62  device_class=SensorDeviceClass.TEMPERATURE,
63  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
64  state_class=SensorStateClass.MEASUREMENT,
65  available=lambda probe: probe is not None,
66  value=lambda probe: probe.ambient_temperature,
67  ),
68  # Internal temperature (probe tip)
70  key="internal",
71  translation_key="internal",
72  device_class=SensorDeviceClass.TEMPERATURE,
73  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
74  state_class=SensorStateClass.MEASUREMENT,
75  available=lambda probe: probe is not None,
76  value=lambda probe: probe.internal_temperature,
77  ),
78  # Name of selected meat in user language or user given custom name
80  key="cook_name",
81  translation_key="cook_name",
82  available=lambda probe: probe is not None and probe.cook is not None,
83  value=lambda probe: probe.cook.name if probe.cook else None,
84  ),
85  # One of Not Started, Configured, Started, Ready For Resting, Resting,
86  # Slightly Underdone, Finished, Slightly Overdone, OVERCOOK!. Not translated.
88  key="cook_state",
89  translation_key="cook_state",
90  available=lambda probe: probe is not None and probe.cook is not None,
91  value=lambda probe: probe.cook.state if probe.cook else None,
92  ),
93  # Target temperature
95  key="cook_target_temp",
96  translation_key="cook_target_temp",
97  device_class=SensorDeviceClass.TEMPERATURE,
98  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
99  state_class=SensorStateClass.MEASUREMENT,
100  available=lambda probe: probe is not None and probe.cook is not None,
101  value=lambda probe: probe.cook.target_temperature
102  if probe.cook and hasattr(probe.cook, "target_temperature")
103  else None,
104  ),
105  # Peak temperature
107  key="cook_peak_temp",
108  translation_key="cook_peak_temp",
109  device_class=SensorDeviceClass.TEMPERATURE,
110  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
111  state_class=SensorStateClass.MEASUREMENT,
112  available=lambda probe: probe is not None and probe.cook is not None,
113  value=lambda probe: probe.cook.peak_temperature
114  if probe.cook and hasattr(probe.cook, "peak_temperature")
115  else None,
116  ),
117  # Remaining time in seconds. When unknown/calculating default is used. Default: -1
118  # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time.
120  key="cook_time_remaining",
121  translation_key="cook_time_remaining",
122  device_class=SensorDeviceClass.TIMESTAMP,
123  available=lambda probe: probe is not None and probe.cook is not None,
124  value=_remaining_time_to_timestamp,
125  ),
126  # Time since the start of cook in seconds. Default: 0. Exposed as a TIMESTAMP sensor
127  # where the timestamp is current time - elapsed time.
129  key="cook_time_elapsed",
130  translation_key="cook_time_elapsed",
131  device_class=SensorDeviceClass.TIMESTAMP,
132  available=lambda probe: probe is not None and probe.cook is not None,
133  value=_elapsed_time_to_timestamp,
134  ),
135 )
136 
137 
139  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
140 ) -> None:
141  """Set up the entry."""
142  coordinator: DataUpdateCoordinator[dict[str, MeaterProbe]] = hass.data[DOMAIN][
143  entry.entry_id
144  ]["coordinator"]
145 
146  @callback
147  def async_update_data():
148  """Handle updated data from the API endpoint."""
149  if not coordinator.last_update_success:
150  return None
151 
152  devices = coordinator.data
153  entities = []
154  known_probes: set = hass.data[DOMAIN]["known_probes"]
155 
156  # Add entities for temperature probes which we've not yet seen
157  for device_id in devices:
158  if device_id in known_probes:
159  continue
160 
161  entities.extend(
162  [
163  MeaterProbeTemperature(coordinator, device_id, sensor_description)
164  for sensor_description in SENSOR_TYPES
165  ]
166  )
167  known_probes.add(device_id)
168 
169  async_add_entities(entities)
170 
171  return devices
172 
173  # Add a subscriber to the coordinator to discover new temperature probes
174  coordinator.async_add_listener(async_update_data)
175 
176 
178  SensorEntity, CoordinatorEntity[DataUpdateCoordinator[dict[str, MeaterProbe]]]
179 ):
180  """Meater Temperature Sensor Entity."""
181 
182  entity_description: MeaterSensorEntityDescription
183 
184  def __init__(
185  self, coordinator, device_id, description: MeaterSensorEntityDescription
186  ) -> None:
187  """Initialise the sensor."""
188  super().__init__(coordinator)
189  self._attr_device_info_attr_device_info = DeviceInfo(
190  identifiers={
191  # Serial numbers are unique identifiers within a specific domain
192  (DOMAIN, device_id)
193  },
194  manufacturer="Apption Labs",
195  model="Meater Probe",
196  name=f"Meater Probe {device_id}",
197  )
198  self._attr_unique_id_attr_unique_id = f"{device_id}-{description.key}"
199 
200  self.device_iddevice_id = device_id
201  self.entity_descriptionentity_description = description
202 
203  @property
204  def native_value(self):
205  """Return the temperature of the probe."""
206  if not (device := self.coordinator.data.get(self.device_iddevice_id)):
207  return None
208 
209  return self.entity_descriptionentity_description.value(device)
210 
211  @property
212  def available(self) -> bool:
213  """Return if entity is available."""
214  # See if the device was returned from the API. If not, it's offline
215  return (
216  self.coordinator.last_update_success
217  and self.entity_descriptionentity_description.available(
218  self.coordinator.data.get(self.device_iddevice_id)
219  )
220  )
None __init__(self, coordinator, device_id, MeaterSensorEntityDescription description)
Definition: sensor.py:186
datetime|None _elapsed_time_to_timestamp(MeaterProbe probe)
Definition: sensor.py:39
datetime|None _remaining_time_to_timestamp(MeaterProbe probe)
Definition: sensor.py:46
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:140