278 files

This commit is contained in:
Home Assistant Version Control
2026-09-13 20:38:58 +00:00
parent d7a0a08372
commit 1b4f5f68c6
278 changed files with 36892 additions and 7150 deletions
@@ -24,7 +24,6 @@ from ..const import (
CONF_OBJECT_WARRANTY_EXPIRY,
CONF_TASKS,
DOMAIN,
GLOBAL_UNIQUE_ID,
MAX_DATE_LENGTH,
MAX_ENTITY_ID_LENGTH,
MAX_ID_LENGTH,
@@ -33,14 +32,15 @@ from ..const import (
MAX_TEXT_LENGTH,
MAX_URL_LENGTH,
)
from ..helpers.aggregate import get_coordinator_data, get_store, is_object_entry
from ..helpers.pause import reanchor_recurring_task
from ..helpers.permissions import require_write
from ..helpers.sanitize import cap_object_fields, strip_object_reference, strip_task_runtime_state
from . import (
_build_object_response,
_get_object_entries,
_get_runtime_data,
_load_object_entry,
_parse_iso_date,
cleanup_group_refs,
)
from .tasks import ( # v1.4.0 (#43): reuse the existing URL safety check
@@ -70,6 +70,15 @@ _OBJECT_STR_FIELD_SCHEMA: dict[Any, Any] = {
}
def _validate_object_dates(connection: websocket_api.ActiveConnection, msg: dict[str, Any]) -> bool:
"""False (after sending ``invalid_date``) when a present installation_date
/ warranty_expiry (#67) is not ``YYYY-MM-DD`` — shared by create and update."""
for field in ("installation_date", "warranty_expiry"):
if msg.get(field) and _parse_iso_date(connection, msg["id"], msg[field], field=field) is None:
return False
return True
def _validate_device_link(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
@@ -108,7 +117,7 @@ def _validate_device_link(
if parent_id := msg.get("parent_entry_id"):
parent = hass.config_entries.async_get_entry(parent_id)
if parent is None or parent.domain != DOMAIN or parent.unique_id == GLOBAL_UNIQUE_ID:
if not is_object_entry(parent):
connection.send_error(msg["id"], "invalid_parent", f"No maintenance object {parent_id!r}")
return False
if self_entry_id is not None:
@@ -149,8 +158,7 @@ async def ws_get_objects(
entries = _get_object_entries(hass)
result = []
for entry in entries:
rd = _get_runtime_data(hass, entry.entry_id)
coord_data = rd.coordinator.data if rd and rd.coordinator else None
coord_data = get_coordinator_data(hass, entry.entry_id)
result.append(_build_object_response(hass, entry, coord_data, compact=compact))
connection.send_result(msg["id"], {"objects": result})
@@ -173,8 +181,7 @@ async def ws_get_object(
if entry is None:
return
rd = _get_runtime_data(hass, entry.entry_id)
coord_data = rd.coordinator.data if rd and rd.coordinator else None
coord_data = get_coordinator_data(hass, entry.entry_id)
connection.send_result(msg["id"], _build_object_response(hass, entry, coord_data))
@@ -249,27 +256,11 @@ async def ws_create_object(
model = (msg.get("model") or "").strip() or None
serial_number = (msg.get("serial_number") or "").strip() or None
# Validate installation_date format if provided
# Validate installation_date / warranty_expiry (#67) format if provided
if not _validate_object_dates(connection, msg):
return
installation_date = msg.get("installation_date")
if installation_date:
from datetime import date as date_cls
try:
date_cls.fromisoformat(installation_date)
except ValueError:
connection.send_error(msg["id"], "invalid_date", "Invalid installation_date format (expected YYYY-MM-DD)")
return
# (#67): validate warranty_expiry format if provided
warranty_expiry = msg.get("warranty_expiry")
if warranty_expiry:
from datetime import date as date_cls
try:
date_cls.fromisoformat(warranty_expiry)
except ValueError:
connection.send_error(msg["id"], "invalid_date", "Invalid warranty_expiry format (expected YYYY-MM-DD)")
return
# v1.4.0 (#43): documentation_url
documentation_url = (msg.get("documentation_url") or "").strip() or None
@@ -346,25 +337,9 @@ async def ws_update_object(
if msg.get("serial_number"):
msg["serial_number"] = msg["serial_number"].strip() or None
# Validate installation_date format if provided
if msg.get("installation_date"):
from datetime import date as date_cls
try:
date_cls.fromisoformat(msg["installation_date"])
except ValueError:
connection.send_error(msg["id"], "invalid_date", "Invalid installation_date format (expected YYYY-MM-DD)")
return
# (#67): validate warranty_expiry format if provided
if msg.get("warranty_expiry"):
from datetime import date as date_cls
try:
date_cls.fromisoformat(msg["warranty_expiry"])
except ValueError:
connection.send_error(msg["id"], "invalid_date", "Invalid warranty_expiry format (expected YYYY-MM-DD)")
return
# Validate installation_date / warranty_expiry (#67) format if provided
if not _validate_object_dates(connection, msg):
return
# v1.4.0 (#43): documentation_url
if "documentation_url" in msg:
@@ -693,13 +668,11 @@ async def ws_unarchive_object(
return
obj.pop("archived_at", None)
rd = _get_runtime_data(hass, entry.entry_id)
store = getattr(rd, "store", None) if rd else None
store = get_store(hass, entry.entry_id)
today_iso = dt_util.now().date().isoformat()
tasks_data = dict(entry.data.get(CONF_TASKS, {}))
new_tasks: dict[str, Any] = {}
for tid, td in tasks_data.items():
for tid, td in entry.data.get(CONF_TASKS, {}).items():
td = dict(td)
if td.get("archived_reason") == ARCHIVE_REASON_OBJECT:
td.pop("archived_at", None)
@@ -744,8 +717,6 @@ async def ws_pause_object(
on that day via the coordinator; without it the pause holds until an
explicit ``object/resume``.
"""
from datetime import date as date_cls
entry = _load_object_entry(hass, connection, msg)
if entry is None:
return
@@ -760,10 +731,8 @@ async def ws_pause_object(
until = msg.get("until")
if until:
try:
until_date = date_cls.fromisoformat(until)
except ValueError:
connection.send_error(msg["id"], "invalid_date", "Invalid until format (expected YYYY-MM-DD)")
until_date = _parse_iso_date(connection, msg["id"], until, field="until")
if until_date is None:
return
if until_date <= dt_util.now().date():
connection.send_error(msg["id"], "invalid_date", "until must be a future date")
@@ -815,8 +784,7 @@ async def ws_resume_object(
connection.send_error(msg["id"], "not_paused", "Object is not paused")
return
rd = _get_runtime_data(hass, entry.entry_id)
store = getattr(rd, "store", None) if rd else None
store = get_store(hass, entry.entry_id)
new_data = build_resumed_entry_data(dict(entry.data), store, dt_util.now().date().isoformat())
hass.config_entries.async_update_entry(entry, data=new_data)
if store is not None:
@@ -949,11 +917,9 @@ async def ws_replace_object(
# Copy the tracked stock counts (dynamic store state) onto the carried
# parts, then let the successor's reconcile recreate any needed reminder.
if part_id_map:
src_rd = getattr(entry, "runtime_data", None)
src_store = getattr(src_rd, "store", None) if src_rd else None
src_store = get_store(hass, entry.entry_id)
new_entry = hass.config_entries.async_get_entry(new_entry_id)
new_rd = getattr(new_entry, "runtime_data", None) if new_entry else None
new_store = getattr(new_rd, "store", None) if new_rd else None
new_store = get_store(hass, new_entry_id)
if src_store is not None and new_store is not None:
for old_pid, new_pid in part_id_map.items():
stock = src_store.get_part_stock(old_pid)