core/homeassistant/components/meater/sensor.py

240 lines
8.0 KiB
Python

"""The Meater Temperature Probe integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from meater.MeaterApi import MeaterProbe
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import MeaterCoordinator
from .const import DOMAIN, MEATER_DATA
from .coordinator import MeaterConfigEntry
COOK_STATES = {
"Not Started": "not_started",
"Configured": "configured",
"Started": "started",
"Ready For Resting": "ready_for_resting",
"Resting": "resting",
"Slightly Underdone": "slightly_underdone",
"Finished": "finished",
"Slightly Overdone": "slightly_overdone",
"OVERCOOK!": "overcooked",
}
@dataclass(frozen=True, kw_only=True)
class MeaterSensorEntityDescription(SensorEntityDescription):
"""Describes meater sensor entity."""
value: Callable[[MeaterProbe], datetime | float | str | None]
unavailable_when_not_cooking: bool = False
def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
"""Convert elapsed time to timestamp."""
if not probe.cook or not hasattr(probe.cook, "time_elapsed"):
return None
return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed)
def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None:
"""Convert remaining time to timestamp."""
if (
not probe.cook
or not hasattr(probe.cook, "time_remaining")
or probe.cook.time_remaining < 0
):
return None
return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining)
SENSOR_TYPES = (
# Ambient temperature
MeaterSensorEntityDescription(
key="ambient",
translation_key="ambient",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value=lambda probe: probe.ambient_temperature,
),
# Internal temperature (probe tip)
MeaterSensorEntityDescription(
key="internal",
translation_key="internal",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value=lambda probe: probe.internal_temperature,
),
# Name of selected meat in user language or user given custom name
MeaterSensorEntityDescription(
key="cook_name",
translation_key="cook_name",
unavailable_when_not_cooking=True,
value=lambda probe: probe.cook.name if probe.cook else None,
),
MeaterSensorEntityDescription(
key="cook_state",
translation_key="cook_state",
unavailable_when_not_cooking=True,
device_class=SensorDeviceClass.ENUM,
options=list(COOK_STATES.values()),
value=lambda probe: COOK_STATES.get(probe.cook.state) if probe.cook else None,
),
# Target temperature
MeaterSensorEntityDescription(
key="cook_target_temp",
translation_key="cook_target_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
unavailable_when_not_cooking=True,
value=(
lambda probe: probe.cook.target_temperature
if probe.cook and hasattr(probe.cook, "target_temperature")
else None
),
),
# Peak temperature
MeaterSensorEntityDescription(
key="cook_peak_temp",
translation_key="cook_peak_temp",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
unavailable_when_not_cooking=True,
value=(
lambda probe: probe.cook.peak_temperature
if probe.cook and hasattr(probe.cook, "peak_temperature")
else None
),
),
# Remaining time in seconds. When unknown/calculating default is used. Default: -1
# Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time.
MeaterSensorEntityDescription(
key="cook_time_remaining",
translation_key="cook_time_remaining",
device_class=SensorDeviceClass.TIMESTAMP,
unavailable_when_not_cooking=True,
value=_remaining_time_to_timestamp,
),
# Time since the start of cook in seconds. Default: 0. Exposed as a TIMESTAMP sensor
# where the timestamp is current time - elapsed time.
MeaterSensorEntityDescription(
key="cook_time_elapsed",
translation_key="cook_time_elapsed",
device_class=SensorDeviceClass.TIMESTAMP,
unavailable_when_not_cooking=True,
value=_elapsed_time_to_timestamp,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MeaterConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the entry."""
coordinator = entry.runtime_data
@callback
def async_update_data():
"""Handle updated data from the API endpoint."""
if not coordinator.last_update_success:
return None
devices = coordinator.data
entities = []
known_probes = hass.data[MEATER_DATA]
# Add entities for temperature probes which we've not yet seen
for device_id in devices:
if device_id in known_probes:
continue
entities.extend(
[
MeaterProbeTemperature(coordinator, device_id, sensor_description)
for sensor_description in SENSOR_TYPES
]
)
known_probes.add(device_id)
async_add_entities(entities)
return devices
# Add a subscriber to the coordinator to discover new temperature probes
coordinator.async_add_listener(async_update_data)
async_update_data()
class MeaterProbeTemperature(SensorEntity, CoordinatorEntity[MeaterCoordinator]):
"""Meater Temperature Sensor Entity."""
_attr_has_entity_name = True
entity_description: MeaterSensorEntityDescription
def __init__(
self,
coordinator: MeaterCoordinator,
device_id: str,
description: MeaterSensorEntityDescription,
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, device_id)
},
manufacturer="Apption Labs",
model="Meater Probe",
name=f"Meater Probe {device_id[:8]}",
)
self._attr_unique_id = f"{device_id}-{description.key}"
self.device_id = device_id
self.entity_description = description
@property
def probe(self) -> MeaterProbe:
"""Return the probe."""
return self.coordinator.data[self.device_id]
@property
def native_value(self) -> datetime | float | str | None:
"""Return the temperature of the probe."""
return self.entity_description.value(self.probe)
@property
def available(self) -> bool:
"""Return if entity is available."""
# See if the device was returned from the API. If not, it's offline
return (
super().available
and self.device_id in self.coordinator.data
and (
not self.entity_description.unavailable_when_not_cooking
or self.probe.cook is not None
)
)