Desmond-Dong's picture
Update app
9da872f
raw
history blame
10.1 kB
"""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 ( # type: ignore[attr-defined]
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, # 0 = none, 1 = config, 2 = diagnostic
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
# Convert string state_class to int if needed (for backward compatibility)
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, # 0 = none, 1 = config, 2 = diagnostic
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, # 0 = none, 1 = config, 2 = diagnostic
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, # 0 = none, 1 = config, 2 = diagnostic
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}")
# Buttons don't have state responses
return
yield # Make this a generator