"""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__) # Fixed entity key mapping - ensures consistent keys across restarts # Keys are based on object_id hash to ensure uniqueness and consistency ENTITY_KEYS: Dict[str, int] = { # Media player (key 0 reserved) "reachy_mini_media_player": 0, # Phase 1: Basic status and volume "daemon_state": 100, "backend_ready": 101, "speaker_volume": 103, # Phase 2: Motor control "motors_enabled": 200, "motor_mode": 201, "wake_up": 202, "go_to_sleep": 203, # Phase 3: Pose control "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, # Phase 4: Look at control "look_at_x": 400, "look_at_y": 401, "look_at_z": 402, # Phase 5: DOA (Direction of Arrival) - re-added for wakeup turn-to-sound "doa_angle": 500, "speech_detected": 501, # Phase 6: Diagnostic information "control_loop_frequency": 600, "sdk_version": 601, "robot_name": 602, "wireless_version": 603, "simulation_mode": 604, "wlan_ip": 605, "error_message": 606, # Moved to diagnostic # Phase 7: IMU sensors "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, # Phase 8: Emotion selector "emotion": 800, # Phase 9: Audio controls "microphone_volume": 900, # Phase 10: Camera "camera_url": 1000, # Keep for backward compatibility "camera": 1001, # New camera entity # Phase 11: LED control (disabled - not visible) # "led_brightness": 1100, # "led_effect": 1101, # "led_color_r": 1102, # "led_color_g": 1103, # "led_color_b": 1104, # Phase 12: Audio processing "agc_enabled": 1200, "agc_max_gain": 1201, "noise_suppression": 1202, "echo_cancellation_converged": 1203, # Phase 13: Sendspin - auto-enabled via mDNS, no user entities needed # Phase 20: Tap detection (disabled - too many false triggers) # "tap_sensitivity": 1400, # Phase 21: Continuous conversation "continuous_conversation": 1500, # Phase 22: Gesture detection "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] # Fallback: generate key from hash (should not happen if all entities are registered) _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 # Gesture detection state self._current_gesture = "none" self._gesture_confidence = 0.0 # Emotion state self._current_emotion = "None" # Map emotion names to available robot emotions # Full list of available emotions from robot self._emotion_map = { "None": None, # Basic emotions "Happy": "cheerful1", "Sad": "sad1", "Angry": "rage1", "Fear": "fear1", "Surprise": "surprised1", "Disgust": "disgusted1", # Extended emotions "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", # Positive actions "Yes": "yes1", "No": "no1", "Welcoming": "welcoming1", "Helpful": "helpful1", "Attentive": "attentive1", "Understanding": "understanding1", "Calming": "calming1", "Relief": "relief1", "Success": "success1", "Serenity": "serenity1", # Negative actions "Oops": "oops1", "Displeased": "displeased1", "Impatient": "impatient1", "Reprimand": "reprimand1", "GoAway": "go_away1", # Special "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) # DOA for wakeup turn-to-sound 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) # Phase 11 (LED control) disabled - LEDs are inside the robot and not visible self._setup_phase12_entities(entities) # Phase 13 (Sendspin) - auto-enabled via mDNS discovery, no user entities # Phase 14 (head_joints, passive_joints) removed - not needed # Phase 20 (Tap detection) disabled - too many false triggers 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, # Slider mode entity_category=1, # config 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 # Head position controls (X, Y, Z in mm) 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, )) # Head orientation controls (Roll, Pitch, Yaw in degrees) 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, )) # Body yaw control 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, )) # Antenna controls 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, # Box mode for precise input 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, # diagnostic 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, # diagnostic 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, # diagnostic 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, # diagnostic 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, # diagnostic 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, # diagnostic 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, # diagnostic 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 # IMU Accelerometer 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, )) # IMU Gyroscope 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, )) # IMU Temperature 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) # Reset to None after playing 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, # Slider mode entity_category=1, # config 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, # config 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, # XVF3800 supports up to 40dB step=1.0, icon="mdi:volume-plus", unit_of_measurement="dB", mode=2, entity_category=1, # config 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, # config 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, # diagnostic 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, # config 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 """ # DOA entities are read-only sensors, no special references needed pass