Files
Cognitive_Agent/tests/unit/test_verified_body_can.py

744 lines
31 KiB
Python

from __future__ import annotations
import unittest
from unittest.mock import patch
from cognitive_agent.application.native_can_feedback import (
decode_feedback,
feedback_matches,
)
from cognitive_agent.application.native_can_intent import (
_parse_exact_command,
merge_native_can_intent,
)
from cognitive_agent.application.native_can_runtime import (
NativeCanCompileError,
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.runtime.contracts import ContractValidator
class _FakeBus:
def __init__(
self,
received: list[CanFrame] | None = None,
*,
fail_send_calls: set[int] | None = None,
) -> None:
self.received = list(received or [])
self.fail_send_calls = set(fail_send_calls or set())
self.send_attempts: list[tuple[int, bytes, bool, bool]] = []
self.sent: list[CanFrame] = []
self.events: list[str] = []
self.debug_commands: list[str] = []
self.closed = False
def set_debug_command(self, command: str) -> None:
self.debug_commands.append(command)
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
self.events.append("send")
self.send_attempts.append(
(arbitration_id, bytes(data), is_fd, bitrate_switch)
)
call_number = len(self.send_attempts)
if call_number in self.fail_send_calls:
raise RuntimeError(f"injected send failure {call_number}")
frame = CanFrame(
arbitration_id,
bytes(data),
channel=1,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
self.sent.append(frame)
return frame
def receive(self, timeout_ms: int, *, include_fd: bool = False) -> CanFrame | None:
self.events.append("receive")
return self.received.pop(0) if self.received else None
def close(self) -> None:
self.closed = True
class VerifiedBodyCanTests(unittest.TestCase):
def setUp(self) -> None:
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
def compile_action(
self, domain: str, capability: str, target: str, parameters: dict
) -> dict:
config = self.runtime.build_function_config(
domain,
[
{
"domain": domain,
"capability": capability,
"target": target,
"operation": "set",
"parameters": parameters,
}
],
"verified-body-can",
"local_rules",
)
return self.runtime.compile(config)["spec"]["commands"][0]
@staticmethod
def shade_feedback(position: int, motion: int = 1) -> CanFrame:
data = bytearray(8)
data[1] = position
data[3] = motion << 3
return CanFrame(0x41A, bytes(data), channel=1)
def assert_fd_brs_triplet(self, command: dict, frame_id: str) -> None:
self.assertEqual(command["signals"]["frame_id"], frame_id)
self.assertTrue(command["signals"]["is_fd"])
self.assertTrue(command["signals"]["bitrate_switch"])
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
def test_indoor_and_all_reading_lights_use_only_verified_4f5(self) -> None:
cases = (
("interior_light.set_indoor_power", "vehicle", "on", "00 00 03 FF FF 00 08 00"),
("interior_light.set_indoor_power", "vehicle", "off", "00 00 03 FF FF 00 04 00"),
("interior_light.set_reading_power", "all", "on", "00 00 03 FF FF 00 08 00"),
("interior_light.set_reading_power", "all", "off", "00 00 03 FF FF 00 04 00"),
)
for capability, target, state, expected_hex in cases:
with self.subTest(capability=capability, state=state):
command = self.compile_action(
"interior_light", capability, target, {"state": state}
)
self.assert_fd_brs_triplet(command, "0x4F5")
self.assertEqual(command["signals"]["data_hex"], expected_hex)
self.assertEqual(command["signals"]["feedback_ids"], "0x42E")
self.assertEqual(
command["signals"]["expected_feedback"],
f"indoor_light:{state}",
)
def test_unverified_light_controls_are_not_hardware_capabilities(self) -> None:
rejected = (
("interior_light.set_wall_power", "vehicle", {"state": "on"}),
("ambient_light.set_mode", "vehicle", {"mode": "cinema"}),
(
"ambient_light.set_illumination",
"vehicle",
{"color_hex": "#203040", "brightness_pct": 40},
),
)
for capability, target, parameters in rejected:
action = {"capability": capability, "target": target, "parameters": parameters}
with self.subTest(capability=capability):
self.assertFalse(self.runtime.supports(action))
with self.assertRaises(NativeCanCompileError):
self.runtime.build_function_config(
capability.split(".", 1)[0],
[action],
"reject-unverified-light",
"local_rules",
)
zoned = {
"capability": "interior_light.set_reading_power",
"target": "front_left",
"parameters": {"state": "on"},
}
self.assertFalse(self.runtime.supports(zoned))
with self.assertRaises(NativeCanCompileError):
config = self.runtime.build_function_config(
"interior_light", [zoned], "reject-zoned-reading", "local_rules"
)
self.runtime.compile(config)
def test_compressor_catalog_action_cannot_reuse_hvac_power_frame(self) -> None:
action = {
"capability": "climate.set_ac",
"target": "vehicle",
"parameters": {"state": "on"},
}
self.assertFalse(self.runtime.supports(action))
self.assertNotIn("climate.set_ac", self.runtime.supported_capabilities)
with self.assertRaisesRegex(NativeCanCompileError, "no HIL-verified physical"):
self.runtime._frame_specs(
{
"capability": "climate.set_ac",
"target": {"zone": "vehicle"},
"parameters": {"state": "on"},
}
)
with self.assertRaises(NativeCanCompileError):
self.runtime.build_function_config(
"climate", [action], "reject-unverified-compressor", "local_rules"
)
def test_exact_parser_exposes_only_verified_light_vocabulary(self) -> None:
self.assertEqual(_parse_exact_command("light wall on"), [])
self.assertEqual(_parse_exact_command("light reading front-left on"), [])
self.assertEqual(_parse_exact_command("light ambient scene cinema"), [])
self.assertEqual(_parse_exact_command("light ambient rgb 32 64 96 40"), [])
color = _parse_exact_command("light ambient color 70")
self.assertEqual(color[0]["capability"], "ambient_light.set_color_index")
self.assertEqual(color[0]["parameters"], {"color_index": 70})
brightness = _parse_exact_command("light ambient brightness 5")
self.assertEqual(
brightness[0]["capability"], "ambient_light.set_brightness_level"
)
self.assertEqual(brightness[0]["parameters"], {"brightness_level": 5})
def test_all_cabin_lights_off_expands_to_verified_light_controls(self) -> None:
parsed = merge_native_can_intent(
{
"actions": [],
"domain_actions": [],
"native_can_actions": [],
"deferred_requests": [],
"model_requests": [],
"detected_domains": [],
"ambiguities": [],
"decision_summary": [],
},
"灯光全部关闭",
)
actions = parsed["native_can_actions"]
self.assertEqual(
[
(
action["capability"],
action["target"],
action["parameters"],
)
for action in actions
],
[
(
"interior_light.set_indoor_power",
"vehicle",
{"state": "off"},
),
(
"interior_light.set_reading_power",
"all",
{"state": "off"},
),
(
"ambient_light.set_power",
"vehicle",
{"state": "off"},
),
],
)
self.assertTrue(all(self.runtime.supports(action) for action in actions))
self.assertEqual(
parsed["detected_domains"], ["interior_light", "ambient_light"]
)
def test_fully_exact_ac_power_is_the_only_executable_plan(self) -> None:
for requested_state in ("on", "off"):
opposite_state = "off" if requested_state == "on" else "on"
for model_capability, model_state in (
("climate.set_ac", requested_state),
("climate.set_ac", opposite_state),
("climate.set_power", opposite_state),
):
with self.subTest(
requested_state=requested_state,
model_capability=model_capability,
model_state=model_state,
):
model_request = {
"domain": "climate",
"capability": model_capability,
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "state", "value": model_state}],
}
routed_action = {
"domain": "climate",
"capability": model_capability,
"target": "vehicle",
"operation": "set",
"parameters": {"state": model_state},
}
parsed = {
"detected_domains": ["climate", "window", "seat"],
"actions": [
{
"target": "passenger",
"function": "ventilation",
"state": "on",
}
],
"model_requests": [model_request],
"domain_actions": [
routed_action,
{
"domain": "window",
"capability": "window.set_motion",
"target": "front_left",
"operation": "set",
"parameters": {"motion": "open"},
},
],
"native_can_actions": [routed_action],
"deferred_requests": [routed_action],
"preset_actions": [{"target": "passenger"}],
"preset_events": [{"action": "apply"}],
"ambiguities": [
{"field": "requests[0].target", "question": "hallucinated"}
],
}
merged = merge_native_can_intent(
parsed, f"ac {requested_state}"
)
self.assertEqual(merged["actions"], [])
self.assertEqual(merged["domain_actions"], [])
self.assertEqual(merged["preset_actions"], [])
self.assertEqual(merged["preset_events"], [])
self.assertEqual(merged["ambiguities"], [])
self.assertEqual(merged["detected_domains"], ["climate"])
self.assertEqual(len(merged["model_requests"]), 1)
self.assertEqual(len(merged["deferred_requests"]), 1)
self.assertEqual(len(merged["native_can_actions"]), 1)
for key in (
"model_requests",
"deferred_requests",
"native_can_actions",
):
self.assertEqual(
merged[key][0]["capability"], "climate.set_power"
)
parameters = merged[key][0]["parameters"]
if isinstance(parameters, list):
parameters = {
item["name"]: item["value"] for item in parameters
}
self.assertEqual(parameters, {"state": requested_state})
natural_model_action = {
"domain": "climate",
"capability": "climate.set_ac",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
natural = merge_native_can_intent(
{
"model_requests": [natural_model_action],
"domain_actions": [natural_model_action],
"native_can_actions": [],
"deferred_requests": [],
},
"打开空调并关闭遮阳帘",
)
self.assertIn(natural_model_action, natural["domain_actions"])
self.assertGreaterEqual(len(natural["native_can_actions"]), 2)
mixed_shade_action = {
"domain": "roof",
"capability": "roof.set_shade_motion",
"target": "vehicle",
"operation": "set",
"parameters": {"motion": "close"},
}
mixed = merge_native_can_intent(
{
"model_requests": [],
"domain_actions": [mixed_shade_action],
"native_can_actions": [],
"deferred_requests": [],
},
"ac on,关闭遮阳帘",
)
self.assertIn(mixed_shade_action, mixed["domain_actions"])
self.assertEqual(
[item["capability"] for item in mixed["native_can_actions"]],
["climate.set_power"],
)
def test_vehicle_lock_is_observational_only_and_fails_closed(self) -> None:
action = {
"domain": "door",
"capability": "door.set_vehicle_lock",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "lock"},
}
catalog_entry = self.runtime.capabilities[action["capability"]]
self.assertEqual(catalog_entry["execution_support"], "observational_only")
self.assertFalse(catalog_entry["physical_control_supported"])
self.assertNotIn(action["capability"], self.runtime.supported_capabilities)
self.assertFalse(self.runtime.supports(action))
with self.assertRaisesRegex(
NativeCanCompileError,
r"unsupported.*current CH1.*0x496.*observational only",
):
self.runtime.build_function_config(
"door", [action], "retired-vehicle-lock", "local_rules"
)
with self.assertRaisesRegex(
NativeCanCompileError, r"unsupported.*current CH1.*observational only"
):
self.runtime._frame_specs({"capability": action["capability"]})
vehicle = _parse_exact_command("door vehicle lock")
self.assertEqual(vehicle[0]["capability"], "door.set_vehicle_lock")
self.assertEqual(vehicle[0]["target"], "vehicle")
def test_rear_lock_compiles_only_the_matrix_backed_second_row_request(self) -> None:
action = {
"domain": "door",
"capability": "door.set_rear_lock",
"target": "rear",
"operation": "set",
"parameters": {"state": "lock"},
}
catalog_entry = self.runtime.capabilities[action["capability"]]
self.assertEqual(
catalog_entry["execution_support"], "matrix_candidate_with_feedback"
)
self.assertTrue(catalog_entry["physical_control_supported"])
self.assertIn(action["capability"], self.runtime.supported_capabilities)
self.assertTrue(self.runtime.supports(action))
cases = (
("lock", "0A 40 00 00 00 00 00 00"),
("unlock", "0A 80 00 00 00 00 00 00"),
)
for state, payload in cases:
with self.subTest(state=state):
command = self.compile_action(
"door", "door.set_rear_lock", "rear", {"state": state}
)
self.assertEqual(command["signals"]["frame_id"], "0x32B")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertFalse(command["signals"]["is_fd"])
self.assertFalse(command["signals"]["bitrate_switch"])
self.assertNotIn("repeat", command)
self.assertEqual(command["signals"]["feedback_ids"], "0x407")
self.assertEqual(
command["signals"]["expected_feedback"], f"rear_lock:{state}"
)
exact = _parse_exact_command(f"door rear {state}")
self.assertEqual(exact[0]["capability"], "door.set_rear_lock")
self.assertEqual(exact[0]["target"], "rear")
def test_climate_cycle_uses_verified_1de_bits_and_404_feedback(self) -> None:
cases = (
("internal", "00 00 08 00 00 00 00 00"),
("external", "00 00 10 00 00 00 00 00"),
)
for mode, payload in cases:
with self.subTest(mode=mode):
command = self.compile_action(
"climate",
"climate.set_cycle_mode",
"vehicle",
{"mode": mode},
)
self.assertEqual(command["signals"]["frame_id"], "0x1DE")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["repeat"], {"count": 3, "period_ms": 100})
self.assertEqual(command["signals"]["feedback_ids"], "0x404")
self.assertEqual(
command["signals"]["expected_feedback"],
f"climate_cycle:{mode}",
)
def test_ambient_controls_use_only_verified_3fc_layouts(self) -> None:
cases = (
("ambient_light.set_power", {"state": "on"}, "00 36 00 75 00 80 00 00", "ambient_power:on"),
("ambient_light.set_power", {"state": "off"}, "00 31 00 75 00 80 00 00", "ambient_power:off"),
("ambient_light.set_color_index", {"color_index": 70}, "46 30 00 75 00 00 00 00", "ambient_color:70"),
("ambient_light.set_brightness_level", {"brightness_level": 1}, "00 32 00 75 00 00 00 00", "ambient_brightness:1"),
("ambient_light.set_brightness_level", {"brightness_level": 5}, "00 36 00 75 00 00 00 00", "ambient_brightness:5"),
)
for capability, parameters, payload, expected_feedback in cases:
with self.subTest(capability=capability, parameters=parameters):
command = self.compile_action(
"ambient_light", capability, "vehicle", parameters
)
self.assert_fd_brs_triplet(command, "0x3FC")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertEqual(command["signals"]["feedback_ids"], "0x42E")
self.assertEqual(
command["signals"]["expected_feedback"], expected_feedback
)
def test_verified_ambient_function_configs_pass_domain_schema(self) -> None:
validator = ContractValidator(REPOSITORY_ROOT)
for capability, parameters in (
("ambient_light.set_color_index", {"color_index": 70}),
("ambient_light.set_brightness_level", {"brightness_level": 5}),
):
with self.subTest(capability=capability):
config = self.runtime.build_function_config(
"ambient_light",
[
{
"domain": "ambient_light",
"capability": capability,
"target": "vehicle",
"operation": "set",
"parameters": parameters,
}
],
"verified-ambient-schema",
"local_rules",
)
self.assertEqual(validator.validate(config), [])
def test_ambient_ranges_and_parameter_shapes_fail_closed(self) -> None:
for capability, parameters in (
("ambient_light.set_color_index", {"color_index": 0}),
("ambient_light.set_color_index", {"color_index": 128}),
("ambient_light.set_brightness_level", {"brightness_level": 0}),
("ambient_light.set_brightness_level", {"brightness_level": 6}),
(
"ambient_light.set_color_index",
{"color_index": 70, "brightness_level": 3},
),
):
with self.subTest(capability=capability, parameters=parameters):
with self.assertRaises(NativeCanCompileError):
self.compile_action("ambient_light", capability, "vehicle", parameters)
def test_shade_presets_and_arbitrary_position_never_use_4f5(self) -> None:
for position, payload in (
(0, "20 00 00 00 00 00 00 00"),
(50, "30 00 00 00 00 00 00 00"),
(100, "10 00 00 00 00 00 00 00"),
):
with self.subTest(position=position):
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": position}
)
self.assertEqual(command["signals"]["frame_id"], "0x431")
self.assertEqual(command["signals"]["data_hex"], payload)
self.assertFalse(command["signals"]["is_fd"])
self.assertEqual(command["signals"]["feedback_ids"], "0x41A")
self.assertEqual(command["signals"]["special_handler"], "shade_preset")
self.assertEqual(
command["signals"]["debug_command"],
f"shade position {position}",
)
arbitrary = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
self.assertEqual(arbitrary["signals"]["frame_id"], "0x431")
self.assertNotEqual(arbitrary["signals"]["frame_id"], "0x4F5")
self.assertEqual(arbitrary["signals"]["special_handler"], "shade_position")
self.assertEqual(arbitrary["signals"]["debug_command"], "shade position 37")
special = arbitrary["signals"]["special_parameters"]
self.assertIn('"fresh_feedback_timeout_ms": 2500', special)
self.assertIn('"stop_data_hex": "40 00 00 00 00 00 00 00"', special)
def test_executor_attaches_compiled_debug_command_to_mqtt_style_bus(self) -> None:
config = self.runtime.build_function_config(
"roof",
[
{
"domain": "roof",
"capability": "roof.set_shade_position",
"target": "vehicle",
"operation": "set",
"parameters": {"position_pct": 100},
}
],
"shade-debug-command",
"local_rules",
)
plan = self.runtime.compile(config)
bus = _FakeBus([self.shade_feedback(100, motion=1)])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
report = executor.execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(bus.debug_commands, ["shade position 100"])
def test_shade_preset_executor_waits_for_authoritative_target(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 100}
)
reached = self.shade_feedback(99, motion=1)
bus = _FakeBus([reached])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_preset(bus, command)
self.assertIs(feedback, reached)
self.assertEqual(len(sent), 1)
self.assertEqual(
bus.send_attempts,
[(0x431, bytes.fromhex("10 00 00 00 00 00 00 00"), False, False)],
)
def test_shade_preset_forces_stop_when_target_feedback_is_still_moving(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 0}
)
reached_while_closing = self.shade_feedback(2, motion=3)
stopped = self.shade_feedback(2, motion=1)
bus = _FakeBus([reached_while_closing, stopped])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_preset(bus, command)
self.assertIs(feedback, stopped)
self.assertEqual(len(sent), 2)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("20 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_position_reads_fresh_state_then_stops(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
before = self.shade_feedback(20, motion=1)
reached = self.shade_feedback(38, motion=2)
stopped = self.shade_feedback(38, motion=1)
bus = _FakeBus([before, reached, stopped])
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
sent, feedback = executor._execute_shade_position(bus, command)
self.assertIs(feedback, stopped)
self.assertEqual(bus.events[0], "receive")
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
self.assertTrue(all(not attempt[2] and not attempt[3] for attempt in bus.send_attempts))
self.assertEqual(len(sent), 2)
def test_arbitrary_shade_position_always_stops_after_feedback_fault(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
bus = _FakeBus()
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
before = self.shade_feedback(20, motion=1)
with patch.object(
executor,
"_wait_for_id",
side_effect=[before, RuntimeError("injected receive failure")],
):
with self.assertRaisesRegex(RuntimeError, "injected receive failure"):
executor._execute_shade_position(bus, command)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_start_failure_still_attempts_stop(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
bus = _FakeBus([self.shade_feedback(20, motion=1)], fail_send_calls={1})
executor = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1)
with self.assertRaisesRegex(RuntimeError, "injected send failure"):
executor._execute_shade_position(bus, command)
self.assertEqual(
[attempt[1] for attempt in bus.send_attempts],
[
bytes.fromhex("10 00 00 00 00 00 00 00"),
bytes.fromhex("40 00 00 00 00 00 00 00"),
],
)
def test_arbitrary_shade_missing_or_invalid_feedback_never_starts_motion(self) -> None:
command = self.compile_action(
"roof", "roof.set_shade_position", "vehicle", {"position_pct": 37}
)
executor = NativeCanExecutor(lambda: _FakeBus(), feedback_timeout_ms=1)
for before in (None, self.shade_feedback(255, motion=1)):
bus = _FakeBus()
with self.subTest(before=before):
with patch.object(executor, "_wait_for_id", return_value=before):
with self.assertRaisesRegex(
RuntimeError, "0x41A|新鲜|有效|fresh|valid"
):
executor._execute_shade_position(bus, command)
self.assertEqual(bus.send_attempts, [])
def test_door_feedback_enums_and_shade_stop_match_reference(self) -> None:
door_data = bytearray(8)
# 0x407: front-left unlock, front-right lock, rear-left lock,
# rear-right unlock; whole-vehicle logical state lock.
door_data[0] = 0b01101001
door_data[1] = 0b00001000
decoded = decode_feedback(CanFrame(0x407, bytes(door_data), channel=1))
locks = decoded["domain_state"]["door"]["locks"]
self.assertEqual(locks["front_left"]["state"], "unlock")
self.assertEqual(locks["front_right"]["state"], "lock")
self.assertEqual(locks["rear_left"]["state"], "lock")
self.assertEqual(locks["rear_right"]["state"], "unlock")
self.assertEqual(
decoded["domain_state"]["door"]["vehicle_lock"]["state"], "lock"
)
all_locked = bytearray(8)
all_locked[0] = 0xAA
all_locked[1] = 0x08
locked_frame = CanFrame(0x407, bytes(all_locked), channel=1)
self.assertTrue(feedback_matches("vehicle_lock:lock", locked_frame))
logical_only = bytearray(all_locked)
logical_only[0] = 0xA9
self.assertFalse(
feedback_matches(
"vehicle_lock:lock",
CanFrame(0x407, bytes(logical_only), channel=1),
)
)
all_unlocked = bytearray(8)
all_unlocked[0] = 0x55
all_unlocked[1] = 0x04
self.assertTrue(
feedback_matches(
"vehicle_lock:unlock",
CanFrame(0x407, bytes(all_unlocked), channel=1),
)
)
shade_data = bytearray(8)
shade_data[1] = 37
shade_data[3] = 1 << 3
shade = CanFrame(0x41A, bytes(shade_data), channel=1)
self.assertTrue(feedback_matches("shade_stopped", shade))
if __name__ == "__main__":
unittest.main()