updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
@@ -9,14 +9,42 @@ from homeassistant.const import Platform
DOMAIN = "maintenance_supporter"
# Keys of the shared runtime singletons inside hass.data[DOMAIN]. Defined here
# (not in __init__) so any module can import them without a circular import —
# a DRY audit (2026-07-10) found the notification-manager key duplicated as a
# string literal in four call sites and the budget/event keys with no constant
# at all (the #86 panel-flag desync came from exactly this literal-key class).
NOTIFICATION_MANAGER_KEY = "_notification_manager"
DOCUMENT_STORE_KEY = "_document_store"
BUDGET_CACHE_KEY = "_budget_cache"
EVENT_UNSUBS_KEY = "_event_unsubs"
# TOP-LEVEL hass.data key (survives the DOMAIN pop on last-entry unload):
# one MaintenanceStore per entry_id, REUSED across entry reloads. Two Store
# instances for the same file would race: a pending 60s debounced save on
# the pre-reload instance fires AFTER the post-reload instance loaded, and
# its whole-file write clobbers everything the new instance saved since
# (silent loss of completions/history — bug audit 2026-07-11).
STORES_CACHE_KEY = f"{DOMAIN}_stores"
def slugify_object_name(name: str) -> str:
"""Convert an object name to a safe slug for use in unique IDs.
Replaces any non-alphanumeric character with underscore, collapses
consecutive underscores, and strips leading/trailing underscores.
A name with no ASCII alphanumerics (CJK, Cyrillic, emoji, punctuation-only)
slugifies to an empty string, which would make every such object collide on
the same ``maintenance_supporter_`` unique id (so a whole non-Latin-script
user base could only ever create ONE object). Fall back to a stable hash of
the original name so distinct names get distinct ids.
"""
return re.sub(r"_+", "_", re.sub(r"[^a-z0-9]", "_", name.lower())).strip("_")
slug = re.sub(r"_+", "_", re.sub(r"[^a-z0-9]", "_", name.lower())).strip("_")
if not slug:
import hashlib
slug = "obj_" + hashlib.sha1(name.strip().encode("utf-8")).hexdigest()[:12]
return slug
PLATFORMS: list[Platform] = [
@@ -118,6 +146,22 @@ CONF_OBJECTS_TABLE_COLUMNS = "objects_table_columns"
# removed from the pickers so a growing catalog never clutters the UI.
CONF_DISABLED_TEMPLATE_IDS = "disabled_template_ids"
# v2.24: saved filter views — named, shared combinations of the panel list's
# filters (status / user / archived) + sort + group-by that any user can apply
# from the toolbar. Stored on the global entry as a list of view dicts; see
# helpers/saved_views.py for the shape + sanitiser. A view's id can later feed
# notification routing ("only notify me about view X").
CONF_SAVED_FILTER_VIEWS = "saved_filter_views"
MAX_SAVED_VIEWS = 50 # coarse ceiling so the global entry can't grow unbounded
MAX_VIEW_NAME_LENGTH = 60
# v2.26: notes of a deleted ADOPTED problem-sensor task, keyed by the watched
# sensor's entity_id, so an un-adopt → re-adopt cycle restores them (roadmap
# "note persistence"). Internal state on the global entry (like saved views),
# not a user-facing setting. FIFO-capped; an entry is consumed on re-adopt.
CONF_ADOPTED_NOTES = "adopted_task_notes"
MAX_ADOPTED_NOTES = 100
# Every column key the objects table can render. The Settings UI offers exactly
# these; the WS update handler drops anything outside this set.
KNOWN_OBJECT_TABLE_COLUMNS = [
@@ -190,6 +234,11 @@ CONF_NOTIFICATION_BUNDLE_THRESHOLD = "notification_bundle_threshold"
# "object_name" — use the object name as the title (helps when phone stacks notifications).
# "task_name" — use the task name as the title.
CONF_NOTIFICATION_TITLE_STYLE = "notification_title_style"
# v2.26: notification routing — id of a saved filter view; when set, only tasks
# MATCHING that view's task-selecting filters (label + responsible user) send
# reminders. "" = no scope (all tasks). See helpers/saved_views.view_matches_task.
CONF_NOTIFY_SCOPE_VIEW_ID = "notify_scope_view_id"
NOTIFICATION_TITLE_STYLES = ("default", "object_name", "task_name")
# --- Config Keys: Notification Actions ---
@@ -259,6 +308,17 @@ CONF_OBJECT_DOCUMENTATION_URL = "documentation_url"
# v1.4.10 (#46): per-object free-form notes (part numbers, procedures, etc.)
CONF_OBJECT_NOTES = "notes"
# --- Config Keys: Spare parts & consumables ---
# Static part definitions live in entry.data["parts"] (like tasks); the
# mutable stock count lives in the per-entry Store. See helpers/parts.py.
CONF_PARTS = "parts"
# Task-side link: task["consumes_parts"] = [{"part_id", "quantity"}] — a
# completion decrements each linked part's stock.
CONF_TASK_CONSUMES_PARTS = "consumes_parts"
# Global setting: shopping-search URL template with a {q} placeholder, used
# for parts without a product_url (default: Amazon by UI language).
CONF_PART_SEARCH_URL_TEMPLATE = "part_search_url_template"
# --- Config Keys: Task ---
CONF_TASKS = "tasks"
CONF_TASK_NAME = "name"
@@ -365,6 +425,7 @@ BUDGET_CURRENCIES: dict[str, str] = {
"CHF": "Fr",
"CAD": "C$",
"AUD": "A$",
"NZD": "NZ$", # #96
"CNY": "¥",
"INR": "",
"BRL": "R$",
@@ -426,6 +487,12 @@ EVENT_TASK_COMPLETED = f"{DOMAIN}_task_completed"
EVENT_TASK_SKIPPED = f"{DOMAIN}_task_skipped"
EVENT_TASK_RESET = f"{DOMAIN}_task_reset"
# Spare-part stock crossings — edge-triggered (one event per transition; a
# further decrease while already low never re-fires). See helpers/parts.py.
EVENT_PART_STOCK_LOW = f"{DOMAIN}_part_stock_low"
EVENT_PART_STOCK_OUT = f"{DOMAIN}_part_stock_out"
EVENT_PART_RESTOCKED = f"{DOMAIN}_part_restocked"
# --- Service Names ---
SERVICE_COMPLETE = "complete"
SERVICE_RESET = "reset"
@@ -562,6 +629,12 @@ MAX_URL_LENGTH = 2048
MAX_ICON_LENGTH = 100 # "mdi:icon-name"
MAX_META_LENGTH = 200 # manufacturer, model, user_id, area_id, etc.
MAX_PANEL_TITLE_LENGTH = 50 # sidebar panel title override
# Completion cost + duration bounds — one source for the WS schemas AND the
# config-flow-path sanitiser (they must agree, else a value the sanitiser
# accepts can bypass the WS cap or vice-versa).
MAX_COST = 1_000_000 # per completion / history entry
MAX_DURATION_MINUTES = 525_600 # one year in minutes
MAX_TYPE_LENGTH = 50 # task_type, schedule_type
MAX_CHECKLIST_ITEMS = 100
MAX_CHECKLIST_ITEM_LENGTH = 500
@@ -573,9 +646,22 @@ MAX_DATE_LENGTH = 20 # ISO 8601 date strings (e.g. 2026-04-21)
MAX_ENTITY_ID_LENGTH = 255 # HA entity_id max
MAX_ENTITY_SLUG_LENGTH = 64 # task entity_slug
MAX_INTERVAL_DAYS = 3650 # 10 years — caps date arithmetic overflow
MAX_IMPORT_PAYLOAD_BYTES = 1_048_576 # 1 MB for csv_content / json_content
# Import payload byte caps. CSV is flat (one row per task) so 1 MB is ample;
# a JSON/YAML backup carries full history + every field, so it needs more head-
# room — a 1000-object export with history routinely exceeds 1 MB. Both were
# hardcoded literals at their call sites (bug audit 2026-07-11); named here so
# the two limits are visible and can't silently drift.
MAX_IMPORT_PAYLOAD_BYTES = 1_048_576 # 1 MB — csv_content
MAX_JSON_IMPORT_PAYLOAD_BYTES = 10 * 1_048_576 # 10 MB — json_content (history-heavy)
MAX_SCHEDULE_TIME_LENGTH = 5 # "HH:MM"
# Per-object fan-out caps — a runaway automation or import loop must not be able
# to inflate one object's ConfigEntry.data without bound. Deliberately generous
# (far above any real device): they catch accidents/abuse, not real use. Parts
# already had MAX_PARTS_PER_OBJECT (50); tasks and documents were unbounded.
MAX_TASKS_PER_OBJECT = 200 # mirrors MAX_GROUP_TASK_REFS; ~10x any real device
MAX_DOCS_PER_OBJECT = 100 # manuals + invoices + photos — 100 is very generous
# --- Trigger Entity Availability ---
STARTUP_GRACE_PERIOD_SECONDS = 300 # 5 minutes
MISSING_ENTITY_THRESHOLD_REFRESHES = 6 # ~30 min at 5-min intervals