456 files
This commit is contained in:
@@ -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)})
|
||||
|
||||
Reference in New Issue
Block a user