feat(n7web): enhance control workflows and status feedback
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.mode_scene_catalog import (
|
||||
ModeSceneCatalogPublisher,
|
||||
ModeSceneMqttBurstPublisher,
|
||||
build_mode_scene_catalog,
|
||||
)
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
|
||||
@@ -156,3 +159,247 @@ def test_repository_presets_are_exported_without_internal_command_flows() -> Non
|
||||
encoded = json.dumps(catalog, ensure_ascii=False)
|
||||
assert "command_flow" not in encoded
|
||||
assert "source_request_id" not in encoded
|
||||
|
||||
|
||||
def test_saved_editor_flow_projects_all_native_and_extension_operations(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
registry = ModeProfileRegistry(
|
||||
tmp_path / "presets",
|
||||
user_root=tmp_path / "profiles",
|
||||
)
|
||||
operations = [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "driver",
|
||||
"parameters": {"temperature_c": 24},
|
||||
},
|
||||
{
|
||||
"domain": "interior_light",
|
||||
"capability": "interior_light.set_reading_power",
|
||||
"target_scope": "all",
|
||||
"parameters": {"state": "on"},
|
||||
},
|
||||
{
|
||||
"domain": "roof",
|
||||
"capability": "roof.set_shade_motion",
|
||||
"target_scope": "vehicle",
|
||||
"parameters": {"motion": "open"},
|
||||
},
|
||||
{
|
||||
"domain": "window",
|
||||
"capability": "window.set_position",
|
||||
"target_scope": "front_right",
|
||||
"parameters": {"position_pct": 50},
|
||||
},
|
||||
]
|
||||
|
||||
def configured_operation(
|
||||
operation_id: str,
|
||||
capability: str,
|
||||
zone: str,
|
||||
parameters: dict[str, object],
|
||||
*,
|
||||
device: str,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"id": operation_id,
|
||||
"capability": capability,
|
||||
"parameters": parameters,
|
||||
"target": {"device": device, "zone": zone},
|
||||
}
|
||||
|
||||
function_configs = {
|
||||
"native_climate": {
|
||||
"spec": {
|
||||
"domain": "climate",
|
||||
"operations": [
|
||||
configured_operation(
|
||||
"driver_set_temperature_1",
|
||||
"climate.set_temperature",
|
||||
"driver",
|
||||
{"temperature_c": 24},
|
||||
device="hvac",
|
||||
)
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_interior_light": {
|
||||
"spec": {
|
||||
"domain": "interior_light",
|
||||
"operations": [
|
||||
configured_operation(
|
||||
"all_set_reading_power_1",
|
||||
"interior_light.set_reading_power",
|
||||
"all",
|
||||
{"state": "on"},
|
||||
device="reading_light",
|
||||
)
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_roof": {
|
||||
"spec": {
|
||||
"domain": "roof",
|
||||
"operations": [
|
||||
configured_operation(
|
||||
"vehicle_set_shade_motion_1",
|
||||
"roof.set_shade_motion",
|
||||
"vehicle",
|
||||
{"motion": "open"},
|
||||
device="roof_shade",
|
||||
)
|
||||
],
|
||||
}
|
||||
},
|
||||
"native_window": {
|
||||
"spec": {
|
||||
"domain": "window",
|
||||
"operations": [
|
||||
configured_operation(
|
||||
"front_right_set_position_1",
|
||||
"window.set_position",
|
||||
"front_right",
|
||||
{"position_pct": 50},
|
||||
device="window",
|
||||
)
|
||||
],
|
||||
}
|
||||
},
|
||||
"self_extension": {
|
||||
"spec": {
|
||||
"domain": "self_extension",
|
||||
"operations": [
|
||||
{
|
||||
"id": "ir-slot:toy-light-001:2-1",
|
||||
"capability": "infrared.emit",
|
||||
"parameters": {"action": "打开", "slot": 2},
|
||||
"target": {
|
||||
"card_uid": "2CC8B103",
|
||||
"device": "toy-light-001",
|
||||
"name": "闪烁灯",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "ir-slot:toy-light-001:6-2",
|
||||
"capability": "infrared.emit",
|
||||
"parameters": {"action": "设置为白色", "slot": 6},
|
||||
"target": {
|
||||
"card_uid": "2CC8B103",
|
||||
"device": "toy-light-001",
|
||||
"name": "闪烁灯",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
registry.save_mode(
|
||||
"阅读模式",
|
||||
operations,
|
||||
mode_id="mode-reading",
|
||||
vehicle_id="denza_n7",
|
||||
command_flow={"function_configs": function_configs},
|
||||
)
|
||||
|
||||
catalog = build_mode_scene_catalog(
|
||||
registry,
|
||||
"denza_n7",
|
||||
generated_at="2026-09-15T00:00:00Z",
|
||||
)
|
||||
reading = next(
|
||||
scene for scene in catalog["scenes"] if scene["id"] == "mode-reading"
|
||||
)
|
||||
|
||||
assert reading["operation_count"] == 6
|
||||
assert len(reading["actions"]) == 6
|
||||
assert [action["capability"] for action in reading["actions"]] == [
|
||||
"climate.set_temperature",
|
||||
"interior_light.set_reading_power",
|
||||
"roof.set_shade_motion",
|
||||
"window.set_position",
|
||||
"infrared.emit",
|
||||
"infrared.emit",
|
||||
]
|
||||
assert reading["domains"] == [
|
||||
"climate",
|
||||
"interior_light",
|
||||
"roof",
|
||||
"self_extension",
|
||||
"window",
|
||||
]
|
||||
first_infrared, second_infrared = reading["actions"][-2:]
|
||||
assert first_infrared["parameters"] == {"action": "打开", "slot": 2}
|
||||
assert second_infrared["parameters"] == {"action": "设置为白色", "slot": 6}
|
||||
assert first_infrared["target"] == {
|
||||
"card_uid": "2CC8B103",
|
||||
"device": "toy-light-001",
|
||||
"name": "闪烁灯",
|
||||
}
|
||||
assert second_infrared["target"] == first_infrared["target"]
|
||||
assert first_infrared["operation"] == "emit"
|
||||
|
||||
|
||||
def test_mode_scene_mqtt_burst_publishes_only_one_exact_card_three_times() -> None:
|
||||
publications: list[tuple[float, dict[str, object]]] = []
|
||||
complete = threading.Event()
|
||||
scene: dict[str, object] = {
|
||||
"id": "mode-reading",
|
||||
"label": "阅读模式",
|
||||
"name": "阅读模式",
|
||||
"background_time": "noon",
|
||||
"description": "由自然语言生成的自定义模式。",
|
||||
"icon": "bookmark-plus",
|
||||
"aliases": ["阅读模式", "阅读"],
|
||||
"origin": "user",
|
||||
"deletable": True,
|
||||
"behavior": "static",
|
||||
"domains": ["climate"],
|
||||
"operation_count": 1,
|
||||
"scope": {"common": False, "vehicle_ids": ["denza_n7"]},
|
||||
"profile_revision": "1234567890abcdef",
|
||||
"actions": [
|
||||
{
|
||||
"type": "climate_temperature",
|
||||
"at_ms": 0,
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": {"temperature_c": 24},
|
||||
"zone": "passenger",
|
||||
"celsius": 24,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def publish(payload: dict[str, object]) -> dict[str, object]:
|
||||
publications.append((time.monotonic(), payload))
|
||||
if len(publications) == 3:
|
||||
complete.set()
|
||||
return {"delivery_status": "published", "message_id": len(publications)}
|
||||
|
||||
publisher = ModeSceneMqttBurstPublisher(
|
||||
publish,
|
||||
topic="from_pc/scenes",
|
||||
burst_window_s=0.12,
|
||||
)
|
||||
queued_at = time.monotonic()
|
||||
try:
|
||||
queued = publisher.queue(scene)
|
||||
assert complete.wait(timeout=1.0)
|
||||
assert queued == {
|
||||
"state": "queued",
|
||||
"topic": "from_pc/scenes",
|
||||
"scene_id": "mode-reading",
|
||||
"burst_count": 3,
|
||||
"burst_window_ms": 120,
|
||||
}
|
||||
assert len(publications) == 3
|
||||
assert publications[-1][0] - queued_at < 0.2
|
||||
assert all(payload == scene for _, payload in publications)
|
||||
assert all("scenes" not in payload for _, payload in publications)
|
||||
assert publisher.snapshot()["state"] == "burst_complete"
|
||||
finally:
|
||||
publisher.close()
|
||||
|
||||
Reference in New Issue
Block a user