361 files
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import date
|
||||
from typing import Any, Final
|
||||
|
||||
@@ -345,6 +345,22 @@ def _strip_empty(d: dict[str, Any]) -> dict[str, Any]:
|
||||
return {k: v for k, v in d.items() if not (v is None or v in (_EMPTY_LIST, _EMPTY_DICT))}
|
||||
|
||||
|
||||
def _place_fields(hass: HomeAssistant, obj_data: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""2026-10 places, for the object's payload: ``place`` (the zone, None =
|
||||
home), ``place_name``, ``place_missing`` (set but the zone is gone) and
|
||||
``remind_on_site``."""
|
||||
from ..helpers.places import object_place, zone_name
|
||||
|
||||
place = object_place(obj_data)
|
||||
name = zone_name(hass, place)
|
||||
return {
|
||||
"place": place,
|
||||
"place_name": name,
|
||||
"place_missing": place is not None and name is None,
|
||||
"remind_on_site": bool(place) and obj_data.get("remind_on_site") is True,
|
||||
}
|
||||
|
||||
|
||||
def _build_object_response(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
@@ -412,6 +428,10 @@ def _build_object_response(
|
||||
"warranty_expiry": obj_data.get("warranty_expiry"),
|
||||
"ha_device_id": obj_data.get("ha_device_id"),
|
||||
"parent_entry_id": obj_data.get("parent_entry_id"),
|
||||
# 2026-10 places: the zone (None = home), its name, whether the
|
||||
# zone is gone (the object then behaves as at home), and whether
|
||||
# its reminders wait for somebody on site.
|
||||
**_place_fields(hass, obj_data),
|
||||
# v1.4.0 (#43): expose to the frontend so the manual link
|
||||
# renders in the object detail header AND, since v1.4.1, on
|
||||
# every task detail page belonging to this object.
|
||||
|
||||
@@ -25,6 +25,7 @@ from ..const import (
|
||||
CONF_ADVANCED_COMPLETION_ACTIONS,
|
||||
CONF_ADVANCED_ENVIRONMENTAL,
|
||||
CONF_ADVANCED_GROUPS,
|
||||
CONF_ADVANCED_PLACES,
|
||||
CONF_ADVANCED_SCHEDULE_TIME,
|
||||
CONF_ADVANCED_SEASONAL,
|
||||
CONF_ARCHIVE_ONEOFF_DAYS,
|
||||
@@ -215,6 +216,7 @@ def _build_full_settings(
|
||||
"checklists": _opt(options, CONF_ADVANCED_CHECKLISTS),
|
||||
"schedule_time": _opt(options, CONF_ADVANCED_SCHEDULE_TIME),
|
||||
"completion_actions": _opt(options, CONF_ADVANCED_COMPLETION_ACTIONS),
|
||||
"places": _opt(options, CONF_ADVANCED_PLACES),
|
||||
},
|
||||
# Top-level (not a feature toggle, not a bool): list of HA user IDs
|
||||
# whose UI gets the full admin panel even though they're not HA admins.
|
||||
|
||||
@@ -1049,6 +1049,9 @@ async def ws_import_json(
|
||||
# An id is looked up (device area, area filters): a list here made
|
||||
# every entity of the object fail to be added (bug audit 2026-09-29).
|
||||
"area_id": obj_data.get("area_id") if isinstance(obj_data.get("area_id"), str) else None,
|
||||
# 2026-10 places: normalized (zone id or nothing) by cap_object_fields.
|
||||
"place": obj_data.get("place") if isinstance(obj_data.get("place"), str) else None,
|
||||
"remind_on_site": obj_data.get("remind_on_site") is True,
|
||||
"installation_date": obj_data.get("installation_date"),
|
||||
"warranty_expiry": obj_data.get("warranty_expiry"),
|
||||
# Imported counterparts of the export fields above; length-capped by
|
||||
|
||||
@@ -20,6 +20,8 @@ from ..const import (
|
||||
CONF_OBJECT_MODEL,
|
||||
CONF_OBJECT_NAME,
|
||||
CONF_OBJECT_NOTES,
|
||||
CONF_OBJECT_PLACE,
|
||||
CONF_OBJECT_REMIND_ON_SITE,
|
||||
CONF_OBJECT_SERIAL_NUMBER,
|
||||
CONF_OBJECT_WARRANTY_EXPIRY,
|
||||
CONF_TASKS,
|
||||
@@ -55,6 +57,8 @@ from .tasks import ( # v1.4.0 (#43): reuse the existing URL safety check
|
||||
# which is also applied on persist via cap_object_fields as a safety net.
|
||||
_OBJECT_STR_FIELD_SCHEMA: dict[Any, Any] = {
|
||||
vol.Optional("area_id"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
# 2026-10 places: an HA zone's entity id; null / "" / "zone.home" = home.
|
||||
vol.Optional("place"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
vol.Optional("manufacturer"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
vol.Optional("model"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
vol.Optional("serial_number"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
@@ -248,6 +252,8 @@ async def async_create_object(
|
||||
notes: str | None = None,
|
||||
ha_device_id: str | None = None,
|
||||
parent_entry_id: str | None = None,
|
||||
place: str | None = None,
|
||||
remind_on_site: bool = False,
|
||||
) -> str:
|
||||
"""Create a maintenance object (config entry) and return its entry_id.
|
||||
|
||||
@@ -282,6 +288,9 @@ async def async_create_object(
|
||||
CONF_OBJECT_NOTES: (notes.strip() if isinstance(notes, str) and notes.strip() else None),
|
||||
"ha_device_id": ha_device_id,
|
||||
"parent_entry_id": parent_entry_id,
|
||||
# Normalized by cap_object_fields (helpers/sanitize._normalize_place).
|
||||
**({CONF_OBJECT_PLACE: place} if place else {}),
|
||||
**({CONF_OBJECT_REMIND_ON_SITE: True} if remind_on_site else {}),
|
||||
"task_ids": [],
|
||||
},
|
||||
CONF_TASKS: {},
|
||||
@@ -297,6 +306,7 @@ async def async_create_object(
|
||||
vol.Required("type"): "maintenance_supporter/object/create",
|
||||
vol.Required("name"): vol.All(str, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
**_OBJECT_STR_FIELD_SCHEMA,
|
||||
vol.Optional("remind_on_site"): bool,
|
||||
vol.Optional("dry_run", default=False): bool,
|
||||
}
|
||||
)
|
||||
@@ -356,6 +366,8 @@ async def ws_create_object(
|
||||
notes=notes,
|
||||
ha_device_id=msg.get("ha_device_id"),
|
||||
parent_entry_id=msg.get("parent_entry_id"),
|
||||
place=msg.get("place"),
|
||||
remind_on_site=msg.get("remind_on_site") is True,
|
||||
)
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], "create_failed", str(err))
|
||||
@@ -369,6 +381,7 @@ async def ws_create_object(
|
||||
vol.Required("entry_id"): ID_FIELD,
|
||||
vol.Optional("name"): vol.All(str, vol.Length(min=1, max=MAX_NAME_LENGTH)),
|
||||
**_OBJECT_STR_FIELD_SCHEMA,
|
||||
vol.Optional("remind_on_site"): bool,
|
||||
}
|
||||
)
|
||||
@require_write
|
||||
@@ -444,6 +457,10 @@ async def ws_update_object(
|
||||
obj[CONF_OBJECT_NAME] = msg["name"]
|
||||
if "area_id" in msg:
|
||||
obj[CONF_OBJECT_AREA] = msg["area_id"]
|
||||
if "place" in msg:
|
||||
obj[CONF_OBJECT_PLACE] = msg["place"]
|
||||
if "remind_on_site" in msg:
|
||||
obj[CONF_OBJECT_REMIND_ON_SITE] = msg["remind_on_site"]
|
||||
if "manufacturer" in msg:
|
||||
obj[CONF_OBJECT_MANUFACTURER] = msg["manufacturer"]
|
||||
if "model" in msg:
|
||||
|
||||
@@ -879,17 +879,14 @@ async def ws_update_task(
|
||||
|
||||
# Clear stale trigger runtime in Store only when trigger fundamentally changes
|
||||
if "trigger_config" in msg:
|
||||
old_tc = stored_task.get("trigger_config") or {}
|
||||
new_tc = msg["trigger_config"] or {}
|
||||
from ..entity.triggers import trigger_runtime_stale
|
||||
|
||||
# An edited baseline counts as fundamental: the Store baseline wins
|
||||
# over the config on restore (#102 restart fix), so without clearing
|
||||
# it a user-entered start value would silently never take effect.
|
||||
if (
|
||||
old_tc.get("type") != new_tc.get("type")
|
||||
or old_tc.get("entity_id") != new_tc.get("entity_id")
|
||||
or old_tc.get("entity_ids") != new_tc.get("entity_ids")
|
||||
or old_tc.get("trigger_baseline_value") != new_tc.get("trigger_baseline_value")
|
||||
):
|
||||
# Compared normalized (trigger_runtime_stale): a task stored in the
|
||||
# legacy form lost its runtime on a target-only edit.
|
||||
if trigger_runtime_stale(stored_task.get("trigger_config"), msg["trigger_config"]):
|
||||
store_tc = get_store(hass, entry.entry_id)
|
||||
if store_tc is not None:
|
||||
store_tc.clear_trigger_runtime(task_id)
|
||||
|
||||
Reference in New Issue
Block a user