456 files

This commit is contained in:
Home Assistant Version Control
2026-10-01 05:38:21 +00:00
parent 4192ecbaae
commit 5d6d845ebf
456 changed files with 64951 additions and 3828 deletions
@@ -44,6 +44,7 @@ from ..helpers.qr_generator import (
generate_qr_svg,
generate_qr_svg_data_uri,
)
from ..helpers.ws_errors import send_translated_error
from ..websocket.tasks import _check_nfc_tag_duplicate, _validate_trigger_config
from . import ID_FIELD, _get_object_entries, _load_object_entry, _load_object_task, _merge_global_options
@@ -529,7 +530,7 @@ async def ws_get_templates(
result = {
"categories": {cat_id: {k: v for k, v in cat.items()} for cat_id, cat in TEMPLATE_CATEGORIES.items()},
"profile": profile.as_dict(),
"profile": profile.as_dict(lang),
"templates": [
{
"id": t.id,
@@ -652,16 +653,16 @@ async def ws_import_csv(
csv_content = msg["csv_content"]
# Guard against oversized payloads (max 1MB / 1000 objects)
if len(csv_content) > MAX_IMPORT_PAYLOAD_BYTES:
connection.send_error(msg["id"], "too_large", "CSV content exceeds 1MB limit")
send_translated_error(connection, msg["id"], "too_large", "CSV content exceeds 1MB limit", translation_key="import_too_large", translation_placeholders={"limit": "1 MB"})
return
objects = import_objects_csv(csv_content, hass=hass)
if len(objects) > 1000:
connection.send_error(msg["id"], "too_many", "CSV contains more than 1000 objects")
send_translated_error(connection, msg["id"], "too_many", "CSV contains more than 1000 objects", translation_key="import_too_many", translation_placeholders={"max": "1000"})
return
if not objects:
connection.send_error(msg["id"], "empty_csv", "No valid objects found in CSV")
send_translated_error(connection, msg["id"], "empty_csv", "No valid objects found in CSV", translation_key="import_csv_no_objects")
return
created = []
@@ -906,18 +907,18 @@ async def ws_import_json(
raw = msg["json_content"]
if len(raw) > MAX_JSON_IMPORT_PAYLOAD_BYTES:
connection.send_error(msg["id"], "too_large", "Content exceeds 10MB limit")
send_translated_error(connection, msg["id"], "too_large", "Content exceeds 10MB limit", translation_key="import_too_large", translation_placeholders={"limit": "10 MB"})
return
try:
data = _parse_structured(raw)
except ValueError:
connection.send_error(msg["id"], "invalid_format", "Content is not valid JSON or YAML")
send_translated_error(connection, msg["id"], "invalid_format", "Content is not valid JSON or YAML", translation_key="import_not_json_yaml")
return
has_settings = isinstance(data, dict) and isinstance(data.get("global_settings"), dict)
if not isinstance(data, dict) or ("objects" not in data and not has_settings):
connection.send_error(msg["id"], "invalid_format", "JSON must contain an 'objects' array")
send_translated_error(connection, msg["id"], "invalid_format", "JSON must contain an 'objects' array", translation_key="import_no_objects_list")
return
# A settings export (see export.build_settings_export) may travel alone or
@@ -928,15 +929,15 @@ async def ws_import_json(
objects = data.get("objects", [])
if not isinstance(objects, list):
connection.send_error(msg["id"], "invalid_format", "'objects' must be an array")
send_translated_error(connection, msg["id"], "invalid_format", "'objects' must be an array", translation_key="import_no_objects_list")
return
if len(objects) > 1000:
connection.send_error(msg["id"], "too_many", "JSON contains more than 1000 objects")
send_translated_error(connection, msg["id"], "too_many", "JSON contains more than 1000 objects", translation_key="import_too_many", translation_placeholders={"max": "1000"})
return
if not objects and not settings_applied:
connection.send_error(msg["id"], "empty", "No objects found in JSON")
send_translated_error(connection, msg["id"], "empty", "No objects found in JSON", translation_key="import_no_objects")
return
created = []
@@ -1136,6 +1137,13 @@ async def ws_import_json(
task_data[BATTERY_FLEET_TASK_FLAG] = True
fleet_task_seen = True
# 2.95: the task's fingerprint (catalog duty / template task) —
# shape-checked, it is untrusted input like everything imported.
from ..helpers.task_origin import ORIGIN_KEY, sanitize_origin
if (origin := sanitize_origin(task_entry.get(ORIGIN_KEY))) is not None:
task_data[ORIGIN_KEY] = origin
# In-cycle checklist ticks: keyed by item TEXT so they survive the
# id regeneration; keys are filtered against the imported checklist
# exactly like the live checklist_progress WS write. Rides
@@ -1441,7 +1449,7 @@ async def ws_generate_qr(
url_mode=url_mode,
)
except ValueError as err:
connection.send_error(msg["id"], "no_url", str(err))
send_translated_error(connection, msg["id"], "no_url", str(err), translation_key="qr_no_url")
return
from functools import partial
@@ -1526,17 +1534,21 @@ async def ws_batch_generate_qr(
# Build the flat (entry_id, object_name, task_id, task_name) target list,
# honouring the optional task_ids filter.
task_filter = set(msg["task_ids"]) if msg.get("task_ids") else None
targets: list[tuple[str, str, str, str]] = []
targets: list[tuple[str, str, str, str, bool]] = []
for entry in entries:
obj_name = object_name(entry)
tasks_data = entry.data.get(CONF_TASKS, {})
for task_id, task_data in tasks_data.items():
if task_filter is not None and task_id not in task_filter:
continue
targets.append((entry.entry_id, obj_name, task_id, task_data.get("name", "")))
has_quick = bool(task_data.get("quick_complete_defaults"))
targets.append((entry.entry_id, obj_name, task_id, task_data.get("name", ""), has_quick))
actions: list[str] = msg["actions"]
total = len(targets) * len(actions)
# A quick-complete code only for tasks with quick-complete defaults — for
# the others it would just open the complete dialog (#192).
pairs = [(target, action) for target in targets for action in actions if action != "quick_complete" or target[4]]
total = len(pairs)
if total == 0:
connection.send_result(msg["id"], {"qrs": [], "total": 0})
return
@@ -1556,33 +1568,32 @@ async def ws_batch_generate_qr(
# since it's CPU-bound (~30-40 ms/QR). Each SVG passes through the LRU
# cache so re-runs after a filter change are near-instant.
results: list[dict[str, Any]] = []
for entry_id, obj_name, task_id, task_name in targets:
for action in actions:
try:
url = build_qr_url(
hass,
entry_id,
task_id=task_id,
action=action,
base_url_override=base_url,
url_mode=url_mode,
)
except ValueError:
# No HA URL configured — skip this row rather than fail the
# whole batch. "server" mode is the only path that raises;
# "companion" and "local" always resolve.
continue
icon = _ACTION_ICON_MAP.get(action) # None for "skip" (no icon)
svg = await hass.async_add_executor_job(_cached_qr_svg, url, icon)
results.append(
{
"entry_id": entry_id,
"task_id": task_id,
"object_name": obj_name,
"task_name": task_name,
"action": action,
"svg": svg,
}
for (entry_id, obj_name, task_id, task_name, _has_quick), action in pairs:
try:
url = build_qr_url(
hass,
entry_id,
task_id=task_id,
action=action,
base_url_override=base_url,
url_mode=url_mode,
)
except ValueError:
# No HA URL configured — skip this row rather than fail the
# whole batch. "server" mode is the only path that raises;
# "companion" and "local" always resolve.
continue
icon = _ACTION_ICON_MAP.get(action) # None for "skip" (no icon)
svg = await hass.async_add_executor_job(_cached_qr_svg, url, icon)
results.append(
{
"entry_id": entry_id,
"task_id": task_id,
"object_name": obj_name,
"task_name": task_name,
"action": action,
"svg": svg,
}
)
connection.send_result(msg["id"], {"qrs": results, "total": len(results)})