updated apps
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user