feat: improve mode and peripheral orchestration
This commit is contained in:
@@ -104,6 +104,7 @@ from cognitive_agent.application.native_can_feedback import ( # noqa: E402
|
||||
)
|
||||
from cognitive_agent.application.native_can_intent import ( # noqa: E402
|
||||
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN,
|
||||
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN,
|
||||
merge_native_can_intent,
|
||||
)
|
||||
from cognitive_agent.application.native_can_relative import ( # noqa: E402
|
||||
@@ -202,6 +203,39 @@ LOCAL_CONTROL_BUDGET_MS = 1000
|
||||
VEHICLE_CONTROL_TRANSPORTS = frozenset(
|
||||
{WIRED_CAN_TRANSPORT, WIRELESS_MQTT_TRANSPORT}
|
||||
)
|
||||
N7_CINEMA_RELATED_PATTERN = re.compile(
|
||||
r"影院|观影|电影|看电影|cinema|movie|theater|theatre",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_CINEMA_BASE_NAMES = frozenset(
|
||||
{"影院", "影院模式", "观影", "观影模式", "cinema", "cinemamode"}
|
||||
)
|
||||
N7_FLASHING_LIGHT_PATTERN = re.compile(
|
||||
r"闪烁灯|闪光灯|闪灯玩具|flashing\s*light",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_LIGHT_STRIP_PATTERN = re.compile(
|
||||
r"灯带|灯条|light\s*strip|led\s*strip",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_PERIPHERAL_ACTIVATE_PATTERN = re.compile(
|
||||
r"打开|开启|启动|点亮|\bon\b|power\s*on",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_PERIPHERAL_OFF_PATTERN = re.compile(
|
||||
r"关闭|关掉|熄灭|\boff\b|power\s*off",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_PERIPHERAL_COLOR_PATTERN = re.compile(
|
||||
r"红色|绿色|蓝色|白色|紫色|黄色|橙色|粉色|"
|
||||
r"\bred\b|\bgreen\b|\bblue\b|\bwhite\b|\bpurple\b|"
|
||||
r"\byellow\b|\borange\b|\bpink\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
N7_CINEMA_VIDEO_APP_PATTERN = re.compile(
|
||||
r"视频|媒体|爱奇艺|video|media|videoplay|iqiyi",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _wireless_mqtt_publish_preflight() -> dict[str, Any]:
|
||||
@@ -472,6 +506,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
lambda scene: self._vehicle_mqtt_transport.publish_mode_scene(scene),
|
||||
topic=MQTT_MODE_SCENE_TOPIC,
|
||||
)
|
||||
self._mode_scene_peripheral_catalog_provider: Callable[
|
||||
[], list[dict[str, Any]]
|
||||
] = lambda: []
|
||||
self._mode_scene_ingress = ModeSceneIngressConsumer(
|
||||
self.import_n7_mode_scene,
|
||||
)
|
||||
@@ -637,6 +674,378 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
|
||||
return self.publish_saved_mode_scene(mode_id, vehicle_id)
|
||||
|
||||
@staticmethod
|
||||
def _n7_scene_identity_text(scene: Mapping[str, Any]) -> str:
|
||||
aliases = scene.get("aliases", [])
|
||||
return " ".join(
|
||||
str(value or "")
|
||||
for value in (
|
||||
scene.get("id"),
|
||||
scene.get("name"),
|
||||
scene.get("label"),
|
||||
scene.get("description"),
|
||||
*(aliases if isinstance(aliases, list) else []),
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _is_n7_cinema_scene(cls, scene: Mapping[str, Any]) -> bool:
|
||||
return N7_CINEMA_RELATED_PATTERN.search(
|
||||
cls._n7_scene_identity_text(scene)
|
||||
) is not None
|
||||
|
||||
@staticmethod
|
||||
def _is_n7_cinema_base_scene(scene: Mapping[str, Any]) -> bool:
|
||||
scene_id = re.sub(r"\s+", "", str(scene.get("id") or "")).casefold()
|
||||
names = {
|
||||
re.sub(r"\s+", "", str(scene.get(field) or "")).casefold()
|
||||
for field in ("name", "label")
|
||||
}
|
||||
return scene_id == "cinema" or bool(names & N7_CINEMA_BASE_NAMES)
|
||||
|
||||
@staticmethod
|
||||
def _n7_scene_action_selector(action: Mapping[str, Any]) -> tuple[object, ...]:
|
||||
parameters = action.get("parameters")
|
||||
slot = (
|
||||
parameters.get("slot")
|
||||
if isinstance(parameters, Mapping)
|
||||
and action.get("capability") == "infrared.emit"
|
||||
else None
|
||||
)
|
||||
return (
|
||||
str(action.get("domain") or ""),
|
||||
str(action.get("capability") or ""),
|
||||
str(action.get("target_scope") or ""),
|
||||
str(action.get("operation") or "set"),
|
||||
int(action.get("at_ms") or 0),
|
||||
slot,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _merge_n7_scene_actions(
|
||||
cls,
|
||||
base_actions: object,
|
||||
overriding_actions: object,
|
||||
) -> list[dict[str, Any]]:
|
||||
merged: list[dict[str, Any]] = []
|
||||
positions: dict[tuple[object, ...], int] = {}
|
||||
for source in (base_actions, overriding_actions):
|
||||
if not isinstance(source, list):
|
||||
continue
|
||||
for raw_action in source:
|
||||
if not isinstance(raw_action, Mapping):
|
||||
continue
|
||||
action = deepcopy(dict(raw_action))
|
||||
selector = cls._n7_scene_action_selector(action)
|
||||
existing = positions.get(selector)
|
||||
if existing is None:
|
||||
positions[selector] = len(merged)
|
||||
merged.append(action)
|
||||
else:
|
||||
merged[existing] = action
|
||||
return merged
|
||||
|
||||
def _n7_cinema_base_scene(
|
||||
self,
|
||||
incoming_scene: Mapping[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
if self._is_n7_cinema_base_scene(incoming_scene):
|
||||
return None
|
||||
vehicle_id = self.vehicle_profiles.default().id
|
||||
candidates: list[tuple[int, dict[str, Any]]] = []
|
||||
for public_profile in self.mode_profiles.public_modes(vehicle_id):
|
||||
try:
|
||||
profile = self.mode_profiles.get_profile(
|
||||
str(public_profile.get("id") or ""),
|
||||
vehicle_id,
|
||||
)
|
||||
except (KeyError, ValueError):
|
||||
continue
|
||||
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
||||
continue
|
||||
command_flow = profile.command_flow
|
||||
source_scene = (
|
||||
command_flow.get("source_scene")
|
||||
if isinstance(command_flow, Mapping)
|
||||
else None
|
||||
)
|
||||
if not isinstance(source_scene, Mapping) or not self._is_n7_cinema_base_scene(
|
||||
source_scene
|
||||
):
|
||||
continue
|
||||
priority = 0 if str(source_scene.get("id") or "") == "cinema" else 1
|
||||
candidates.append((priority, deepcopy(dict(source_scene))))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort(key=lambda item: (item[0], str(item[1].get("id") or "")))
|
||||
return candidates[0][1]
|
||||
|
||||
@staticmethod
|
||||
def _is_n7_cinema_video_launch_action(action: Mapping[str, Any]) -> bool:
|
||||
parameters = action.get("parameters")
|
||||
if (
|
||||
action.get("domain") != "android"
|
||||
or action.get("capability") != "android.launch_app"
|
||||
or action.get("target_scope") != "headunit"
|
||||
or action.get("operation") != "launch"
|
||||
or not isinstance(parameters, Mapping)
|
||||
):
|
||||
return False
|
||||
package_name = str(parameters.get("package_name") or "").strip()
|
||||
activity_name = str(parameters.get("activity_name") or "").strip()
|
||||
if (
|
||||
not package_name
|
||||
or not activity_name
|
||||
or len(package_name) > 256
|
||||
or len(activity_name) > 256
|
||||
):
|
||||
return False
|
||||
return N7_CINEMA_VIDEO_APP_PATTERN.search(
|
||||
" ".join(
|
||||
(
|
||||
str(action.get("type") or ""),
|
||||
package_name,
|
||||
activity_name,
|
||||
)
|
||||
)
|
||||
) is not None
|
||||
|
||||
def _local_cinema_n7_transfer_actions(
|
||||
self,
|
||||
mode_name: str,
|
||||
instruction: str = "",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Reuse the N7 cinema card's video launch for a local cinema variant."""
|
||||
|
||||
scene_identity = {
|
||||
"id": "local-user-mode-variant",
|
||||
"name": f"本地扩展 {str(mode_name or '').strip()}",
|
||||
"label": f"本地扩展 {str(mode_name or '').strip()}",
|
||||
"description": str(instruction or ""),
|
||||
}
|
||||
if not self._is_n7_cinema_scene(scene_identity):
|
||||
return []
|
||||
base_scene = self._n7_cinema_base_scene(scene_identity)
|
||||
if base_scene is None:
|
||||
return []
|
||||
actions = base_scene.get("actions", [])
|
||||
if not isinstance(actions, list):
|
||||
return []
|
||||
return [
|
||||
deepcopy(dict(action))
|
||||
for action in actions
|
||||
if isinstance(action, Mapping)
|
||||
and self._is_n7_cinema_video_launch_action(action)
|
||||
][:1]
|
||||
|
||||
@staticmethod
|
||||
def _n7_transfer_operation_selector(
|
||||
domain: object,
|
||||
capability: object,
|
||||
target_scope: object,
|
||||
operation: object,
|
||||
parameters: object,
|
||||
) -> tuple[str, str, str, str, str]:
|
||||
clean_parameters = dict(parameters) if isinstance(parameters, Mapping) else {}
|
||||
return (
|
||||
str(domain or ""),
|
||||
str(capability or ""),
|
||||
str(target_scope or ""),
|
||||
str(operation or "set"),
|
||||
json.dumps(
|
||||
clean_parameters,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _cinema_preferred_color_patterns(
|
||||
cls,
|
||||
scene: Mapping[str, Any],
|
||||
) -> list[re.Pattern[str]]:
|
||||
text = cls._n7_scene_identity_text(scene)
|
||||
colors = (
|
||||
(r"蓝色|\bblue\b",),
|
||||
(r"紫色|\bpurple\b",),
|
||||
(r"红色|\bred\b",),
|
||||
(r"白色|\bwhite\b",),
|
||||
(r"绿色|\bgreen\b",),
|
||||
(r"黄色|\byellow\b",),
|
||||
(r"橙色|\borange\b",),
|
||||
(r"粉色|\bpink\b",),
|
||||
)
|
||||
explicit = [
|
||||
re.compile(patterns[0], re.IGNORECASE)
|
||||
for patterns in colors
|
||||
if re.search(patterns[0], text, re.IGNORECASE)
|
||||
]
|
||||
defaults = [re.compile(patterns[0], re.IGNORECASE) for patterns in colors]
|
||||
return [*explicit, *(item for item in defaults if item not in explicit)]
|
||||
|
||||
def _n7_cinema_peripheral_actions(
|
||||
self,
|
||||
scene: Mapping[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
try:
|
||||
raw_catalog = self._mode_scene_peripheral_catalog_provider()
|
||||
except Exception: # noqa: BLE001 - an unavailable catalog must not block import
|
||||
return []
|
||||
if not isinstance(raw_catalog, list):
|
||||
return []
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
identities: dict[str, str] = {}
|
||||
for raw in raw_catalog:
|
||||
if not isinstance(raw, Mapping):
|
||||
continue
|
||||
device_id = str(raw.get("device_id") or "").strip()
|
||||
card_uid = str(raw.get("card_uid") or "").strip()
|
||||
slot = raw.get("slot")
|
||||
if (
|
||||
not device_id
|
||||
or not card_uid
|
||||
or raw.get("status") != "learned"
|
||||
or raw.get("enabled", True) is False
|
||||
or isinstance(slot, bool)
|
||||
or not isinstance(slot, int)
|
||||
or not 1 <= slot <= 248
|
||||
):
|
||||
continue
|
||||
item = deepcopy(dict(raw))
|
||||
grouped.setdefault(device_id, []).append(item)
|
||||
names = [
|
||||
raw.get("device_name"),
|
||||
*(raw.get("device_aliases", []) if isinstance(raw.get("device_aliases"), list) else []),
|
||||
*(raw.get("object_names", []) if isinstance(raw.get("object_names"), list) else []),
|
||||
]
|
||||
identities[device_id] = " ".join(str(value or "") for value in names)
|
||||
|
||||
def action_text(item: Mapping[str, Any]) -> str:
|
||||
return " ".join(
|
||||
[
|
||||
str(item.get("action") or ""),
|
||||
*(item.get("aliases", []) if isinstance(item.get("aliases"), list) else []),
|
||||
*(item.get("action_names", []) if isinstance(item.get("action_names"), list) else []),
|
||||
]
|
||||
)
|
||||
|
||||
def as_scene_action(
|
||||
item: Mapping[str, Any],
|
||||
*,
|
||||
category: str,
|
||||
kind: str,
|
||||
) -> dict[str, Any]:
|
||||
device_id = str(item.get("device_id") or "")
|
||||
device_name = str(item.get("device_name") or device_id)
|
||||
slot = int(item["slot"])
|
||||
action_name = str(item.get("action") or "").strip()
|
||||
return {
|
||||
"type": "infrared_emit",
|
||||
"at_ms": 0,
|
||||
"domain": "self_extension",
|
||||
"capability": "infrared.emit",
|
||||
"target_scope": device_id,
|
||||
"target": {
|
||||
"device": device_id,
|
||||
"name": device_name,
|
||||
"card_uid": str(item.get("card_uid") or ""),
|
||||
},
|
||||
"operation": "emit",
|
||||
"parameters": {"action": action_name, "slot": slot},
|
||||
"operation_ref": f"pc-cinema-{category}-{kind}-{slot}",
|
||||
}
|
||||
|
||||
selected_devices: set[str] = set()
|
||||
result: list[dict[str, Any]] = []
|
||||
color_patterns = self._cinema_preferred_color_patterns(scene)
|
||||
for category, identity_pattern in (
|
||||
("flashing-light", N7_FLASHING_LIGHT_PATTERN),
|
||||
("light-strip", N7_LIGHT_STRIP_PATTERN),
|
||||
):
|
||||
device_id = next(
|
||||
(
|
||||
key
|
||||
for key in sorted(grouped)
|
||||
if key not in selected_devices
|
||||
and identity_pattern.search(identities.get(key, key))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if device_id is None:
|
||||
continue
|
||||
selected_devices.add(device_id)
|
||||
actions = sorted(grouped[device_id], key=lambda item: int(item["slot"]))
|
||||
activation = next(
|
||||
(
|
||||
item
|
||||
for item in actions
|
||||
if N7_PERIPHERAL_ACTIVATE_PATTERN.search(action_text(item))
|
||||
and N7_PERIPHERAL_OFF_PATTERN.search(action_text(item)) is None
|
||||
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item)) is None
|
||||
),
|
||||
None,
|
||||
)
|
||||
color = next(
|
||||
(
|
||||
item
|
||||
for pattern in color_patterns
|
||||
for item in actions
|
||||
if pattern.search(action_text(item))
|
||||
and N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if color is None:
|
||||
color = next(
|
||||
(
|
||||
item
|
||||
for item in actions
|
||||
if N7_PERIPHERAL_COLOR_PATTERN.search(action_text(item))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if activation is not None:
|
||||
result.append(
|
||||
as_scene_action(activation, category=category, kind="activate")
|
||||
)
|
||||
if color is not None and (
|
||||
activation is None or color.get("id") != activation.get("id")
|
||||
):
|
||||
result.append(as_scene_action(color, category=category, kind="color"))
|
||||
return result
|
||||
|
||||
def _augment_n7_cinema_scene(
|
||||
self,
|
||||
scene: dict[str, Any],
|
||||
) -> dict[str, object] | None:
|
||||
if not self._is_n7_cinema_scene(scene):
|
||||
return None
|
||||
base_scene = self._n7_cinema_base_scene(scene)
|
||||
if base_scene is None:
|
||||
return None
|
||||
base_actions = base_scene.get("actions", [])
|
||||
incoming_actions = scene.get("actions", [])
|
||||
inherited = self._merge_n7_scene_actions(base_actions, incoming_actions)
|
||||
peripheral_actions = self._n7_cinema_peripheral_actions(scene)
|
||||
augmented = self._merge_n7_scene_actions(inherited, peripheral_actions)
|
||||
if len(augmented) > 256:
|
||||
raise ValueError("影院相关模式合并后的动作数量超过 256 项。")
|
||||
scene["actions"] = augmented
|
||||
scene["operation_count"] = len(augmented)
|
||||
scene["domains"] = list(
|
||||
dict.fromkeys(str(action.get("domain") or "") for action in augmented)
|
||||
)
|
||||
return {
|
||||
"base_scene_id": str(base_scene.get("id") or ""),
|
||||
"base_action_count": len(base_actions) if isinstance(base_actions, list) else 0,
|
||||
"incoming_action_count": (
|
||||
len(incoming_actions) if isinstance(incoming_actions, list) else 0
|
||||
),
|
||||
"peripheral_action_count": len(peripheral_actions),
|
||||
"merged_action_count": len(augmented),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _imported_scene_function_configs(
|
||||
scene: dict[str, Any],
|
||||
@@ -694,6 +1103,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
"""Upsert one validated N7 card without publishing it back to the broker."""
|
||||
|
||||
document = deepcopy(dict(scene))
|
||||
cinema_augmentation = self._augment_n7_cinema_scene(document)
|
||||
actions = document.get("actions")
|
||||
if not isinstance(actions, list) or not actions:
|
||||
raise ValueError("from_n7/scenes card must contain executable actions")
|
||||
@@ -764,7 +1174,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
sync_origin=MQTT_MODE_SCENE_INGRESS_TOPIC,
|
||||
upstream_origin=str(document.get("origin") or ""),
|
||||
)
|
||||
return {
|
||||
result = {
|
||||
"status": "imported",
|
||||
"scene_id": saved.id,
|
||||
"scene_name": saved.name,
|
||||
@@ -772,6 +1182,9 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
"profile_revision": self.mode_profiles.profile_revision(saved),
|
||||
"storage": "persistent_yaml",
|
||||
}
|
||||
if cinema_augmentation is not None:
|
||||
result["cinema_augmentation"] = cinema_augmentation
|
||||
return result
|
||||
|
||||
@property
|
||||
def driver_only_ch1_execution_active(self) -> bool:
|
||||
@@ -1015,14 +1428,42 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
|
||||
requests = self.mode_profiles.requests_for_profiles([profile])
|
||||
if profile.sync_origin != MQTT_MODE_SCENE_INGRESS_TOPIC:
|
||||
return [
|
||||
request
|
||||
for operation, request in zip(profile.operations, requests, strict=True)
|
||||
if not (
|
||||
transfer_actions = self._local_cinema_n7_transfer_actions(
|
||||
profile.name,
|
||||
profile.instruction,
|
||||
)
|
||||
transfer_selectors = {
|
||||
self._n7_transfer_operation_selector(
|
||||
action.get("domain"),
|
||||
action.get("capability"),
|
||||
action.get("target_scope"),
|
||||
action.get("operation"),
|
||||
action.get("parameters"),
|
||||
)
|
||||
for action in transfer_actions
|
||||
}
|
||||
local_requests = []
|
||||
for operation, request in zip(
|
||||
profile.operations,
|
||||
requests,
|
||||
strict=True,
|
||||
):
|
||||
if (
|
||||
operation.domain == "self_extension"
|
||||
and operation.capability == "infrared.emit"
|
||||
):
|
||||
continue
|
||||
selector = self._n7_transfer_operation_selector(
|
||||
operation.domain,
|
||||
operation.capability,
|
||||
operation.target_scope,
|
||||
operation.operation,
|
||||
operation.parameters,
|
||||
)
|
||||
], []
|
||||
if selector in transfer_selectors:
|
||||
continue
|
||||
local_requests.append(request)
|
||||
return local_requests, transfer_actions
|
||||
source_scene = (
|
||||
profile.command_flow.get("source_scene")
|
||||
if isinstance(profile.command_flow, dict)
|
||||
@@ -1109,7 +1550,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
execution_mode: str,
|
||||
local_preview: bool,
|
||||
) -> dict[str, Any]:
|
||||
"""Forward only N7-originated actions never submitted by this PC."""
|
||||
"""Forward validated N7-only actions never submitted by this PC."""
|
||||
|
||||
actions = result.get("n7_transfer_candidates")
|
||||
scene = result.get("n7_source_scene")
|
||||
@@ -1579,6 +2020,48 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
raise ValueError("动态模式更新必须保留完整时间轴,请按阶段重新生成预览。")
|
||||
if schedule is not None:
|
||||
operations = [deepcopy(op) for phase in schedule.phases for op in phase.operations]
|
||||
n7_transfer_actions = (
|
||||
self._local_cinema_n7_transfer_actions(
|
||||
clean_name,
|
||||
clean_instruction,
|
||||
)
|
||||
if schedule is None
|
||||
else []
|
||||
)
|
||||
if n7_transfer_actions:
|
||||
command_flow["n7_transfer_actions"] = deepcopy(n7_transfer_actions)
|
||||
existing_operation_selectors = {
|
||||
self._n7_transfer_operation_selector(
|
||||
operation.get("domain"),
|
||||
operation.get("capability"),
|
||||
operation.get("target_scope"),
|
||||
operation.get("operation"),
|
||||
operation.get("parameters"),
|
||||
)
|
||||
for operation in operations
|
||||
if isinstance(operation, Mapping)
|
||||
}
|
||||
for action in n7_transfer_actions:
|
||||
selector = self._n7_transfer_operation_selector(
|
||||
action.get("domain"),
|
||||
action.get("capability"),
|
||||
action.get("target_scope"),
|
||||
action.get("operation"),
|
||||
action.get("parameters"),
|
||||
)
|
||||
if selector in existing_operation_selectors:
|
||||
continue
|
||||
operations.append(
|
||||
{
|
||||
"domain": str(action.get("domain") or ""),
|
||||
"capability": str(action.get("capability") or ""),
|
||||
"target_scope": str(action.get("target_scope") or ""),
|
||||
"operation": str(action.get("operation") or "launch"),
|
||||
"parameters": deepcopy(dict(action.get("parameters") or {})),
|
||||
"reason": "继承车机基础影院模式的视频媒体启动动作。",
|
||||
}
|
||||
)
|
||||
existing_operation_selectors.add(selector)
|
||||
saved = self.mode_profiles.save_mode(
|
||||
clean_name,
|
||||
operations,
|
||||
@@ -1843,6 +2326,16 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
|
||||
self._vehicle_mqtt_transport.set_extension_control_rejection_handler(handler)
|
||||
|
||||
def set_mode_scene_peripheral_catalog_provider(
|
||||
self,
|
||||
provider: Callable[[], list[dict[str, Any]]] | None,
|
||||
) -> None:
|
||||
"""Supply the durable NFC-authorized IR catalog for N7 scene imports."""
|
||||
|
||||
if provider is not None and not callable(provider):
|
||||
raise TypeError("mode scene peripheral catalog provider must be callable")
|
||||
self._mode_scene_peripheral_catalog_provider = provider or (lambda: [])
|
||||
|
||||
def _synchronize_vehicle_state_once(
|
||||
self,
|
||||
conversation_id: str,
|
||||
@@ -8372,6 +8865,11 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
and "按摩" in clauses[-1]
|
||||
and DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.search(fragment)
|
||||
)
|
||||
thermal_continuation = bool(
|
||||
clauses
|
||||
and re.search(r"通风|加热", clauses[-1])
|
||||
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(fragment)
|
||||
)
|
||||
# An exclusion is a qualifier of the control immediately before it,
|
||||
# not an independent command. Keeping both pieces together lets
|
||||
# the deterministic window parser remove the excluded targets
|
||||
@@ -8379,7 +8877,7 @@ class SeatDemoService(ModeScheduleServiceMixin):
|
||||
exclusion_qualifier = bool(
|
||||
clauses and re.search(r"除外|除去|不包括|不含", fragment)
|
||||
)
|
||||
if massage_continuation or exclusion_qualifier:
|
||||
if massage_continuation or thermal_continuation or exclusion_qualifier:
|
||||
clauses[-1] = f"{clauses[-1]},{fragment}"
|
||||
else:
|
||||
clauses.append(fragment)
|
||||
|
||||
Reference in New Issue
Block a user