Skip to content

Commit 20fe787

Browse files
authored
Merge pull request #59 from MTrab/fix/revert-navmap-camera-merge
Revert navmap camera merge until validated
2 parents 0f23794 + 7fa813e commit 20fe787

7 files changed

Lines changed: 4 additions & 196 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ After setup, Home Assistant can expose data from Vector, including:
1717
- Eye color preset control (disabled by default)
1818
- Quick actions as buttons (sleep, go home, explore, listen for a beat, fetch cube)
1919
- Vision camera entity (disabled by default)
20-
- Nav map camera entity (disabled by default)
2120
- Home Assistant actions/services:
2221
- `vector.say_text`
2322
- `vector.set_eye_color`
@@ -78,7 +77,7 @@ To enable them:
7877

7978
1. Open your Vector device in Home Assistant.
8079
2. Open the entity list.
81-
3. Enable entities like `Vision`, `Nav map`, `Volume`, `Stimulation`, and diagnostic sensors as needed.
80+
3. Enable entities like `Vision`, `Volume`, `Stimulation`, and diagnostic sensors as needed.
8281

8382
## Actions for Automations
8483

custom_components/vector/camera.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ async def async_setup_entry(
1919
) -> None:
2020
"""Set up Vector camera entities from config entry."""
2121
coordinator: VectorCoordinator = entry.runtime_data["coordinator"]
22-
async_add_entities(
23-
[
24-
VectorVisionCamera(coordinator, entry),
25-
VectorNavMapCamera(coordinator, entry),
26-
]
27-
)
22+
async_add_entities([VectorVisionCamera(coordinator, entry)])
2823

2924

3025
class VectorVisionCamera(VectorEntity, Camera):
@@ -63,38 +58,3 @@ async def async_camera_image(
6358
return frame
6459

6560
return self._assets.image_bytes(VectorAsset.IMG_UNKNOWN)
66-
67-
68-
class VectorNavMapCamera(VectorEntity, Camera):
69-
"""Vector nav map camera entity using NavMapFeed stream."""
70-
71-
_attr_has_entity_name = True
72-
_attr_translation_key = "nav_map"
73-
_attr_entity_registry_enabled_default = False
74-
_attr_frame_interval = 0.5
75-
76-
def __init__(self, coordinator: VectorCoordinator, entry: ConfigEntry) -> None:
77-
"""Initialize Vector nav map camera entity."""
78-
Camera.__init__(self)
79-
VectorEntity.__init__(self, coordinator, entry)
80-
self._attr_unique_id = f"{entry.entry_id}_nav_map"
81-
self._assets = VectorAssetHandler()
82-
83-
async def async_added_to_hass(self) -> None:
84-
"""Prepare bundled fallback assets."""
85-
await super().async_added_to_hass()
86-
await self._assets.async_prepare(self.hass)
87-
88-
async def async_camera_image(
89-
self,
90-
width: int | None = None,
91-
height: int | None = None,
92-
) -> bytes | None:
93-
"""Return latest nav map PNG frame bytes."""
94-
del width, height
95-
96-
frame = await self.coordinator.async_get_latest_nav_map_frame(wait_timeout=1.0)
97-
if frame is not None:
98-
return frame
99-
100-
return self._assets.image_bytes(VectorAsset.IMG_UNKNOWN)

custom_components/vector/coordinator.py

Lines changed: 0 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
_INITIAL_REFRESH_MAX_RETRY_DELAY_SECONDS = 60.0
3636
_CAMERA_STREAM_READ_TIMEOUT_SECONDS = 30.0
3737
_CAMERA_RECONNECT_DELAY_SECONDS = 2.0
38-
_NAV_MAP_WAIT_TIMEOUT_SECONDS = 1.0
39-
_NAV_MAP_RECONNECT_DELAY_SECONDS = 2.0
40-
_NAV_MAP_FEED_FREQUENCY_HZ = 2.0
4138
_AUTH_BACKOFF_BASE_DELAY_SECONDS = 15.0
4239
_AUTH_BACKOFF_MAX_DELAY_SECONDS = 300.0
4340
_APP_INTENT_RPC_PATH = "/Anki.Vector.external_interface.ExternalInterface/AppIntent"
@@ -97,25 +94,19 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
9794
self.lift_height_mm: float | None = None
9895
self.camera_frame: bytes | None = None
9996
self.camera_frame_updated_monotonic: float | None = None
100-
self.nav_map_frame: bytes | None = None
101-
self.nav_map_frame_updated_monotonic: float | None = None
10297
self._client: Any | None = None
10398
self._robot_config: Any | None = None
10499
self._pyddlvector: Any | None = None
105100
self._messaging: Any | None = None
106-
self._latest_robot_state: Any | None = None
107101
self._activity_tracker: Any | None = None
108102
self._telemetry_filter: Any | None = None
109103
self._event_listener_task: asyncio.Task[None] | None = None
110104
self._camera_stream_task: asyncio.Task[None] | None = None
111-
self._nav_map_stream_task: asyncio.Task[None] | None = None
112105
self._wake_enable_stream_task: asyncio.Task[None] | None = None
113106
self._wake_camera_restart_task: asyncio.Task[None] | None = None
114107
self._camera_stream_lock = asyncio.Lock()
115-
self._nav_map_stream_lock = asyncio.Lock()
116108
self._image_stream_enable_lock = asyncio.Lock()
117109
self._camera_frame_event = asyncio.Event()
118-
self._nav_map_frame_event = asyncio.Event()
119110
self._settings_lock = asyncio.Lock()
120111
self._auth_backoff_delay_seconds = _AUTH_BACKOFF_BASE_DELAY_SECONDS
121112
self._auth_backoff_lock = asyncio.Lock()
@@ -266,15 +257,6 @@ async def async_shutdown(self) -> None:
266257
finally:
267258
self._camera_stream_task = None
268259

269-
if self._nav_map_stream_task is not None:
270-
self._nav_map_stream_task.cancel()
271-
try:
272-
await self._nav_map_stream_task
273-
except asyncio.CancelledError:
274-
pass
275-
finally:
276-
self._nav_map_stream_task = None
277-
278260
if self._wake_enable_stream_task is not None:
279261
self._wake_enable_stream_task.cancel()
280262
try:
@@ -403,7 +385,6 @@ async def _async_event_listener_loop(self) -> None:
403385
has_changes = False
404386
if event_type == "robot_state":
405387
robot_state = event.robot_state
406-
self._latest_robot_state = robot_state
407388
previous_activity = self.current_activity
408389
if self._activity_tracker is not None:
409390
next_activity = _normalize_activity_state(
@@ -821,95 +802,6 @@ async def async_get_latest_camera_frame(
821802

822803
return self.camera_frame
823804

824-
async def async_start_nav_map_stream(self) -> None:
825-
"""Ensure persistent nav map stream task is running."""
826-
async with self._nav_map_stream_lock:
827-
if (
828-
self._nav_map_stream_task is not None
829-
and not self._nav_map_stream_task.done()
830-
):
831-
return
832-
self._nav_map_stream_task = self.hass.async_create_background_task(
833-
self._async_nav_map_stream_loop(),
834-
name=f"vector_nav_map_stream_{self.entry.entry_id}",
835-
)
836-
837-
async def async_get_latest_nav_map_frame(
838-
self,
839-
*,
840-
wait_timeout: float = _NAV_MAP_WAIT_TIMEOUT_SECONDS,
841-
) -> bytes | None:
842-
"""Return latest nav map PNG frame, optionally waiting for first frame."""
843-
await self.async_start_nav_map_stream()
844-
845-
if self.nav_map_frame is not None:
846-
return self.nav_map_frame
847-
848-
try:
849-
await asyncio.wait_for(
850-
self._nav_map_frame_event.wait(), timeout=wait_timeout
851-
)
852-
except TimeoutError:
853-
return None
854-
855-
return self.nav_map_frame
856-
857-
def _nav_map_robot_pose_provider(self) -> Any | None:
858-
"""Return current robot pose in nav-map coordinates when available."""
859-
if self._pyddlvector is None:
860-
return None
861-
if not hasattr(self._pyddlvector, "nav_map_robot_pose_from_state"):
862-
return None
863-
robot_state = self._latest_robot_state
864-
if robot_state is None:
865-
return None
866-
return self._pyddlvector.nav_map_robot_pose_from_state(robot_state)
867-
868-
async def _async_nav_map_stream_loop(self) -> None:
869-
"""Keep nav-map feed stream and cache latest rendered PNG frame."""
870-
while True:
871-
try:
872-
client, _ = await self._async_get_client()
873-
pyddlvector, _ = await self._async_get_modules()
874-
if not hasattr(pyddlvector, "iter_nav_map_frames"):
875-
_LOGGER.debug(
876-
"pyddlvector does not provide iter_nav_map_frames; nav map camera disabled"
877-
)
878-
return
879-
880-
async for frame in pyddlvector.iter_nav_map_frames(
881-
client,
882-
frequency=_NAV_MAP_FEED_FREQUENCY_HZ,
883-
read_timeout=_CAMERA_STREAM_READ_TIMEOUT_SECONDS,
884-
reconnect_delay=_NAV_MAP_RECONNECT_DELAY_SECONDS,
885-
robot_pose_provider=self._nav_map_robot_pose_provider,
886-
):
887-
frame_bytes = bytes(getattr(frame, "data", b""))
888-
if not frame_bytes:
889-
continue
890-
self.nav_map_frame = frame_bytes
891-
self.nav_map_frame_updated_monotonic = time.monotonic()
892-
self._nav_map_frame_event.set()
893-
except asyncio.CancelledError:
894-
raise
895-
except Exception as err:
896-
if _is_unauthenticated_error(err):
897-
await self._async_handle_auth_failure("nav map stream", err)
898-
continue
899-
details = str(err).strip()
900-
if details:
901-
_LOGGER.debug("Vector nav map stream interrupted: %s", details)
902-
else:
903-
_LOGGER.debug(
904-
"Vector nav map stream interrupted (%s)",
905-
err.__class__.__name__,
906-
exc_info=True,
907-
)
908-
await asyncio.sleep(_NAV_MAP_RECONNECT_DELAY_SECONDS)
909-
continue
910-
911-
await asyncio.sleep(_NAV_MAP_RECONNECT_DELAY_SECONDS)
912-
913805
async def _async_camera_stream_loop(self) -> None:
914806
"""Keep persistent camera feed stream and cache latest frame."""
915807
while True:

custom_components/vector/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"iot_class": "local_push",
1515
"issue_tracker": "https://github.com/MTrab/vector/issues",
1616
"requirements": [
17-
"pyddlvector@git+https://github.com/MTrab/pyddlvector.git@feat/navmap-frame-feed"
17+
"pyddlvector@git+https://github.com/MTrab/pyddlvector.git@main"
1818
],
1919
"version": "0.1.0",
2020
"zeroconf": [

custom_components/vector/strings.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
"camera": {
44
"vision": {
55
"name": "Vision"
6-
},
7-
"nav_map": {
8-
"name": "Nav map"
96
}
107
},
118
"button": {

custom_components/vector/translations/en.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
"camera": {
44
"vision": {
55
"name": "Vision"
6-
},
7-
"nav_map": {
8-
"name": "Nav map"
96
}
107
},
118
"button": {

tests/test_camera.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import asyncio
66
from types import SimpleNamespace
77

8-
from custom_components.vector.camera import VectorNavMapCamera, VectorVisionCamera
8+
from custom_components.vector.camera import VectorVisionCamera
99
from custom_components.vector.const import CONF_HOST, CONF_ROBOT_NAME
1010

1111

@@ -15,9 +15,7 @@ class FakeCoordinator:
1515
def __init__(self, *, activity: str, frame: bytes | None) -> None:
1616
self.current_activity = activity
1717
self._frame = frame
18-
self._nav_map_frame = frame
1918
self.start_calls = 0
20-
self.nav_map_start_calls = 0
2119

2220
def async_add_listener(self, update_callback):
2321
del update_callback
@@ -32,15 +30,6 @@ async def async_get_latest_camera_frame(
3230
del wait_timeout
3331
return self._frame
3432

35-
async def async_start_nav_map_stream(self) -> None:
36-
self.nav_map_start_calls += 1
37-
38-
async def async_get_latest_nav_map_frame(
39-
self, *, wait_timeout: float = 1.0
40-
) -> bytes | None:
41-
del wait_timeout
42-
return self._nav_map_frame
43-
4433

4534
def _entry(data: dict[str, str], entry_id: str = "entry-1") -> SimpleNamespace:
4635
return SimpleNamespace(data=data, entry_id=entry_id)
@@ -80,29 +69,3 @@ def test_camera_returns_live_frame_when_available() -> None:
8069

8170
image = asyncio.run(entity.async_camera_image())
8271
assert image == b"\xff\xd8\xff"
83-
84-
85-
def test_nav_map_camera_entity_disabled_by_default() -> None:
86-
coordinator = FakeCoordinator(activity="idle", frame=b"\x89PNG")
87-
entry = _entry({CONF_ROBOT_NAME: "Vector-ABCD", CONF_HOST: "192.168.1.10"})
88-
entity = VectorNavMapCamera(coordinator, entry)
89-
assert entity.entity_registry_enabled_default is False
90-
91-
92-
def test_nav_map_camera_returns_unknown_asset_when_no_frame() -> None:
93-
coordinator = FakeCoordinator(activity="idle", frame=None)
94-
entry = _entry({CONF_ROBOT_NAME: "Vector-ABCD", CONF_HOST: "192.168.1.10"})
95-
entity = VectorNavMapCamera(coordinator, entry)
96-
97-
image = asyncio.run(entity.async_camera_image())
98-
assert image is not None
99-
assert image.startswith(b"\x89PNG")
100-
101-
102-
def test_nav_map_camera_returns_live_frame_when_available() -> None:
103-
coordinator = FakeCoordinator(activity="idle", frame=b"\x89PNG\r\n\x1a\nframe")
104-
entry = _entry({CONF_ROBOT_NAME: "Vector-ABCD", CONF_HOST: "192.168.1.10"})
105-
entity = VectorNavMapCamera(coordinator, entry)
106-
107-
image = asyncio.run(entity.async_camera_image())
108-
assert image == b"\x89PNG\r\n\x1a\nframe"

0 commit comments

Comments
 (0)