184 files
This commit is contained in:
@@ -156,6 +156,8 @@ def _build_task_summary(
|
||||
"custom_icon": task_data.get("custom_icon"),
|
||||
"nfc_tag_id": task_data.get("nfc_tag_id"),
|
||||
"require_tag_scan": bool(task_data.get("require_tag_scan", False)),
|
||||
# #150: skip lock — False disables skipping on every surface.
|
||||
"allow_skip": task_data.get("allow_skip") is not False,
|
||||
# v2.20 (#83): unit for `reading`-type tasks; values live in history.
|
||||
"reading_unit": task_data.get("reading_unit"),
|
||||
# Spare parts: consumption links ([{part_id, quantity}]) and, on an
|
||||
|
||||
@@ -714,6 +714,7 @@ async def ws_import_json(
|
||||
"custom_icon",
|
||||
"nfc_tag_id",
|
||||
"require_tag_scan",
|
||||
"allow_skip",
|
||||
"responsible_user_id",
|
||||
"entity_slug",
|
||||
"trigger_config",
|
||||
|
||||
@@ -313,11 +313,16 @@ async def ws_skip_task(
|
||||
return
|
||||
rd, _entry = ctx
|
||||
|
||||
await rd.coordinator.skip_maintenance(
|
||||
task_id=msg["task_id"],
|
||||
reason=msg.get("reason"),
|
||||
as_missed=msg.get("as_missed", False),
|
||||
)
|
||||
try:
|
||||
await rd.coordinator.skip_maintenance(
|
||||
task_id=msg["task_id"],
|
||||
reason=msg.get("reason"),
|
||||
as_missed=msg.get("as_missed", False),
|
||||
)
|
||||
except ServiceValidationError as err:
|
||||
# #150: the task carries a skip lock.
|
||||
connection.send_error(msg["id"], "skip_disabled", str(err))
|
||||
return
|
||||
connection.send_result(msg["id"], {"success": True})
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
"custom_icon": "custom_icon",
|
||||
"nfc_tag_id": "nfc_tag_id",
|
||||
"require_tag_scan": "require_tag_scan",
|
||||
"allow_skip": "allow_skip",
|
||||
"reading_unit": "reading_unit",
|
||||
"consumes_parts": "consumes_parts",
|
||||
"phases": "phases",
|
||||
@@ -195,6 +196,9 @@ _TASK_CREATE_SCHEMA: dict[Any, Any] = {
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=MAX_NFC_TAG_LENGTH)), None),
|
||||
# Proof of presence (#139 family): completion only via NFC/QR scan.
|
||||
vol.Optional("require_tag_scan"): vol.Any(bool, None),
|
||||
# #150: per-task skip lock — false hides Skip in the UIs and the
|
||||
# coordinator refuses (WS + voice), so automations cannot skip either.
|
||||
vol.Optional("allow_skip"): vol.Any(bool, None),
|
||||
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=MAX_READING_UNIT_LENGTH)), None),
|
||||
# Spare parts consumed on completion: [{part_id, quantity}].
|
||||
@@ -372,6 +376,9 @@ async def ws_create_task(
|
||||
tc_warnings.append(nfc_warn)
|
||||
if msg.get("require_tag_scan") is not None:
|
||||
task_data["require_tag_scan"] = bool(msg["require_tag_scan"])
|
||||
# #150: stored only when False — absence means skipping is allowed.
|
||||
if msg.get("allow_skip") is False:
|
||||
task_data["allow_skip"] = False
|
||||
# v2.20 (#83): unit for `reading`-type tasks.
|
||||
if msg.get("reading_unit") is not None:
|
||||
task_data["reading_unit"] = (msg["reading_unit"] or "").strip() or None
|
||||
@@ -472,6 +479,9 @@ _TASK_UPDATE_SCHEMA: dict[Any, Any] = {
|
||||
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=MAX_NFC_TAG_LENGTH)), None),
|
||||
# Proof of presence (#139 family): completion only via NFC/QR scan.
|
||||
vol.Optional("require_tag_scan"): vol.Any(bool, None),
|
||||
# #150: per-task skip lock — false hides Skip in the UIs and the
|
||||
# coordinator refuses (WS + voice), so automations cannot skip either.
|
||||
vol.Optional("allow_skip"): vol.Any(bool, None),
|
||||
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
|
||||
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=MAX_READING_UNIT_LENGTH)), None),
|
||||
# Spare parts consumed on completion: [{part_id, quantity}].
|
||||
@@ -582,6 +592,14 @@ async def ws_update_task(
|
||||
if msg_key in msg:
|
||||
task[data_key] = msg[msg_key]
|
||||
|
||||
# #150: allow_skip is stored only when False (absence = allowed) — the
|
||||
# verbatim copy above would persist True/None literals.
|
||||
if "allow_skip" in msg:
|
||||
if msg["allow_skip"] is False:
|
||||
task["allow_skip"] = False
|
||||
else:
|
||||
task.pop("allow_skip", None)
|
||||
|
||||
# The loop above copies values verbatim, which is wrong for part links:
|
||||
# `task/create` validates them and `task/update` did not, so an edit could
|
||||
# persist a link to a part — or, since #111, to an OBJECT — that does not
|
||||
|
||||
@@ -113,6 +113,7 @@ _TRIGGER_ALLOWED_KEYS: set[str] = {
|
||||
# runtime
|
||||
"trigger_runtime_hours",
|
||||
"trigger_on_states",
|
||||
"trigger_runtime_max_session_seconds",
|
||||
# state_change
|
||||
"trigger_from_state",
|
||||
"trigger_to_state",
|
||||
@@ -322,6 +323,18 @@ def _validate_trigger_values(trigger_type: str, trigger_config: dict[str, Any],
|
||||
elif isinstance(raw, str):
|
||||
trigger_config["trigger_runtime_hours"] = num
|
||||
|
||||
# #149: per-session cap against stuck sensors — whole seconds, 1..86400.
|
||||
raw = trigger_config.get("trigger_runtime_max_session_seconds")
|
||||
if raw is not None:
|
||||
num = _coerce_number(raw)
|
||||
if num is None or not num.is_integer() or not 1 <= num <= 86400:
|
||||
errors.append(
|
||||
f"trigger_config.trigger_runtime_max_session_seconds must be a whole number "
|
||||
f"between 1 and 86400 (seconds), got {raw!r}"
|
||||
)
|
||||
else:
|
||||
trigger_config["trigger_runtime_max_session_seconds"] = int(num)
|
||||
|
||||
raw = trigger_config.get("trigger_delta_mode")
|
||||
if raw is not None and not isinstance(raw, bool):
|
||||
errors.append(f"trigger_config.trigger_delta_mode must be true or false, got {raw!r}")
|
||||
|
||||
Reference in New Issue
Block a user