| """Entity registry for ESPHome entities. |
| |
| This module handles the registration and management of all ESPHome entities |
| for the Reachy Mini voice assistant. |
| """ |
|
|
| import logging |
| from typing import TYPE_CHECKING, Callable, Dict, List, Optional |
|
|
| from .entity import BinarySensorEntity, CameraEntity, NumberEntity, TextSensorEntity |
| from .entity_extensions import SensorEntity, SwitchEntity, SelectEntity, ButtonEntity |
|
|
| if TYPE_CHECKING: |
| from .reachy_controller import ReachyController |
| from .camera_server import MJPEGCameraServer |
|
|
| _LOGGER = logging.getLogger(__name__) |
|
|
|
|
| |
| |
| ENTITY_KEYS: Dict[str, int] = { |
| |
| "reachy_mini_media_player": 0, |
| |
| "daemon_state": 100, |
| "backend_ready": 101, |
| "speaker_volume": 103, |
| |
| "motors_enabled": 200, |
| "motor_mode": 201, |
| "wake_up": 202, |
| "go_to_sleep": 203, |
| |
| "head_x": 300, |
| "head_y": 301, |
| "head_z": 302, |
| "head_roll": 303, |
| "head_pitch": 304, |
| "head_yaw": 305, |
| "body_yaw": 306, |
| "antenna_left": 307, |
| "antenna_right": 308, |
| |
| "look_at_x": 400, |
| "look_at_y": 401, |
| "look_at_z": 402, |
| |
| "doa_angle": 500, |
| "speech_detected": 501, |
| |
| "control_loop_frequency": 600, |
| "sdk_version": 601, |
| "robot_name": 602, |
| "wireless_version": 603, |
| "simulation_mode": 604, |
| "wlan_ip": 605, |
| "error_message": 606, |
| |
| "imu_accel_x": 700, |
| "imu_accel_y": 701, |
| "imu_accel_z": 702, |
| "imu_gyro_x": 703, |
| "imu_gyro_y": 704, |
| "imu_gyro_z": 705, |
| "imu_temperature": 706, |
| |
| "emotion": 800, |
| |
| "microphone_volume": 900, |
| |
| "camera_url": 1000, |
| "camera": 1001, |
| |
| |
| |
| |
| |
| |
| |
| "agc_enabled": 1200, |
| "agc_max_gain": 1201, |
| "noise_suppression": 1202, |
| "echo_cancellation_converged": 1203, |
| |
| |
| |
| |
| "continuous_conversation": 1500, |
| |
| "gesture_detected": 1600, |
| "gesture_confidence": 1601, |
| } |
|
|
|
|
| def get_entity_key(object_id: str) -> int: |
| """Get a consistent entity key for the given object_id.""" |
| if object_id in ENTITY_KEYS: |
| return ENTITY_KEYS[object_id] |
| |
| _LOGGER.warning(f"Entity key not found for {object_id}, generating from hash") |
| return abs(hash(object_id)) % 10000 + 2000 |
|
|
|
|
| class EntityRegistry: |
| """Registry for managing ESPHome entities.""" |
|
|
| def __init__( |
| self, |
| server, |
| reachy_controller: "ReachyController", |
| camera_server: Optional["MJPEGCameraServer"] = None, |
| play_emotion_callback: Optional[Callable[[str], None]] = None, |
| ): |
| """Initialize the entity registry. |
| |
| Args: |
| server: The VoiceSatelliteProtocol server instance |
| reachy_controller: The ReachyController instance |
| camera_server: Optional camera server for camera entity |
| play_emotion_callback: Optional callback for playing emotions |
| """ |
| self.server = server |
| self.reachy_controller = reachy_controller |
| self.camera_server = camera_server |
| self._play_emotion_callback = play_emotion_callback |
| |
| |
| self._current_gesture = "none" |
| self._gesture_confidence = 0.0 |
|
|
| |
| self._current_emotion = "None" |
| |
| |
| self._emotion_map = { |
| "None": None, |
| |
| "Happy": "cheerful1", |
| "Sad": "sad1", |
| "Angry": "rage1", |
| "Fear": "fear1", |
| "Surprise": "surprised1", |
| "Disgust": "disgusted1", |
| |
| "Laughing": "laughing1", |
| "Loving": "loving1", |
| "Proud": "proud1", |
| "Grateful": "grateful1", |
| "Enthusiastic": "enthusiastic1", |
| "Curious": "curious1", |
| "Amazed": "amazed1", |
| "Shy": "shy1", |
| "Confused": "confused1", |
| "Thoughtful": "thoughtful1", |
| "Anxious": "anxiety1", |
| "Scared": "scared1", |
| "Frustrated": "frustrated1", |
| "Irritated": "irritated1", |
| "Furious": "furious1", |
| "Contempt": "contempt1", |
| "Bored": "boredom1", |
| "Tired": "tired1", |
| "Exhausted": "exhausted1", |
| "Lonely": "lonely1", |
| "Downcast": "downcast1", |
| "Resigned": "resigned1", |
| "Uncertain": "uncertain1", |
| "Uncomfortable": "uncomfortable1", |
| "Lost": "lost1", |
| "Indifferent": "indifferent1", |
| |
| "Yes": "yes1", |
| "No": "no1", |
| "Welcoming": "welcoming1", |
| "Helpful": "helpful1", |
| "Attentive": "attentive1", |
| "Understanding": "understanding1", |
| "Calming": "calming1", |
| "Relief": "relief1", |
| "Success": "success1", |
| "Serenity": "serenity1", |
| |
| "Oops": "oops1", |
| "Displeased": "displeased1", |
| "Impatient": "impatient1", |
| "Reprimand": "reprimand1", |
| "GoAway": "go_away1", |
| |
| "Come": "come1", |
| "Inquiring": "inquiring1", |
| "Sleep": "sleep1", |
| "Dance": "dance1", |
| "Electric": "electric1", |
| "Dying": "dying1", |
| } |
|
|
| def setup_all_entities(self, entities: List) -> None: |
| """Setup all entity phases. |
| |
| Args: |
| entities: The list to append entities to |
| """ |
| self._setup_phase1_entities(entities) |
| self._setup_phase2_entities(entities) |
| self._setup_phase3_entities(entities) |
| self._setup_phase4_entities(entities) |
| self._setup_phase5_entities(entities) |
| self._setup_phase6_entities(entities) |
| self._setup_phase7_entities(entities) |
| self._setup_phase8_entities(entities) |
| self._setup_phase9_entities(entities) |
| self._setup_phase10_entities(entities) |
| |
| self._setup_phase12_entities(entities) |
| |
| |
| |
| self._setup_phase21_entities(entities) |
| self._setup_phase22_entities(entities) |
|
|
| _LOGGER.info("All entities registered: %d total", len(entities)) |
|
|
| def _setup_phase1_entities(self, entities: List) -> None: |
| """Setup Phase 1 entities: Basic status and volume control.""" |
| rc = self.reachy_controller |
|
|
| entities.append(TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("daemon_state"), |
| name="Daemon State", |
| object_id="daemon_state", |
| icon="mdi:robot", |
| value_getter=rc.get_daemon_state, |
| )) |
|
|
| entities.append(BinarySensorEntity( |
| server=self.server, |
| key=get_entity_key("backend_ready"), |
| name="Backend Ready", |
| object_id="backend_ready", |
| icon="mdi:check-circle", |
| device_class="connectivity", |
| value_getter=rc.get_backend_ready, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("speaker_volume"), |
| name="Speaker Volume", |
| object_id="speaker_volume", |
| min_value=0.0, |
| max_value=100.0, |
| step=1.0, |
| icon="mdi:volume-high", |
| unit_of_measurement="%", |
| mode=2, |
| entity_category=1, |
| value_getter=rc.get_speaker_volume, |
| value_setter=rc.set_speaker_volume, |
| )) |
|
|
| _LOGGER.debug("Phase 1 entities registered: daemon_state, backend_ready, speaker_volume") |
|
|
| def _setup_phase2_entities(self, entities: List) -> None: |
| """Setup Phase 2 entities: Motor control.""" |
| rc = self.reachy_controller |
|
|
| entities.append(SwitchEntity( |
| server=self.server, |
| key=get_entity_key("motors_enabled"), |
| name="Motors Enabled", |
| object_id="motors_enabled", |
| icon="mdi:engine", |
| device_class="switch", |
| value_getter=rc.get_motors_enabled, |
| value_setter=rc.set_motors_enabled, |
| )) |
|
|
| entities.append(ButtonEntity( |
| server=self.server, |
| key=get_entity_key("wake_up"), |
| name="Wake Up", |
| object_id="wake_up", |
| icon="mdi:alarm", |
| device_class="restart", |
| on_press=rc.wake_up, |
| )) |
|
|
| entities.append(ButtonEntity( |
| server=self.server, |
| key=get_entity_key("go_to_sleep"), |
| name="Go to Sleep", |
| object_id="go_to_sleep", |
| icon="mdi:sleep", |
| device_class="restart", |
| on_press=rc.go_to_sleep, |
| )) |
|
|
| _LOGGER.debug("Phase 2 entities registered: motors_enabled, wake_up, go_to_sleep") |
|
|
| def _setup_phase3_entities(self, entities: List) -> None: |
| """Setup Phase 3 entities: Pose control.""" |
| rc = self.reachy_controller |
|
|
| |
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_x"), |
| name="Head X Position", |
| object_id="head_x", |
| min_value=-50.0, |
| max_value=50.0, |
| step=1.0, |
| icon="mdi:axis-x-arrow", |
| unit_of_measurement="mm", |
| mode=2, |
| value_getter=rc.get_head_x, |
| value_setter=rc.set_head_x, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_y"), |
| name="Head Y Position", |
| object_id="head_y", |
| min_value=-50.0, |
| max_value=50.0, |
| step=1.0, |
| icon="mdi:axis-y-arrow", |
| unit_of_measurement="mm", |
| mode=2, |
| value_getter=rc.get_head_y, |
| value_setter=rc.set_head_y, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_z"), |
| name="Head Z Position", |
| object_id="head_z", |
| min_value=-50.0, |
| max_value=50.0, |
| step=1.0, |
| icon="mdi:axis-z-arrow", |
| unit_of_measurement="mm", |
| mode=2, |
| value_getter=rc.get_head_z, |
| value_setter=rc.set_head_z, |
| )) |
|
|
| |
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_roll"), |
| name="Head Roll", |
| object_id="head_roll", |
| min_value=-40.0, |
| max_value=40.0, |
| step=1.0, |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_head_roll, |
| value_setter=rc.set_head_roll, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_pitch"), |
| name="Head Pitch", |
| object_id="head_pitch", |
| min_value=-40.0, |
| max_value=40.0, |
| step=1.0, |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_head_pitch, |
| value_setter=rc.set_head_pitch, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("head_yaw"), |
| name="Head Yaw", |
| object_id="head_yaw", |
| min_value=-180.0, |
| max_value=180.0, |
| step=1.0, |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_head_yaw, |
| value_setter=rc.set_head_yaw, |
| )) |
|
|
| |
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("body_yaw"), |
| name="Body Yaw", |
| object_id="body_yaw", |
| min_value=-160.0, |
| max_value=160.0, |
| step=1.0, |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_body_yaw, |
| value_setter=rc.set_body_yaw, |
| )) |
|
|
| |
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("antenna_left"), |
| name="Antenna(L)", |
| object_id="antenna_left", |
| min_value=-90.0, |
| max_value=90.0, |
| step=1.0, |
| icon="mdi:antenna", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_antenna_left, |
| value_setter=rc.set_antenna_left, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("antenna_right"), |
| name="Antenna(R)", |
| object_id="antenna_right", |
| min_value=-90.0, |
| max_value=90.0, |
| step=1.0, |
| icon="mdi:antenna", |
| unit_of_measurement="°", |
| mode=2, |
| value_getter=rc.get_antenna_right, |
| value_setter=rc.set_antenna_right, |
| )) |
|
|
| _LOGGER.debug("Phase 3 entities registered: head position/orientation, body_yaw, antennas") |
|
|
| def _setup_phase4_entities(self, entities: List) -> None: |
| """Setup Phase 4 entities: Look at control.""" |
| rc = self.reachy_controller |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("look_at_x"), |
| name="Look At X", |
| object_id="look_at_x", |
| min_value=-2.0, |
| max_value=2.0, |
| step=0.1, |
| icon="mdi:crosshairs-gps", |
| unit_of_measurement="m", |
| mode=1, |
| value_getter=rc.get_look_at_x, |
| value_setter=rc.set_look_at_x, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("look_at_y"), |
| name="Look At Y", |
| object_id="look_at_y", |
| min_value=-2.0, |
| max_value=2.0, |
| step=0.1, |
| icon="mdi:crosshairs-gps", |
| unit_of_measurement="m", |
| mode=1, |
| value_getter=rc.get_look_at_y, |
| value_setter=rc.set_look_at_y, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("look_at_z"), |
| name="Look At Z", |
| object_id="look_at_z", |
| min_value=-2.0, |
| max_value=2.0, |
| step=0.1, |
| icon="mdi:crosshairs-gps", |
| unit_of_measurement="m", |
| mode=1, |
| value_getter=rc.get_look_at_z, |
| value_setter=rc.set_look_at_z, |
| )) |
|
|
| _LOGGER.debug("Phase 4 entities registered: look_at_x/y/z") |
|
|
| def _setup_phase5_entities(self, entities: List) -> None: |
| """Setup Phase 5 entities: DOA (Direction of Arrival) for wakeup turn-to-sound.""" |
| rc = self.reachy_controller |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("doa_angle"), |
| name="DOA Angle", |
| object_id="doa_angle", |
| icon="mdi:surround-sound", |
| unit_of_measurement="°", |
| accuracy_decimals=1, |
| state_class="measurement", |
| value_getter=rc.get_doa_angle_degrees, |
| )) |
|
|
| entities.append(BinarySensorEntity( |
| server=self.server, |
| key=get_entity_key("speech_detected"), |
| name="Speech Detected", |
| object_id="speech_detected", |
| icon="mdi:account-voice", |
| device_class="sound", |
| value_getter=rc.get_speech_detected, |
| )) |
|
|
| _LOGGER.debug("Phase 5 entities registered: doa_angle, speech_detected") |
|
|
| def _setup_phase6_entities(self, entities: List) -> None: |
| """Setup Phase 6 entities: Diagnostic information.""" |
| rc = self.reachy_controller |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("control_loop_frequency"), |
| name="Control Loop Frequency", |
| object_id="control_loop_frequency", |
| icon="mdi:speedometer", |
| unit_of_measurement="Hz", |
| accuracy_decimals=1, |
| state_class="measurement", |
| entity_category=2, |
| value_getter=rc.get_control_loop_frequency, |
| )) |
|
|
| entities.append(TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("sdk_version"), |
| name="SDK Version", |
| object_id="sdk_version", |
| icon="mdi:information", |
| entity_category=2, |
| value_getter=rc.get_sdk_version, |
| )) |
|
|
| entities.append(TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("robot_name"), |
| name="Robot Name", |
| object_id="robot_name", |
| icon="mdi:robot", |
| entity_category=2, |
| value_getter=rc.get_robot_name, |
| )) |
|
|
| entities.append(BinarySensorEntity( |
| server=self.server, |
| key=get_entity_key("wireless_version"), |
| name="Wireless Version", |
| object_id="wireless_version", |
| icon="mdi:wifi", |
| device_class="connectivity", |
| entity_category=2, |
| value_getter=rc.get_wireless_version, |
| )) |
|
|
| entities.append(BinarySensorEntity( |
| server=self.server, |
| key=get_entity_key("simulation_mode"), |
| name="Simulation Mode", |
| object_id="simulation_mode", |
| icon="mdi:virtual-reality", |
| entity_category=2, |
| value_getter=rc.get_simulation_mode, |
| )) |
|
|
| entities.append(TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("wlan_ip"), |
| name="WLAN IP", |
| object_id="wlan_ip", |
| icon="mdi:ip-network", |
| entity_category=2, |
| value_getter=rc.get_wlan_ip, |
| )) |
|
|
| entities.append(TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("error_message"), |
| name="Error Message", |
| object_id="error_message", |
| icon="mdi:alert-circle", |
| entity_category=2, |
| value_getter=rc.get_error_message, |
| )) |
|
|
| _LOGGER.debug( |
| "Phase 6 entities registered: control_loop_frequency, sdk_version, " |
| "robot_name, wireless_version, simulation_mode, wlan_ip, error_message" |
| ) |
|
|
| def _setup_phase7_entities(self, entities: List) -> None: |
| """Setup Phase 7 entities: IMU sensors (wireless only).""" |
| rc = self.reachy_controller |
|
|
| |
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_accel_x"), |
| name="IMU Accel X", |
| object_id="imu_accel_x", |
| icon="mdi:axis-x-arrow", |
| unit_of_measurement="m/s²", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_accel_x, |
| )) |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_accel_y"), |
| name="IMU Accel Y", |
| object_id="imu_accel_y", |
| icon="mdi:axis-y-arrow", |
| unit_of_measurement="m/s²", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_accel_y, |
| )) |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_accel_z"), |
| name="IMU Accel Z", |
| object_id="imu_accel_z", |
| icon="mdi:axis-z-arrow", |
| unit_of_measurement="m/s²", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_accel_z, |
| )) |
|
|
| |
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_gyro_x"), |
| name="IMU Gyro X", |
| object_id="imu_gyro_x", |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="rad/s", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_gyro_x, |
| )) |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_gyro_y"), |
| name="IMU Gyro Y", |
| object_id="imu_gyro_y", |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="rad/s", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_gyro_y, |
| )) |
|
|
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_gyro_z"), |
| name="IMU Gyro Z", |
| object_id="imu_gyro_z", |
| icon="mdi:rotate-3d-variant", |
| unit_of_measurement="rad/s", |
| accuracy_decimals=3, |
| state_class="measurement", |
| value_getter=rc.get_imu_gyro_z, |
| )) |
|
|
| |
| entities.append(SensorEntity( |
| server=self.server, |
| key=get_entity_key("imu_temperature"), |
| name="IMU Temperature", |
| object_id="imu_temperature", |
| icon="mdi:thermometer", |
| unit_of_measurement="°C", |
| accuracy_decimals=1, |
| device_class="temperature", |
| state_class="measurement", |
| value_getter=rc.get_imu_temperature, |
| )) |
|
|
| _LOGGER.debug("Phase 7 entities registered: IMU accelerometer, gyroscope, temperature") |
|
|
| def _setup_phase8_entities(self, entities: List) -> None: |
| """Setup Phase 8 entities: Emotion selector.""" |
|
|
| def get_emotion() -> str: |
| return self._current_emotion |
|
|
| def set_emotion(emotion: str) -> None: |
| self._current_emotion = emotion |
| emotion_name = self._emotion_map.get(emotion) |
| if emotion_name and self._play_emotion_callback: |
| self._play_emotion_callback(emotion_name) |
| |
| self._current_emotion = "None" |
|
|
| entities.append(SelectEntity( |
| server=self.server, |
| key=get_entity_key("emotion"), |
| name="Emotion", |
| object_id="emotion", |
| options=list(self._emotion_map.keys()), |
| icon="mdi:emoticon", |
| value_getter=get_emotion, |
| value_setter=set_emotion, |
| )) |
|
|
| _LOGGER.debug("Phase 8 entities registered: emotion selector") |
|
|
| def _setup_phase9_entities(self, entities: List) -> None: |
| """Setup Phase 9 entities: Audio controls.""" |
| rc = self.reachy_controller |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("microphone_volume"), |
| name="Microphone Volume", |
| object_id="microphone_volume", |
| min_value=0.0, |
| max_value=100.0, |
| step=1.0, |
| icon="mdi:microphone", |
| unit_of_measurement="%", |
| mode=2, |
| entity_category=1, |
| value_getter=rc.get_microphone_volume, |
| value_setter=rc.set_microphone_volume, |
| )) |
|
|
| _LOGGER.debug("Phase 9 entities registered: microphone_volume") |
|
|
| def _setup_phase10_entities(self, entities: List) -> None: |
| """Setup Phase 10 entities: Camera for Home Assistant integration.""" |
|
|
| def get_camera_image() -> Optional[bytes]: |
| """Get camera snapshot as JPEG bytes.""" |
| if self.camera_server: |
| return self.camera_server.get_snapshot() |
| return None |
|
|
| entities.append(CameraEntity( |
| server=self.server, |
| key=get_entity_key("camera"), |
| name="Camera", |
| object_id="camera", |
| icon="mdi:camera", |
| image_getter=get_camera_image, |
| )) |
|
|
| _LOGGER.debug("Phase 10 entities registered: camera (ESPHome Camera entity)") |
|
|
| def _setup_phase12_entities(self, entities: List) -> None: |
| """Setup Phase 12 entities: Audio processing parameters (via local SDK).""" |
| rc = self.reachy_controller |
|
|
| def set_agc_enabled_with_save(enabled: bool) -> None: |
| """Set AGC enabled and save to preferences.""" |
| rc.set_agc_enabled(enabled) |
| if hasattr(self.server, 'state') and self.server.state: |
| self.server.state.preferences.agc_enabled = enabled |
| self.server.state.save_preferences() |
| _LOGGER.debug("AGC enabled saved to preferences: %s", enabled) |
|
|
| def set_agc_max_gain_with_save(gain: float) -> None: |
| """Set AGC max gain and save to preferences.""" |
| rc.set_agc_max_gain(gain) |
| if hasattr(self.server, 'state') and self.server.state: |
| self.server.state.preferences.agc_max_gain = gain |
| self.server.state.save_preferences() |
| _LOGGER.debug("AGC max gain saved to preferences: %.1f dB", gain) |
|
|
| def set_noise_suppression_with_save(level: float) -> None: |
| """Set noise suppression and save to preferences.""" |
| rc.set_noise_suppression(level) |
| if hasattr(self.server, 'state') and self.server.state: |
| self.server.state.preferences.noise_suppression = level |
| self.server.state.save_preferences() |
| _LOGGER.debug("Noise suppression saved to preferences: %.1f%%", level) |
|
|
| entities.append(SwitchEntity( |
| server=self.server, |
| key=get_entity_key("agc_enabled"), |
| name="AGC Enabled", |
| object_id="agc_enabled", |
| icon="mdi:tune-vertical", |
| device_class="switch", |
| entity_category=1, |
| value_getter=rc.get_agc_enabled, |
| value_setter=set_agc_enabled_with_save, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("agc_max_gain"), |
| name="AGC Max Gain", |
| object_id="agc_max_gain", |
| min_value=0.0, |
| max_value=40.0, |
| step=1.0, |
| icon="mdi:volume-plus", |
| unit_of_measurement="dB", |
| mode=2, |
| entity_category=1, |
| value_getter=rc.get_agc_max_gain, |
| value_setter=set_agc_max_gain_with_save, |
| )) |
|
|
| entities.append(NumberEntity( |
| server=self.server, |
| key=get_entity_key("noise_suppression"), |
| name="Noise Suppression", |
| object_id="noise_suppression", |
| min_value=0.0, |
| max_value=100.0, |
| step=1.0, |
| icon="mdi:volume-off", |
| unit_of_measurement="%", |
| mode=2, |
| entity_category=1, |
| value_getter=rc.get_noise_suppression, |
| value_setter=set_noise_suppression_with_save, |
| )) |
|
|
| entities.append(BinarySensorEntity( |
| server=self.server, |
| key=get_entity_key("echo_cancellation_converged"), |
| name="Echo Cancellation Converged", |
| object_id="echo_cancellation_converged", |
| icon="mdi:waveform", |
| device_class="running", |
| entity_category=2, |
| value_getter=rc.get_echo_cancellation_converged, |
| )) |
|
|
| _LOGGER.debug( |
| "Phase 12 entities registered: agc_enabled, agc_max_gain, " |
| "noise_suppression, echo_cancellation_converged" |
| ) |
|
|
| def _setup_phase21_entities(self, entities: List) -> None: |
| """Setup Phase 21 entities: Continuous conversation mode.""" |
|
|
| def get_continuous_conversation() -> bool: |
| """Get current continuous conversation mode state.""" |
| if hasattr(self.server, 'state') and self.server.state: |
| prefs = self.server.state.preferences |
| return getattr(prefs, 'continuous_conversation', False) |
| return False |
|
|
| def set_continuous_conversation(enabled: bool) -> None: |
| """Set continuous conversation mode and save to preferences.""" |
| if hasattr(self.server, 'state') and self.server.state: |
| self.server.state.preferences.continuous_conversation = enabled |
| self.server.state.save_preferences() |
| _LOGGER.info("Continuous conversation mode %s", "enabled" if enabled else "disabled") |
|
|
| entities.append(SwitchEntity( |
| server=self.server, |
| key=get_entity_key("continuous_conversation"), |
| name="Continuous Conversation", |
| object_id="continuous_conversation", |
| icon="mdi:message-reply-text", |
| device_class="switch", |
| entity_category=1, |
| value_getter=get_continuous_conversation, |
| value_setter=set_continuous_conversation, |
| )) |
|
|
| _LOGGER.debug("Phase 21 entities registered: continuous_conversation") |
|
|
| def _setup_phase22_entities(self, entities: List) -> None: |
| """Setup Phase 22 entities: Gesture detection.""" |
|
|
| def get_gesture() -> str: |
| """Get current detected gesture.""" |
| if self.camera_server: |
| return self.camera_server.get_current_gesture() |
| return "none" |
|
|
| def get_gesture_confidence() -> float: |
| """Get gesture detection confidence.""" |
| if self.camera_server: |
| return self.camera_server.get_gesture_confidence() |
| return 0.0 |
|
|
| gesture_entity = TextSensorEntity( |
| server=self.server, |
| key=get_entity_key("gesture_detected"), |
| name="Gesture Detected", |
| object_id="gesture_detected", |
| icon="mdi:hand-wave", |
| value_getter=get_gesture, |
| ) |
| entities.append(gesture_entity) |
| self._gesture_entity = gesture_entity |
|
|
| confidence_entity = SensorEntity( |
| server=self.server, |
| key=get_entity_key("gesture_confidence"), |
| name="Gesture Confidence", |
| object_id="gesture_confidence", |
| icon="mdi:percent", |
| unit_of_measurement="%", |
| accuracy_decimals=1, |
| state_class="measurement", |
| value_getter=get_gesture_confidence, |
| ) |
| entities.append(confidence_entity) |
| self._gesture_confidence_entity = confidence_entity |
|
|
| _LOGGER.debug("Phase 22 entities registered: gesture_detected, gesture_confidence") |
|
|
| def update_gesture_state(self) -> None: |
| """Push gesture state update to Home Assistant.""" |
| if hasattr(self, '_gesture_entity') and self._gesture_entity: |
| self._gesture_entity.update_state() |
| if hasattr(self, '_gesture_confidence_entity') and self._gesture_confidence_entity: |
| self._gesture_confidence_entity.update_state() |
|
|
| def find_entity_references(self, entities: List) -> None: |
| """Find and store references to special entities from existing list. |
| |
| Args: |
| entities: The list of existing entities to search |
| """ |
| |
| pass |
|
|