331 files

This commit is contained in:
Home Assistant Version Control
2026-10-04 05:26:42 +00:00
parent 22ba8b4648
commit c4d601bfba
331 changed files with 10227 additions and 50159 deletions
@@ -298,6 +298,8 @@ def _build_task_summary(
"trigger_baseline_value": ct.get("_trigger_baseline_value"),
"times_performed": ct.get("_times_performed", 0),
"total_cost": ct.get("_total_cost", 0.0),
# #200: per completion, credits left out (total_cost nets them).
"average_cost": ct.get("_average_cost"),
"average_duration": ct.get("_average_duration"),
# Adaptive scheduling
"adaptive_config": task_data.get("adaptive_config"),
@@ -826,6 +828,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
ws_update_history_entry,
ws_update_task,
)
from .tasks_bulk import ws_update_many_tasks
from .users import ws_assign_user, ws_list_users, ws_tasks_by_user
from .vacation import (
ws_vacation_end_now,
@@ -857,6 +860,7 @@ def async_register_commands(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, ws_unarchive_task)
websocket_api.async_register_command(hass, ws_pause_task)
websocket_api.async_register_command(hass, ws_resume_task)
websocket_api.async_register_command(hass, ws_update_many_tasks)
websocket_api.async_register_command(hass, ws_list_tasks)
websocket_api.async_register_command(hass, ws_task_history)
websocket_api.async_register_command(hass, ws_complete_task)
@@ -26,11 +26,13 @@ from ..const import (
CONF_OBJECT_MODEL,
CONF_TASKS,
DOMAIN,
MAX_COST,
MAX_ENTITY_SLUG_LENGTH,
MAX_ID_LENGTH,
MAX_IMPORT_PAYLOAD_BYTES,
MAX_JSON_IMPORT_PAYLOAD_BYTES,
MAX_VACATION_EXEMPT_TASKS,
MIN_COST,
)
from ..helpers.aggregate import object_name
from ..helpers.dates import normalize_hhmm, parse_iso_date
@@ -92,7 +94,8 @@ _TASK_FIELD_TYPES: dict[str, type | tuple[type, ...]] = {
def _sanitize_history(history: Any) -> list[dict[str, Any]]:
"""Scrub imported history entries: drop a non-finite/negative ``cost``.
"""Scrub imported history entries: drop a non-finite or out-of-range ``cost``
(a credit — #200 — is negative and stays).
Every live write path range-guards cost, but import copied history verbatim
and ``json.loads``/``yaml.safe_load`` both accept ``NaN``/``Infinity``. Such
@@ -132,7 +135,9 @@ def _sanitize_history(history: Any) -> list[dict[str, Any]]:
if "completed_by" in clean and not isinstance(clean["completed_by"], str):
clean.pop("completed_by")
cost = clean.get("cost")
if isinstance(cost, bool) or not isinstance(cost, (int, float)) or not math.isfinite(cost) or cost < 0:
if "cost" in clean and (
isinstance(cost, bool) or not isinstance(cost, (int, float)) or not math.isfinite(cost) or not MIN_COST <= cost <= MAX_COST
):
clean.pop("cost", None)
# #104 bookkeeping: the parts value is money too (same NaN hole), the
# two markers are exact values or nothing.
@@ -19,6 +19,7 @@ from ..const import (
MAX_DURATION_MINUTES,
MAX_TEXT_LENGTH,
MAX_TIMESTAMP_LENGTH,
MIN_COST,
)
from ..helpers.completion_photos import MAX_COMPLETION_PHOTOS, normalize_photo_doc_ids
from ..helpers.parts import MAX_PART_STOCK
@@ -71,7 +72,7 @@ def _refuse_too_early(connection: websocket_api.ActiveConnection, msg: dict[str,
vol.Required("entry_id"): ID_FIELD,
vol.Required("task_id"): ID_FIELD,
vol.Optional("notes"): vol.Any(vol.All(str, vol.Length(max=MAX_TEXT_LENGTH)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=MIN_COST, max=MAX_COST)), None),
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
# Restrict checklist_state to {string-key (≤500): bool, ...} with
# a hard cap on entries. Without this, attackers (or bad clients)
@@ -16,6 +16,7 @@ from ..const import (
MAX_META_LENGTH,
MAX_TEXT_LENGTH,
MAX_TIMESTAMP_LENGTH,
MIN_COST,
)
from ..helpers.completion_photos import (
MAX_COMPLETION_PHOTOS,
@@ -60,7 +61,7 @@ from . import (
# Patch fields — all optional; absent fields stay unchanged.
vol.Optional("timestamp"): vol.All(str, vol.Length(max=MAX_TIMESTAMP_LENGTH)),
vol.Optional("notes"): vol.Any(vol.All(str, vol.Length(max=MAX_TEXT_LENGTH)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=MIN_COST, max=MAX_COST)), None),
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
vol.Optional("completed_by"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
# Recorded readings (#161 phase 2): a typo among nine meter values is