Desmond-Dong commited on
Commit
6fd277f
·
1 Parent(s): ec4e118

"feat-replace-camera-status-with-camera-entity"

Browse files
reachy_mini_ha_voice/entity.py CHANGED
@@ -9,6 +9,7 @@ import logging
9
  from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
10
  ListEntitiesBinarySensorResponse,
11
  ListEntitiesButtonResponse,
 
12
  ListEntitiesMediaPlayerResponse,
13
  ListEntitiesNumberResponse,
14
  ListEntitiesRequest,
@@ -18,6 +19,8 @@ from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
18
  ListEntitiesTextSensorResponse,
19
  BinarySensorStateResponse,
20
  ButtonCommandRequest,
 
 
21
  MediaPlayerCommandRequest,
22
  MediaPlayerStateResponse,
23
  NumberCommandRequest,
@@ -343,3 +346,55 @@ class NumberEntity(ESPHomeEntity):
343
  def update_state(self) -> None:
344
  """Send state update to Home Assistant."""
345
  self.server.send_messages([self._get_state_message()])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
10
  ListEntitiesBinarySensorResponse,
11
  ListEntitiesButtonResponse,
12
+ ListEntitiesCameraResponse,
13
  ListEntitiesMediaPlayerResponse,
14
  ListEntitiesNumberResponse,
15
  ListEntitiesRequest,
 
19
  ListEntitiesTextSensorResponse,
20
  BinarySensorStateResponse,
21
  ButtonCommandRequest,
22
+ CameraImageRequest,
23
+ CameraImageResponse,
24
  MediaPlayerCommandRequest,
25
  MediaPlayerStateResponse,
26
  NumberCommandRequest,
 
346
  def update_state(self) -> None:
347
  """Send state update to Home Assistant."""
348
  self.server.send_messages([self._get_state_message()])
349
+
350
+
351
+ class CameraEntity(ESPHomeEntity):
352
+ """Camera entity for ESPHome (provides image snapshots)."""
353
+
354
+ def __init__(
355
+ self,
356
+ server: APIServer,
357
+ key: int,
358
+ name: str,
359
+ object_id: str,
360
+ icon: str = "mdi:camera",
361
+ image_getter: Optional[Callable[[], Optional[bytes]]] = None,
362
+ ) -> None:
363
+ ESPHomeEntity.__init__(self, server)
364
+ self.key = key
365
+ self.name = name
366
+ self.object_id = object_id
367
+ self.icon = icon
368
+ self._image_getter = image_getter
369
+
370
+ def get_image(self) -> Optional[bytes]:
371
+ """Get the current camera image as JPEG bytes."""
372
+ if self._image_getter:
373
+ return self._image_getter()
374
+ return None
375
+
376
+ def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
377
+ if isinstance(msg, ListEntitiesRequest):
378
+ yield ListEntitiesCameraResponse(
379
+ object_id=self.object_id,
380
+ key=self.key,
381
+ name=self.name,
382
+ icon=self.icon,
383
+ )
384
+ elif isinstance(msg, CameraImageRequest) and msg.key == self.key:
385
+ # Return camera image
386
+ image_data = self.get_image()
387
+ if image_data:
388
+ yield CameraImageResponse(
389
+ key=self.key,
390
+ data=image_data,
391
+ done=True,
392
+ )
393
+ else:
394
+ # Return empty response if no image available
395
+ yield CameraImageResponse(
396
+ key=self.key,
397
+ data=b"",
398
+ done=True,
399
+ )
400
+
reachy_mini_ha_voice/reachy_controller.py CHANGED
@@ -756,45 +756,6 @@ class ReachyController:
756
  logger.error(f"Error getting IMU temperature: {e}")
757
  return 0.0
758
 
759
- # ========== Phase 10: Camera Status ==========
760
-
761
- def get_camera_streaming(self) -> bool:
762
- """Check if camera is streaming."""
763
- if not self.is_available:
764
- return False
765
- try:
766
- # Check if media manager has camera initialized
767
- if self.reachy.media and self.reachy.media.camera:
768
- return True
769
- return False
770
- except Exception as e:
771
- logger.debug(f"Error checking camera streaming: {e}")
772
- return False
773
-
774
- def get_camera_fps(self) -> float:
775
- """Get camera FPS."""
776
- if not self.is_available:
777
- return 0.0
778
- try:
779
- if self.reachy.media and self.reachy.media.camera:
780
- return float(self.reachy.media.camera.framerate)
781
- return 0.0
782
- except Exception as e:
783
- logger.debug(f"Error getting camera FPS: {e}")
784
- return 0.0
785
-
786
- def get_camera_url(self) -> str:
787
- """Get camera stream URL."""
788
- if not self.is_available:
789
- return "N/A"
790
- try:
791
- status = self.reachy.client.get_status(wait=False)
792
- wlan_ip = status.get('wlan_ip', 'localhost')
793
- return f"http://{wlan_ip}:8081/stream"
794
- except Exception as e:
795
- logger.debug(f"Error getting camera URL: {e}")
796
- return "N/A"
797
-
798
  # ========== Phase 11: LED Control (via local SDK) ==========
799
 
800
  def _get_respeaker(self):
 
756
  logger.error(f"Error getting IMU temperature: {e}")
757
  return 0.0
758
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  # ========== Phase 11: LED Control (via local SDK) ==========
760
 
761
  def _get_respeaker(self):
reachy_mini_ha_voice/satellite.py CHANGED
@@ -48,7 +48,7 @@ from pymicro_wakeword import MicroWakeWord
48
  from pyopen_wakeword import OpenWakeWord
49
 
50
  from .api_server import APIServer
51
- from .entity import BinarySensorEntity, MediaPlayerEntity, NumberEntity, TextSensorEntity
52
  from .entity_extensions import SensorEntity, SwitchEntity, SelectEntity, ButtonEntity
53
  from .models import AvailableWakeWord, ServerState, WakeWordType
54
  from .util import call_all
@@ -116,10 +116,8 @@ class VoiceSatelliteProtocol(APIServer):
116
  "emotion_disgust": 805,
117
  # Phase 9: Audio controls
118
  "microphone_volume": 900,
119
- # Phase 10: Camera status
120
- "camera_streaming": 1000,
121
- "camera_fps": 1001,
122
- "camera_url": 1002,
123
  # Phase 11: LED control
124
  "led_brightness": 1100,
125
  "led_effect": 1101,
@@ -1269,46 +1267,28 @@ class VoiceSatelliteProtocol(APIServer):
1269
  _LOGGER.info("Phase 9 entities registered: microphone_volume")
1270
 
1271
  def _setup_phase10_entities(self) -> None:
1272
- """Setup Phase 10 entities: Camera status."""
1273
-
1274
- # Camera streaming status
1275
- camera_streaming = BinarySensorEntity(
 
 
 
 
 
 
 
 
1276
  server=self,
1277
- key=self._get_entity_key("camera_streaming"),
1278
- name="Camera Streaming",
1279
- object_id="camera_streaming",
1280
  icon="mdi:camera",
1281
- device_class="running",
1282
- value_getter=self.reachy_controller.get_camera_streaming,
1283
- )
1284
- self.state.entities.append(camera_streaming)
1285
-
1286
- # Camera FPS
1287
- camera_fps = SensorEntity(
1288
- server=self,
1289
- key=self._get_entity_key("camera_fps"),
1290
- name="Camera FPS",
1291
- object_id="camera_fps",
1292
- icon="mdi:camera-timer",
1293
- unit_of_measurement="fps",
1294
- accuracy_decimals=0,
1295
- state_class="measurement",
1296
- value_getter=self.reachy_controller.get_camera_fps,
1297
- )
1298
- self.state.entities.append(camera_fps)
1299
-
1300
- # Camera URL
1301
- camera_url = TextSensorEntity(
1302
- server=self,
1303
- key=self._get_entity_key("camera_url"),
1304
- name="Camera URL",
1305
- object_id="camera_url",
1306
- icon="mdi:link",
1307
- value_getter=self.reachy_controller.get_camera_url,
1308
  )
1309
- self.state.entities.append(camera_url)
1310
 
1311
- _LOGGER.info("Phase 10 entities registered: camera_streaming, camera_fps, camera_url")
1312
 
1313
  def _setup_phase11_entities(self) -> None:
1314
  """Setup Phase 11 entities: LED control (via local SDK)."""
 
48
  from pyopen_wakeword import OpenWakeWord
49
 
50
  from .api_server import APIServer
51
+ from .entity import BinarySensorEntity, CameraEntity, MediaPlayerEntity, NumberEntity, TextSensorEntity
52
  from .entity_extensions import SensorEntity, SwitchEntity, SelectEntity, ButtonEntity
53
  from .models import AvailableWakeWord, ServerState, WakeWordType
54
  from .util import call_all
 
116
  "emotion_disgust": 805,
117
  # Phase 9: Audio controls
118
  "microphone_volume": 900,
119
+ # Phase 10: Camera
120
+ "camera": 1000,
 
 
121
  # Phase 11: LED control
122
  "led_brightness": 1100,
123
  "led_effect": 1101,
 
1267
  _LOGGER.info("Phase 9 entities registered: microphone_volume")
1268
 
1269
  def _setup_phase10_entities(self) -> None:
1270
+ """Setup Phase 10 entities: Camera."""
1271
+
1272
+ # Camera entity - provides actual camera image in Home Assistant
1273
+ def get_camera_image() -> bytes:
1274
+ """Get camera snapshot from camera server."""
1275
+ if self.camera_server:
1276
+ image = self.camera_server.get_snapshot()
1277
+ if image:
1278
+ return image
1279
+ return b""
1280
+
1281
+ camera = CameraEntity(
1282
  server=self,
1283
+ key=self._get_entity_key("camera"),
1284
+ name="Camera",
1285
+ object_id="camera",
1286
  icon="mdi:camera",
1287
+ image_getter=get_camera_image,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  )
1289
+ self.state.entities.append(camera)
1290
 
1291
+ _LOGGER.info("Phase 10 entities registered: camera")
1292
 
1293
  def _setup_phase11_entities(self) -> None:
1294
  """Setup Phase 11 entities: LED control (via local SDK)."""