updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
@@ -37,12 +37,14 @@ from ..const import (
MAX_TEXT_LENGTH,
MAX_TYPE_LENGTH,
MAX_URL_LENGTH,
NOTIFICATION_MANAGER_KEY,
HistoryEntryType,
)
from ..helpers.dates import INTERVAL_UNITS
from ..helpers.permissions import require_write
from ..helpers.schedule import (
FLAT_RECURRENCE_KEYS,
KIND_INTERVAL,
Schedule,
normalize_task_storage,
)
@@ -103,6 +105,8 @@ from .tasks_validation import (
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=256)), None),
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=32)), None),
# Spare parts consumed on completion: [{part_id, quantity}].
vol.Optional("consumes_parts"): vol.Any(list, None),
vol.Optional("priority"): vol.In(TASK_PRIORITIES),
vol.Optional("checklist"): vol.Any(
vol.All([vol.All(str, vol.Length(max=MAX_CHECKLIST_ITEM_LENGTH))], vol.Length(max=MAX_CHECKLIST_ITEMS)), None
@@ -163,7 +167,21 @@ async def ws_create_task(
# Recurrence: an explicit nested `schedule` (calendar kinds) takes
# precedence; otherwise build from the flat v2.6.x fields.
if msg.get("schedule"):
task_data["schedule"] = Schedule.from_dict(msg["schedule"]).to_dict()
incoming = Schedule.from_dict(msg["schedule"]).to_dict()
task_data["schedule"] = incoming
# A BARE interval schedule (kind=interval, no `every`) is only the
# carrier for the season/ends extras the panel always sends for a
# time-based task — the real interval still rides the flat
# interval_days / interval_unit / interval_anchor fields. Carry them
# over so normalize_task_storage merges them onto the schedule; without
# this the interval was dropped entirely (#88 regression).
if incoming.get("kind") == KIND_INTERVAL and incoming.get("every") is None:
if msg.get("interval_days") is not None:
task_data["interval_days"] = msg["interval_days"]
if msg.get("interval_unit", "days") != "days":
task_data["interval_unit"] = msg["interval_unit"]
if msg.get("interval_anchor", "completion") != "completion":
task_data["interval_anchor"] = msg["interval_anchor"]
else:
task_data["schedule_type"] = msg.get("schedule_type", "time_based")
if msg.get("interval_days") is not None:
@@ -248,6 +266,15 @@ async def ws_create_task(
# 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
if msg.get("consumes_parts") is not None:
from ..const import CONF_PARTS
from ..helpers.parts import sanitize_consumes_parts
links = sanitize_consumes_parts(
msg["consumes_parts"], set(entry.data.get(CONF_PARTS) or {})
)
if links:
task_data["consumes_parts"] = links
if msg.get("checklist"):
task_data["checklist"] = msg["checklist"]
if msg.get("labels"):
@@ -276,13 +303,17 @@ async def ws_create_task(
connection.send_result(msg["id"], result)
return
await async_persist_task(
hass,
entry,
task_data,
last_performed=initial_last_performed,
history=initial_history,
)
try:
await async_persist_task(
hass,
entry,
task_data,
last_performed=initial_last_performed,
history=initial_history,
)
except ValueError as err:
connection.send_error(msg["id"], "limit_reached", str(err))
return
result = {"task_id": task_id}
if tc_warnings:
@@ -325,6 +356,8 @@ async def ws_create_task(
vol.Optional("nfc_tag_id"): vol.Any(vol.All(str, vol.Length(max=256)), None),
# v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...).
vol.Optional("reading_unit"): vol.Any(vol.All(str, vol.Length(max=32)), None),
# Spare parts consumed on completion: [{part_id, quantity}].
vol.Optional("consumes_parts"): vol.Any(list, None),
vol.Optional("priority"): vol.In(TASK_PRIORITIES),
vol.Optional("checklist"): vol.Any(
vol.All([vol.All(str, vol.Length(max=MAX_CHECKLIST_ITEM_LENGTH))], vol.Length(max=MAX_CHECKLIST_ITEMS)), None
@@ -441,6 +474,7 @@ async def ws_update_task(
"custom_icon": "custom_icon",
"nfc_tag_id": "nfc_tag_id",
"reading_unit": "reading_unit",
"consumes_parts": "consumes_parts",
"priority": "priority",
"checklist": "checklist",
"labels": "labels",
@@ -464,9 +498,20 @@ async def ws_update_task(
or msg.get("schedule_type") in FLAT_SCHEDULE_TYPES
)
if msg.get("schedule"):
for key in FLAT_RECURRENCE_KEYS:
task.pop(key, None)
task["schedule"] = Schedule.from_dict(msg["schedule"]).to_dict()
incoming = Schedule.from_dict(msg["schedule"]).to_dict()
task["schedule"] = incoming
# A COMPLETE nested schedule is authoritative — drop the flat recurrence
# keys so it wins. But a BARE interval schedule (kind=interval with no
# `every`) is only the carrier for the season/ends extras the panel
# always sends for a time-based task; the real interval still rides the
# flat interval_days / interval_unit / interval_anchor fields, which
# normalize_task_storage merges onto it. Popping them for the bare
# interval dropped the interval entirely (#88 regression from the
# season/ends work). Keep them in that one case.
bare_interval = incoming.get("kind") == KIND_INTERVAL and incoming.get("every") is None
if not bare_interval:
for key in FLAT_RECURRENCE_KEYS:
task.pop(key, None)
elif _flat_recurrence_edit:
task.pop("schedule", None)
elif "schedule_type" in msg:
@@ -567,6 +612,11 @@ async def async_delete_task(
return False
old_trigger_config = new_tasks[task_id].get("trigger_config")
# Adopted problem-sensor task? Preserve its notes for a later re-adopt
# (no-op for everything else).
from ..helpers.problem_sensors import stash_task_notes_for_readopt
stash_task_notes_for_readopt(hass, new_tasks[task_id])
del new_tasks[task_id]
new_data[CONF_TASKS] = new_tasks
@@ -585,7 +635,7 @@ async def async_delete_task(
await store.async_save()
# Clean up notification state for deleted task
nm = hass.data.get(DOMAIN, {}).get("_notification_manager")
nm = hass.data.get(DOMAIN, {}).get(NOTIFICATION_MANAGER_KEY)
if nm is not None:
nm.clear_task_state(entry.entry_id, task_id)