| """Extended ESPHome entity types for Reachy Mini control."""
|
|
|
| from collections.abc import Iterable
|
| from typing import Callable, List, Optional
|
| import logging
|
|
|
| from aioesphomeapi.api_pb2 import (
|
| ListEntitiesButtonResponse,
|
| ListEntitiesRequest,
|
| ListEntitiesSelectResponse,
|
| ListEntitiesSensorResponse,
|
| ListEntitiesSwitchResponse,
|
| ButtonCommandRequest,
|
| SelectCommandRequest,
|
| SelectStateResponse,
|
| SensorStateResponse,
|
| SubscribeHomeAssistantStatesRequest,
|
| SubscribeStatesRequest,
|
| SwitchCommandRequest,
|
| SwitchStateResponse,
|
| )
|
| from google.protobuf import message
|
|
|
| from .api_server import APIServer
|
| from .entity import ESPHomeEntity
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
|
|
| class SensorStateClass:
|
| """ESPHome SensorStateClass enum values."""
|
| NONE = 0
|
| MEASUREMENT = 1
|
| TOTAL_INCREASING = 2
|
| TOTAL = 3
|
|
|
|
|
| class SensorEntity(ESPHomeEntity):
|
| """Sensor entity for ESPHome (read-only numeric values)."""
|
|
|
| def __init__(
|
| self,
|
| server: APIServer,
|
| key: int,
|
| name: str,
|
| object_id: str,
|
| icon: str = "",
|
| unit_of_measurement: str = "",
|
| accuracy_decimals: int = 2,
|
| device_class: str = "",
|
| state_class: int = SensorStateClass.NONE,
|
| entity_category: int = 0,
|
| value_getter: Optional[Callable[[], float]] = None,
|
| ) -> None:
|
| ESPHomeEntity.__init__(self, server)
|
| self.key = key
|
| self.name = name
|
| self.object_id = object_id
|
| self.icon = icon
|
| self.unit_of_measurement = unit_of_measurement
|
| self.accuracy_decimals = accuracy_decimals
|
| self.device_class = device_class
|
| self.entity_category = entity_category
|
|
|
| if isinstance(state_class, str):
|
| state_class_map = {
|
| "": SensorStateClass.NONE,
|
| "measurement": SensorStateClass.MEASUREMENT,
|
| "total_increasing": SensorStateClass.TOTAL_INCREASING,
|
| "total": SensorStateClass.TOTAL,
|
| }
|
| self.state_class = state_class_map.get(state_class.lower(), SensorStateClass.NONE)
|
| else:
|
| self.state_class = state_class
|
| self._value_getter = value_getter
|
| self._value = 0.0
|
|
|
| @property
|
| def value(self) -> float:
|
| if self._value_getter:
|
| return self._value_getter()
|
| return self._value
|
|
|
| @value.setter
|
| def value(self, new_value: float) -> None:
|
| self._value = new_value
|
|
|
| def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
| if isinstance(msg, ListEntitiesRequest):
|
| yield ListEntitiesSensorResponse(
|
| object_id=self.object_id,
|
| key=self.key,
|
| name=self.name,
|
| icon=self.icon,
|
| unit_of_measurement=self.unit_of_measurement,
|
| accuracy_decimals=self.accuracy_decimals,
|
| device_class=self.device_class,
|
| state_class=self.state_class,
|
| entity_category=self.entity_category,
|
| )
|
| elif isinstance(msg, (SubscribeHomeAssistantStatesRequest, SubscribeStatesRequest)):
|
| yield self._get_state_message()
|
|
|
| def _get_state_message(self) -> SensorStateResponse:
|
| return SensorStateResponse(
|
| key=self.key,
|
| state=self.value,
|
| missing_state=False,
|
| )
|
|
|
| def update_state(self) -> None:
|
| """Send state update to Home Assistant."""
|
| self.server.send_messages([self._get_state_message()])
|
|
|
|
|
| class SwitchEntity(ESPHomeEntity):
|
| """Switch entity for ESPHome (read-write boolean values)."""
|
|
|
| def __init__(
|
| self,
|
| server: APIServer,
|
| key: int,
|
| name: str,
|
| object_id: str,
|
| icon: str = "",
|
| device_class: str = "",
|
| entity_category: int = 0,
|
| value_getter: Optional[Callable[[], bool]] = None,
|
| value_setter: Optional[Callable[[bool], None]] = None,
|
| ) -> None:
|
| ESPHomeEntity.__init__(self, server)
|
| self.key = key
|
| self.name = name
|
| self.object_id = object_id
|
| self.icon = icon
|
| self.device_class = device_class
|
| self.entity_category = entity_category
|
| self._value_getter = value_getter
|
| self._value_setter = value_setter
|
| self._value = False
|
|
|
| @property
|
| def value(self) -> bool:
|
| if self._value_getter:
|
| return self._value_getter()
|
| return self._value
|
|
|
| @value.setter
|
| def value(self, new_value: bool) -> None:
|
| if self._value_setter:
|
| self._value_setter(new_value)
|
| self._value = new_value
|
|
|
| def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
| if isinstance(msg, ListEntitiesRequest):
|
| yield ListEntitiesSwitchResponse(
|
| object_id=self.object_id,
|
| key=self.key,
|
| name=self.name,
|
| icon=self.icon,
|
| device_class=self.device_class,
|
| entity_category=self.entity_category,
|
| )
|
| elif isinstance(msg, (SubscribeHomeAssistantStatesRequest, SubscribeStatesRequest)):
|
| yield self._get_state_message()
|
| elif isinstance(msg, SwitchCommandRequest) and msg.key == self.key:
|
| self.value = msg.state
|
| yield self._get_state_message()
|
|
|
| def _get_state_message(self) -> SwitchStateResponse:
|
| return SwitchStateResponse(
|
| key=self.key,
|
| state=self.value,
|
| )
|
|
|
| def update_state(self) -> None:
|
| """Send state update to Home Assistant."""
|
| self.server.send_messages([self._get_state_message()])
|
|
|
|
|
| class SelectEntity(ESPHomeEntity):
|
| """Select entity for ESPHome (read-write string selection)."""
|
|
|
| def __init__(
|
| self,
|
| server: APIServer,
|
| key: int,
|
| name: str,
|
| object_id: str,
|
| options: List[str],
|
| icon: str = "",
|
| entity_category: int = 0,
|
| value_getter: Optional[Callable[[], str]] = None,
|
| value_setter: Optional[Callable[[str], None]] = None,
|
| ) -> None:
|
| ESPHomeEntity.__init__(self, server)
|
| self.key = key
|
| self.name = name
|
| self.object_id = object_id
|
| self.options = options
|
| self.icon = icon
|
| self.entity_category = entity_category
|
| self._value_getter = value_getter
|
| self._value_setter = value_setter
|
| self._value = options[0] if options else ""
|
|
|
| @property
|
| def value(self) -> str:
|
| if self._value_getter:
|
| return self._value_getter()
|
| return self._value
|
|
|
| @value.setter
|
| def value(self, new_value: str) -> None:
|
| if new_value in self.options:
|
| if self._value_setter:
|
| self._value_setter(new_value)
|
| self._value = new_value
|
| else:
|
| logger.warning(f"Invalid option '{new_value}' for {self.name}")
|
|
|
| def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
| if isinstance(msg, ListEntitiesRequest):
|
| yield ListEntitiesSelectResponse(
|
| object_id=self.object_id,
|
| key=self.key,
|
| name=self.name,
|
| icon=self.icon,
|
| options=self.options,
|
| entity_category=self.entity_category,
|
| )
|
| elif isinstance(msg, (SubscribeHomeAssistantStatesRequest, SubscribeStatesRequest)):
|
| yield self._get_state_message()
|
| elif isinstance(msg, SelectCommandRequest) and msg.key == self.key:
|
| self.value = msg.state
|
| yield self._get_state_message()
|
|
|
| def _get_state_message(self) -> SelectStateResponse:
|
| return SelectStateResponse(
|
| key=self.key,
|
| state=self.value,
|
| missing_state=False,
|
| )
|
|
|
| def update_state(self) -> None:
|
| """Send state update to Home Assistant."""
|
| self.server.send_messages([self._get_state_message()])
|
|
|
|
|
| class ButtonEntity(ESPHomeEntity):
|
| """Button entity for ESPHome (trigger actions)."""
|
|
|
| def __init__(
|
| self,
|
| server: APIServer,
|
| key: int,
|
| name: str,
|
| object_id: str,
|
| icon: str = "",
|
| device_class: str = "",
|
| entity_category: int = 0,
|
| on_press: Optional[Callable[[], None]] = None,
|
| ) -> None:
|
| ESPHomeEntity.__init__(self, server)
|
| self.key = key
|
| self.name = name
|
| self.object_id = object_id
|
| self.icon = icon
|
| self.device_class = device_class
|
| self.entity_category = entity_category
|
| self._on_press = on_press
|
|
|
| def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
|
| if isinstance(msg, ListEntitiesRequest):
|
| yield ListEntitiesButtonResponse(
|
| object_id=self.object_id,
|
| key=self.key,
|
| name=self.name,
|
| icon=self.icon,
|
| device_class=self.device_class,
|
| entity_category=self.entity_category,
|
| )
|
| elif isinstance(msg, ButtonCommandRequest) and msg.key == self.key:
|
| if self._on_press:
|
| try:
|
| self._on_press()
|
| logger.info(f"Button '{self.name}' pressed")
|
| except Exception as e:
|
| logger.error(f"Error executing button '{self.name}': {e}")
|
|
|
| return
|
| yield
|
|
|