"""Definition of Picnic sensors.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime from typing import Any, Literal, cast from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CURRENCY_EURO from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) from homeassistant.util import dt as dt_util from .const import ( ADDRESS, ATTRIBUTION, CONF_COORDINATOR, DOMAIN, SENSOR_CART_ITEMS_COUNT, SENSOR_CART_TOTAL_PRICE, SENSOR_LAST_ORDER_DELIVERY_TIME, SENSOR_LAST_ORDER_MAX_ORDER_TIME, SENSOR_LAST_ORDER_SLOT_END, SENSOR_LAST_ORDER_SLOT_START, SENSOR_LAST_ORDER_STATUS, SENSOR_LAST_ORDER_TOTAL_PRICE, SENSOR_NEXT_DELIVERY_ETA_END, SENSOR_NEXT_DELIVERY_ETA_START, SENSOR_NEXT_DELIVERY_SLOT_END, SENSOR_NEXT_DELIVERY_SLOT_START, SENSOR_SELECTED_SLOT_END, SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, SENSOR_SELECTED_SLOT_START, ) @dataclass class PicnicRequiredKeysMixin: """Mixin for required keys.""" data_type: Literal[ "cart_data", "slot_data", "next_delivery_data", "last_order_data" ] value_fn: Callable[[Any], StateType | datetime] @dataclass class PicnicSensorEntityDescription(SensorEntityDescription, PicnicRequiredKeysMixin): """Describes Picnic sensor entity.""" entity_registry_enabled_default: bool = False SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( PicnicSensorEntityDescription( key=SENSOR_CART_ITEMS_COUNT, translation_key=SENSOR_CART_ITEMS_COUNT, icon="mdi:format-list-numbered", data_type="cart_data", value_fn=lambda cart: cart.get("total_count", 0), ), PicnicSensorEntityDescription( key=SENSOR_CART_TOTAL_PRICE, translation_key=SENSOR_CART_TOTAL_PRICE, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="cart_data", value_fn=lambda cart: cart.get("total_price", 0) / 100, ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_START, translation_key=SENSOR_SELECTED_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_END, translation_key=SENSOR_SELECTED_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, translation_key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, translation_key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="slot_data", value_fn=lambda slot: ( slot["minimum_order_value"] / 100 if slot.get("minimum_order_value") else None ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_START, translation_key=SENSOR_LAST_ORDER_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("slot", {}).get("window_start")) ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_END, translation_key=SENSOR_LAST_ORDER_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("slot", {}).get("window_end")) ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_STATUS, translation_key=SENSOR_LAST_ORDER_STATUS, icon="mdi:list-status", data_type="last_order_data", value_fn=lambda last_order: last_order.get("status"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_MAX_ORDER_TIME, translation_key=SENSOR_LAST_ORDER_MAX_ORDER_TIME, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("slot", {}).get("cut_off_time")) ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_DELIVERY_TIME, translation_key=SENSOR_LAST_ORDER_DELIVERY_TIME, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", value_fn=lambda last_order: dt_util.parse_datetime( str(last_order.get("delivery_time", {}).get("start")) ), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_TOTAL_PRICE, translation_key=SENSOR_LAST_ORDER_TOTAL_PRICE, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:cash-marker", data_type="last_order_data", value_fn=lambda last_order: last_order.get("total_price", 0) / 100, ), PicnicSensorEntityDescription( key=SENSOR_NEXT_DELIVERY_ETA_START, translation_key=SENSOR_NEXT_DELIVERY_ETA_START, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("eta", {}).get("start")) ), ), PicnicSensorEntityDescription( key=SENSOR_NEXT_DELIVERY_ETA_END, translation_key=SENSOR_NEXT_DELIVERY_ETA_END, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("eta", {}).get("end")) ), ), PicnicSensorEntityDescription( key=SENSOR_NEXT_DELIVERY_SLOT_START, translation_key=SENSOR_NEXT_DELIVERY_SLOT_START, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-start", data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("slot", {}).get("window_start")) ), ), PicnicSensorEntityDescription( key=SENSOR_NEXT_DELIVERY_SLOT_END, translation_key=SENSOR_NEXT_DELIVERY_SLOT_END, device_class=SensorDeviceClass.TIMESTAMP, icon="mdi:calendar-end", data_type="next_delivery_data", value_fn=lambda next_delivery: dt_util.parse_datetime( str(next_delivery.get("slot", {}).get("window_end")) ), ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up Picnic sensor entries.""" picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR] # Add an entity for each sensor type async_add_entities( PicnicSensor(picnic_coordinator, config_entry, description) for description in SENSOR_TYPES ) class PicnicSensor(SensorEntity, CoordinatorEntity): """The CoordinatorEntity subclass representing Picnic sensors.""" _attr_has_entity_name = True _attr_attribution = ATTRIBUTION entity_description: PicnicSensorEntityDescription def __init__( self, coordinator: DataUpdateCoordinator[Any], config_entry: ConfigEntry, description: PicnicSensorEntityDescription, ) -> None: """Init a Picnic sensor.""" super().__init__(coordinator) self.entity_description = description self.entity_id = f"sensor.picnic_{description.key}" self._service_unique_id = config_entry.unique_id self._attr_unique_id = f"{config_entry.unique_id}.{description.key}" @property def native_value(self) -> StateType | datetime: """Return the value reported by the sensor.""" data_set = ( self.coordinator.data.get(self.entity_description.data_type, {}) if self.coordinator.data is not None else {} ) return self.entity_description.value_fn(data_set) @property def available(self) -> bool: """Return True if last update was successful.""" return self.coordinator.last_update_success @property def device_info(self) -> DeviceInfo: """Return device info.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, cast(str, self._service_unique_id))}, manufacturer="Picnic", model=self._service_unique_id, name=f"Picnic: {self.coordinator.data[ADDRESS]}", )