feat: add seat firmware workbench and OTA delivery

This commit is contained in:
vee
2026-09-16 17:55:30 +08:00
parent 3eeca2ea75
commit ab8e43050c
70 changed files with 22594 additions and 316 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
"""Conservative, local defaults for adding named devices to an existing mode.
Only a complete *bare device list* uses these defaults. Explicit parameters and
actions stay in the normal parser; unrecognised list entries never disappear.
Defaults are proposals for a fully validated preview, not executable commands.
"""
from __future__ import annotations
import re
from copy import deepcopy
from typing import Any
_ADD_PREFIX = re.compile(
r"^(?:(?:请|帮我|帮忙|再|还|也|同时|顺便|继续|并且|并)\s*)*"
r"(?:加入|添加|增加|补充|加上|追加|新增)\s*"
)
_DEVICE_GROUPS = {
"空调": "climate", "冷气": "climate",
"氛围灯": "ambient_light", "气氛灯": "ambient_light",
"阅读灯": "reading", "读书灯": "reading",
"车窗": "window", "窗户": "window",
"遮阳帘": "roof", "天幕遮阳帘": "roof", "天窗遮阳帘": "roof",
"室内灯": "indoor", "顶灯": "indoor",
}
_LIST_SEPARATOR = re.compile(r"[、,,;;\s]+|以及|还有|和|与|及")
_EXPLICIT_ACTION = re.compile(
r"\d|[零一二三四五六七八九十百]+(?:度|档|挡|秒|分钟|%)|"
r"打开|开启|关闭|关掉|开到|关到|调整|设置|调到|调成|变成|改成|"
r"升高|降低|调高|调低|调亮|调暗|全开|全关|半开|一半|"
r"红色|蓝色|绿色|白色|暖色|冷色|亮度|温度|风量|内循环|外循环"
)
def strip_mode_addition_prefix(change_text: str) -> str:
"""Remove only the editing verb, not any device, value or negation."""
return _ADD_PREFIX.sub("", change_text.strip(), count=1)
def mode_device_additions(
change_text: str,
base_operations: list[dict[str, Any]],
default_operations: list[dict[str, Any]],
) -> list[dict[str, Any]] | None:
"""Return missing device defaults, None for non-list/explicit edits.
Existing groups are already configured and are left completely untouched,
including their target zones, power state and custom parameter values.
Reading and indoor lights are separate groups within the same domain.
"""
if not _ADD_PREFIX.match(change_text.strip()):
return None
body = strip_mode_addition_prefix(change_text).strip("。.!!吧 ")
if _EXPLICIT_ACTION.search(body):
return None
body = re.sub(r"(?:等)?(?:相关)?(?:设备)?(?:的)?(?:控制)?(?:动作|能力|联动)?$", "", body)
body = re.sub(r"等$", "", body)
devices = [item for item in _LIST_SEPARATOR.split(body) if item]
groups: list[str] = []
for device in devices:
device = re.sub(r"(?:设备)?(?:的)?(?:控制)?(?:动作|能力)$", "", device)
group = _DEVICE_GROUPS.get(device)
if group is None:
raise ValueError(f"未识别或暂不支持增补设备“{device}”;未修改任何模式动作。")
if group not in groups:
groups.append(group)
if not groups:
raise ValueError("请说明要加入哪些设备,或提供明确的设备动作。")
def belongs(operation: dict[str, Any], group: str) -> bool:
capability = str(operation.get("capability") or "")
if group == "reading":
return capability == "interior_light.set_reading_power"
if group == "indoor":
return capability == "interior_light.set_indoor_power"
return operation.get("domain") == group
additions: list[dict[str, Any]] = []
for group in groups:
if any(belongs(operation, group) for operation in base_operations):
continue
candidates = [operation for operation in default_operations if belongs(operation, group)]
if not candidates:
raise ValueError(f"该设备尚无经过登记的默认编排,请补充明确目标:{group}。")
additions.extend(deepcopy(candidates))
return additions
@@ -170,11 +170,12 @@ _MODE_PROFILE_DELETE_BA = re.compile(
)
_MODE_PROFILE_UPDATE_NEGATION = re.compile(
r"不要|别(?:再)?|不用|无需|不必|禁止|不许|不得|"
r"取消(?:修改|调整|更改|更新|变更|操作)|不执行|不下发|不保存"
r"取消(?:修改|调整|更改|更新|变更|加入|添加|增加|补充|加上|追加|新增|操作)|"
r"撤销|算了|不执行|不下发|不保存"
)
_MODE_PROFILE_UPDATE_PREFIX_NEGATION = re.compile(
r"^(?:请|麻烦)?(?:先|暂时)?(?:不要|别|不用|无需|不必|禁止|不许|不得)"
r"(?:把|将|修改|调整|更改|更新|变更)"
r"(?:把|将|在|给|为|往|向|修改|调整|更改|更新|变更)"
)
_MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)"
_MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])"
@@ -197,7 +198,19 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
rf"^{_LEADING_POLITE}{_QUOTED_MODE_NAME}[,,::]"
r"(?P<change>.+?)[。.!!]?$"
)
_MODE_PROFILE_APPEND_VERB = r"(?:加入|添加|增加|补充|加上|追加|新增)"
_APPEND_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:在|给|为|往|向)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?[,,::]?"
rf"(?:再|还|也|同时|另外)?(?P<change>{_MODE_PROFILE_APPEND_VERB}.+?)[。.!!]?$"
)
_APPEND_TO_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:把|将)(?P<change>.+?)"
rf"(?P<append_verb>{_MODE_PROFILE_APPEND_VERB})(?:到|进)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?(?:吧|呀|啊|哦|谢谢(?:你)?)?[。.!!]?$"
)
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
rf"{_MODE_PROFILE_APPEND_VERB}|"
r"修改|改成|改为|更改|更新|变更|变成|变为|换成|"
r"调整|调节|调高|调低|调到|调成|调为|调至|"
r"设置|设为|设成|设到|设至|"
@@ -208,7 +221,7 @@ MODE_PROFILE_CHANGE_PREDICATE = re.compile(
r"(?:重复|循环)(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)次"
)
MODE_PROFILE_QUERY_CUE = re.compile(
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|能否|能不能|可不可以|会不会|"
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$"
@@ -610,6 +623,8 @@ def _candidate_matches(
) -> list[tuple[re.Match[str], bool]]:
matches: list[tuple[re.Match[str], bool]] = []
for pattern, predicate_required in (
(_APPEND_MODE_UPDATE, True),
(_APPEND_TO_MODE_UPDATE, False),
(_PREFIX_UPDATE, False),
(_BA_UPDATE, True),
(_NAME_FIRST_UPDATE, True),
@@ -629,6 +644,14 @@ def _candidate_matches(
return matches
def _mode_profile_update_change(match: re.Match[str], name: str) -> str:
change = _strip_repeated_mode_update_subject(str(match.group("change") or ""), name)
append_verb = str(match.groupdict().get("append_verb") or "")
# Keep reverse-order additions on the same service contract as name-first
# additions. Device names, explicit targets and parameters remain unchanged.
return f"{append_verb}{change}" if append_verb else change
def _strip_repeated_mode_update_subject(change: str, mode_name: str) -> str:
"""Remove a repeated ``把<same mode>的`` wrapper from an update body."""
@@ -708,10 +731,7 @@ def is_mode_profile_update_shape(
and _is_unquoted_device_property_mode(match, name)
):
continue
change = _strip_repeated_mode_update_subject(
str(match.group("change") or ""),
name,
)
change = _mode_profile_update_change(match, name)
if change and (
not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change)
):
@@ -746,10 +766,9 @@ def parse_mode_profile_update(
and _is_unquoted_device_property_mode(match, name)
):
continue
change = _strip_repeated_mode_update_subject(
str(match.group("change") or ""),
name,
)
change = _mode_profile_update_change(match, name)
if re.fullmatch(rf"(?:再|还|也|同时|另外)?{_MODE_PROFILE_APPEND_VERB}(?:一下)?", change):
continue
if (
(not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change))
and not _MODE_PROFILE_UPDATE_NEGATION.search(change)