"""Constants for the Maintenance Supporter integration.""" from __future__ import annotations import re from enum import StrEnum from homeassistant.const import Platform DOMAIN = "maintenance_supporter" 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. """ return re.sub(r"_+", "_", re.sub(r"[^a-z0-9]", "_", name.lower())).strip("_") PLATFORMS: list[Platform] = [ Platform.SENSOR, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CALENDAR, Platform.TODO, ] # --- Unique IDs --- GLOBAL_UNIQUE_ID = "maintenance_supporter_global" # --- Defaults --- DEFAULT_WARNING_DAYS = 7 DEFAULT_INTERVAL_DAYS = 30 DEFAULT_UPDATE_INTERVAL_MINUTES = 5 DEFAULT_MAX_HISTORY_ENTRIES = 500 DEFAULT_SNOOZE_DURATION_HOURS = 4 DEFAULT_MAX_NOTIFICATIONS_PER_DAY = 0 # 0 = unlimited # --- Adaptive Scheduling Defaults --- DEFAULT_ADAPTIVE_EWA_ALPHA = 0.3 DEFAULT_ADAPTIVE_MIN_INTERVAL = 7 DEFAULT_ADAPTIVE_MAX_INTERVAL = 365 DEFAULT_ADAPTIVE_MIN_COMPLETIONS = 3 # Before showing suggestions DEFAULT_ADAPTIVE_WEIBULL_MIN = 5 # Before fitting Weibull DEFAULT_ADAPTIVE_RELIABILITY_TARGET = 0.9 # 90% for Weibull # --- Seasonal Scheduling Defaults --- DEFAULT_SEASONAL_MIN_DATA = 6 # Min intervals across all months before seasonal adjustment DEFAULT_SEASONAL_FACTOR_MIN = 0.3 # Floor: never less than 30% of base interval DEFAULT_SEASONAL_FACTOR_MAX = 3.0 # Ceiling: never more than 300% of base interval # --- Hemisphere-aware Season Mapping --- NORTHERN_SEASONS: dict[str, list[int]] = { "spring": [3, 4, 5], "summer": [6, 7, 8], "fall": [9, 10, 11], "winter": [12, 1, 2], } SOUTHERN_SEASONS: dict[str, list[int]] = { "spring": [9, 10, 11], "summer": [12, 1, 2], "fall": [3, 4, 5], "winter": [6, 7, 8], } # --- Config Keys: Global --- CONF_DEFAULT_WARNING_DAYS = "default_warning_days" CONF_NOTIFICATIONS_ENABLED = "notifications_enabled" CONF_NOTIFY_SERVICE = "notify_service" CONF_PANEL_ENABLED = "panel_enabled" # v2.10.4 (#69 follow-up): the sidebar panel is the integration's hub — the # auto-dashboard's "Open Maintenance panel" button, QR codes, and notifications # all link to /maintenance-supporter. With the panel off those links 404, so it # now defaults ON. An explicit opt-out (panel_enabled: false in options) is # still honoured. Read this default everywhere instead of inlining the literal. DEFAULT_PANEL_ENABLED = True CONF_PANEL_TITLE = "panel_title" # --- Config Keys: Advanced Feature Visibility --- CONF_ADVANCED_ADAPTIVE = "advanced_adaptive_visible" CONF_ADVANCED_PREDICTIONS = "advanced_predictions_visible" CONF_ADVANCED_SEASONAL = "advanced_seasonal_visible" CONF_ADVANCED_ENVIRONMENTAL = "advanced_environmental_visible" CONF_ADVANCED_BUDGET = "advanced_budget_visible" CONF_ADVANCED_GROUPS = "advanced_groups_visible" CONF_ADVANCED_CHECKLISTS = "advanced_checklists_visible" CONF_ADVANCED_SCHEDULE_TIME = "advanced_schedule_time_visible" # v1.3.0: gates the per-task on_complete_action + quick_complete_defaults # UI sections in the task dialog, plus the quick_complete chip in the # Print-QR generator. Default OFF — beginners aren't overwhelmed; data # still persists if a user toggles the flag off again. CONF_ADVANCED_COMPLETION_ACTIONS = "advanced_completion_actions_visible" # Panel-access overrides: HA user IDs who get the full admin panel despite # not being HA admins. Empty list means only admins see the full panel. # The allowlist grants create/edit/delete ONLY when CONF_OPERATOR_WRITE_ENABLED # is also on; with it off (the default) listed users get read-only access. CONF_ADMIN_PANEL_USER_IDS = "admin_panel_user_ids" # v2.8.4: master switch for operator write delegation. Default OFF → only HA # admins may create/edit/delete and the panel-access allowlist is read-only. # When True, allowlisted non-admins additionally gain full content CRUD. Only # admins can flip it (panel Settings tab / config-flow Panel Access step, both # admin-gated), preserving the escalation boundary in helpers/permissions.py. CONF_OPERATOR_WRITE_ENABLED = "operator_write_enabled" # (#67): objects-table column configuration for the panel All-Objects view # (table mode). Global setting holding the ordered list of object columns to # show. Selectable from KNOWN_OBJECT_TABLE_COLUMNS only — known object fields, # never arbitrary state attributes. CONF_OBJECTS_TABLE_COLUMNS = "objects_table_columns" # v2.21: template-gallery curation — ids of built-in templates the admin has # hidden from the "From template" pickers (panel gallery + config flow). The # templates stay functional (direct WS calls still work); they are only # removed from the pickers so a growing catalog never clutters the UI. CONF_DISABLED_TEMPLATE_IDS = "disabled_template_ids" # 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 = [ "name", "manufacturer", "model", "serial_number", "installation_date", "warranty_expiry", "area_id", "documentation_url", "notes", "task_count", "actions", ] # Default column set + order when the user hasn't customised it. Mirrors the # locked design: Name · Manufacturer · Model · Serial · Installed · Warranty · # Area · Tasks · link (documentation_url + notes are opt-in extras). DEFAULT_OBJECTS_TABLE_COLUMNS = [ "name", "manufacturer", "model", "serial_number", "installation_date", "warranty_expiry", "area_id", "task_count", "actions", ] # --- Vacation mode (v1.2.0) --- # When active, suppresses notifications for non-exempt tasks across the # vacation window plus an N-day buffer (so a task that comes due the day # of return doesn't immediately fire). CONF_VACATION_ENABLED = "vacation_enabled" CONF_VACATION_START = "vacation_start" # ISO date "YYYY-MM-DD" CONF_VACATION_END = "vacation_end" # ISO date CONF_VACATION_BUFFER_DAYS = "vacation_buffer_days" CONF_VACATION_EXEMPT_TASK_IDS = "vacation_exempt_task_ids" DEFAULT_VACATION_BUFFER_DAYS = 3 # --- Config Keys: Notification Per-Status --- CONF_NOTIFY_DUE_SOON_ENABLED = "notify_due_soon_enabled" CONF_NOTIFY_DUE_SOON_INTERVAL = "notify_due_soon_interval_hours" CONF_NOTIFY_OVERDUE_ENABLED = "notify_overdue_enabled" CONF_NOTIFY_OVERDUE_INTERVAL = "notify_overdue_interval_hours" CONF_NOTIFY_TRIGGERED_ENABLED = "notify_triggered_enabled" CONF_NOTIFY_TRIGGERED_INTERVAL = "notify_triggered_interval_hours" # --- Config Keys: Notification Quiet Hours --- CONF_QUIET_HOURS_ENABLED = "quiet_hours_enabled" CONF_QUIET_HOURS_START = "quiet_hours_start" CONF_QUIET_HOURS_END = "quiet_hours_end" # Shared HH:MM or HH:MM:SS time pattern (0–23 h, 0–59 m/s) — the single source # for validating quiet-hours times in BOTH the options flow and the WS handler. TIME_HHMMSS_PATTERN = re.compile(r"^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$") # --- Config Keys: Notification Limits --- CONF_MAX_NOTIFICATIONS_PER_DAY = "max_notifications_per_day" # --- Config Keys: Notification Bundling --- CONF_NOTIFICATION_BUNDLING_ENABLED = "notification_bundling_enabled" CONF_NOTIFICATION_BUNDLE_THRESHOLD = "notification_bundle_threshold" # --- Config Keys: Notification Title (v1.4.0 #44) --- # How to format the notification title. # "default" — generic per-status title (e.g. "Maintenance overdue!"). Backwards-compatible. # "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" NOTIFICATION_TITLE_STYLES = ("default", "object_name", "task_name") # --- Config Keys: Notification Actions --- CONF_ACTION_COMPLETE_ENABLED = "action_complete_enabled" CONF_ACTION_SKIP_ENABLED = "action_skip_enabled" CONF_ACTION_SNOOZE_ENABLED = "action_snooze_enabled" CONF_SNOOZE_DURATION_HOURS = "snooze_duration_hours" # v2.15.0: opt-in weekly digest — a single Monday-morning summary notification. CONF_WEEKLY_DIGEST_ENABLED = "weekly_digest_enabled" CONF_WARRANTY_REMINDER_ENABLED = "warranty_reminder_enabled" CONF_WARRANTY_REMINDER_DAYS = "warranty_reminder_days" DEFAULT_WARRANTY_REMINDER_DAYS = 30 # Multiple lead-time reminders: list of days-before-due at which an extra # reminder fires (e.g. [14, 3, 0]). Empty = feature off (single warning_days # threshold behaviour unchanged). Checked by a daily tick, not the # status-change path. CONF_REMINDER_LEAD_DAYS = "reminder_lead_days" MAX_REMINDER_LEADS = 10 # --- Panel --- PANEL_NAME = "maintenance-supporter" PANEL_TITLE = "Maintenance" PANEL_ICON = "mdi:wrench-clock" PANEL_URL = "/maintenance_supporter_panel" CARD_URL = "/maintenance_supporter_card" # v2.3.4 (issue #52): strategy is now a multi-file split bundle. The entry # URL has to live UNDER a directory prefix so the entry's relative # ./chunks/X.js dynamic imports resolve to //chunks/X.js. Pre-2.3.4 # users had a flat URL — old dashboards still load fine because the strategy # is identified by ``custom:maintenance-supporter`` (a logical name resolved # via window.customStrategies), not by URL. STRATEGY_DIR_URL = "/maintenance_supporter_strategy" STRATEGY_URL = f"{STRATEGY_DIR_URL}/maintenance-dashboard-strategy.js" STRATEGY_CHUNKS_URL = f"{STRATEGY_DIR_URL}/chunks" # v2.8.1 (strategy timeout fix): a tiny zero-import shim is what we register as # the frontend extra-module-url. It defines the dashboard-strategy element # synchronously (winning HA's 5 s whenDefined race under heavy plugin load) and # lazy-imports the full STRATEGY_URL bundle on first use. The bundle's own # relative ./chunks/* imports still resolve because it is served under # STRATEGY_DIR_URL. The shim has no relative imports, so a flat URL is fine. STRATEGY_SHIM_URL = "/maintenance_supporter_strategy_shim.js" CALENDAR_CARD_URL = "/maintenance_supporter_calendar_card" # Runtime-loaded UI translations (frontend/locales/.json), served as a # directory so the panel/card fetch the active language on demand. Mirrors # LOCALES_BASE in frontend-src/styles.ts: only EN is bundled into the JS (as the # fallback); the other languages live here, so a translation edit needs no # frontend bundle rebuild. LOCALES_URL = "/maintenance_supporter_locales" # v2.21: vendored third-party assets (pdf.js for the work sheet's inline # manual excerpt) — served statically, loaded ONLY by the work-sheet tab so # the panel bundle stays lean. VENDOR_URL = "/maintenance_supporter_vendor" # --- Config Keys: Object --- CONF_OBJECT = "object" CONF_OBJECT_NAME = "name" CONF_OBJECT_AREA = "area_id" CONF_OBJECT_MANUFACTURER = "manufacturer" CONF_OBJECT_MODEL = "model" CONF_OBJECT_SERIAL_NUMBER = "serial_number" CONF_OBJECT_INSTALLATION_DATE = "installation_date" # (#67): per-object warranty expiry date (ISO YYYY-MM-DD) for asset tracking CONF_OBJECT_WARRANTY_EXPIRY = "warranty_expiry" # v1.4.0 (#43): per-object link to PDF manual / vendor page 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: Task --- CONF_TASKS = "tasks" CONF_TASK_NAME = "name" CONF_TASK_TYPE = "type" CONF_TASK_ENABLED = "enabled" CONF_TASK_INTERVAL_DAYS = "interval_days" CONF_TASK_WARNING_DAYS = "warning_days" CONF_TASK_LAST_PERFORMED = "last_performed" CONF_TASK_SCHEDULE_TYPE = "schedule_type" CONF_TASK_INTERVAL_UNIT = "interval_unit" CONF_TASK_DUE_DATE = "due_date" # Recurrence extras (config/options flow): seasonal window + finite-series end. CONF_TASK_SEASON_MONTHS = "season_months" CONF_TASK_ENDS_COUNT = "ends_count" CONF_TASK_ENDS_UNTIL = "ends_until" CONF_TASK_NOTES = "notes" CONF_TASK_DOCUMENTATION_URL = "documentation_url" CONF_TASK_ICON = "custom_icon" CONF_TASK_NFC_TAG = "nfc_tag_id" # v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). CONF_TASK_READING_UNIT = "reading_unit" MAX_READING_UNIT_LENGTH = 32 CONF_TASK_INTERVAL_ANCHOR = "interval_anchor" CONF_TASK_SCHEDULE_TIME = "schedule_time" CONF_TASK_PRIORITY = "priority" # Config-flow-only field: comma-separated labels text, parsed into the # persisted ``labels`` list by the task-base handler. CONF_TASK_LABELS_TEXT = "labels_text" CONF_TASK_ASSIGNEE_POOL = "assignee_pool" CONF_TASK_ROTATION_STRATEGY = "rotation_strategy" # Rotation strategies for shared tasks: advance the "currently responsible" # pointer on each completion. None/"" = no rotation (single-assignee behaviour). ROTATION_STRATEGIES = ("round_robin", "least_completed", "random") MAX_ASSIGNEE_POOL = 25 # --- Config Keys: User Assignment --- CONF_RESPONSIBLE_USER_ID = "responsible_user_id" # --- Config Keys: Trigger --- CONF_TRIGGER_CONFIG = "trigger_config" CONF_TRIGGER_TYPE = "trigger_type" CONF_TRIGGER_ENTITY = "trigger_entity" CONF_TRIGGER_ENTITY_IDS = "entity_ids" CONF_TRIGGER_ENTITY_LOGIC = "entity_logic" CONF_TRIGGER_STATE = "_trigger_state" CONF_COMPOUND_LOGIC = "compound_logic" CONF_COMPOUND_CONDITIONS = "conditions" DEFAULT_ENTITY_LOGIC = "any" CONF_TRIGGER_ATTRIBUTE = "trigger_attribute" CONF_TRIGGER_ABOVE = "trigger_above" CONF_TRIGGER_BELOW = "trigger_below" CONF_TRIGGER_FOR_MINUTES = "trigger_for_minutes" CONF_TRIGGER_TARGET_VALUE = "trigger_target_value" CONF_TRIGGER_DELTA_MODE = "trigger_delta_mode" CONF_TRIGGER_BASELINE_VALUE = "trigger_baseline_value" CONF_TRIGGER_FROM_STATE = "trigger_from_state" CONF_TRIGGER_TO_STATE = "trigger_to_state" CONF_TRIGGER_TARGET_CHANGES = "trigger_target_changes" CONF_TRIGGER_RUNTIME_HOURS = "trigger_runtime_hours" CONF_TRIGGER_ON_STATES = "trigger_on_states" # --- Config Keys: Adaptive Scheduling --- CONF_ADAPTIVE_CONFIG = "adaptive_config" CONF_ADAPTIVE_ENABLED = "adaptive_enabled" CONF_ADAPTIVE_EWA_ALPHA = "ewa_alpha" CONF_ADAPTIVE_MIN_INTERVAL = "min_interval_days" CONF_ADAPTIVE_MAX_INTERVAL = "max_interval_days" # --- Config Keys: Seasonal Scheduling --- CONF_SEASONAL_ENABLED = "seasonal_enabled" CONF_SEASONAL_OVERRIDES = "seasonal_overrides" CONF_SEASONAL_HEMISPHERE = "seasonal_hemisphere" # --- Sensor Prediction Defaults (Phase 3) --- DEFAULT_DEGRADATION_LOOKBACK_DAYS = 30 # Days of recorder data for slope computation DEFAULT_DEGRADATION_MIN_POINTS = 10 # Min hourly data points to compute regression DEFAULT_DEGRADATION_SIGNIFICANCE = 0.05 # Min |slope|/mean ratio for rising/falling # --- Environmental Adjustment Defaults (Phase 3) --- DEFAULT_ENVIRONMENTAL_LOOKBACK_DAYS = 90 # Days of env data for correlation DEFAULT_ENVIRONMENTAL_CORRELATION_MIN = 0.3 # Min |r| to apply adjustment DEFAULT_ENVIRONMENTAL_FACTOR_MIN = 0.5 # Floor for env adjustment DEFAULT_ENVIRONMENTAL_FACTOR_MAX = 2.0 # Ceiling for env adjustment DEFAULT_ENVIRONMENTAL_MIN_COMPLETIONS = 3 # Min completions with env data # --- Config Keys: Sensor Prediction (Phase 3) --- CONF_SENSOR_PREDICTION_ENABLED = "sensor_prediction_enabled" CONF_ENVIRONMENTAL_ENTITY = "environmental_entity" CONF_ENVIRONMENTAL_ATTRIBUTE = "environmental_attribute" # --- Budget --- CONF_BUDGET_MONTHLY = "budget_monthly" CONF_BUDGET_YEARLY = "budget_yearly" CONF_BUDGET_ALERTS_ENABLED = "budget_alerts_enabled" CONF_BUDGET_ALERT_THRESHOLD = "budget_alert_threshold" CONF_BUDGET_CURRENCY = "budget_currency" BUDGET_CURRENCIES: dict[str, str] = { "EUR": "€", "USD": "$", "GBP": "£", "JPY": "¥", "CHF": "Fr", "CAD": "C$", "AUD": "A$", "CNY": "¥", "INR": "₹", "BRL": "R$", "CZK": "Kč", "PLN": "zł", "RUB": "₽", "SEK": "kr", "NOK": "kr", "DKK": "kr", "UAH": "₴", } # Default currency when the user hasn't chosen one. Its symbol is derived from # BUDGET_CURRENCIES so the two never disagree. DEFAULT_BUDGET_CURRENCY = "EUR" # --- Archive & Retention (v2.10.0) --- # Global int settings (panel-managed via WS, like objects_table_columns): # archive_oneoff_days — auto-archive a completed one-off this many days # after completion. 0 = disabled (manual only). # delete_archived_oneoff_days — auto-delete an AUTO-archived one-off this many # days after it was archived. 0 = never. Manual # archives are NEVER auto-deleted (delete stays explicit). CONF_ARCHIVE_ONEOFF_DAYS = "archive_oneoff_days" CONF_DELETE_ARCHIVED_ONEOFF_DAYS = "delete_archived_oneoff_days" DEFAULT_ARCHIVE_ONEOFF_DAYS = 14 DEFAULT_DELETE_ARCHIVED_ONEOFF_DAYS = 0 # 0 = never # Per-task/object `archived_reason` values (stored alongside `archived_at`). # Drives two policies: only AUTO is eligible for auto-delete; only OBJECT is # undone by an object unarchive cascade (MANUAL/AUTO survive it). ARCHIVE_REASON_MANUAL = "manual" ARCHIVE_REASON_AUTO = "auto" ARCHIVE_REASON_OBJECT = "object" # --- Groups --- CONF_GROUPS = "groups" # --- Checklist --- CONF_CHECKLIST = "checklist" # --- History --- CONF_HISTORY = "history" # --- Runtime keys (not persisted) --- RUNTIME_TRIGGER_ACTIVE = "_trigger_active" RUNTIME_TRIGGER_CURRENT_VALUE = "_trigger_current_value" RUNTIME_TRIGGER_CHANGE_COUNT = "_trigger_change_count" RUNTIME_TRIGGER_CURRENT_DELTA = "_trigger_current_delta" # --- Event Types --- EVENT_TRIGGER_ACTIVATED = f"{DOMAIN}_trigger_activated" EVENT_TRIGGER_DEACTIVATED = f"{DOMAIN}_trigger_deactivated" # v1.3.0: completion lifecycle events. Fired by coordinator after the # state mutation has persisted. Power users wire HA automations on these; # the integration's own action_listener also subscribes to the COMPLETED # event to dispatch the per-task on_complete_action service-call. EVENT_TASK_COMPLETED = f"{DOMAIN}_task_completed" EVENT_TASK_SKIPPED = f"{DOMAIN}_task_skipped" EVENT_TASK_RESET = f"{DOMAIN}_task_reset" # --- Service Names --- SERVICE_COMPLETE = "complete" SERVICE_RESET = "reset" SERVICE_SKIP = "skip" SERVICE_EXPORT = "export_data" SERVICE_ADD_OBJECT = "add_object" SERVICE_ADD_TASK = "add_task" SERVICE_UPDATE_TASK = "update_task" SERVICE_DELETE_TASK = "delete_task" SERVICE_LIST_TASKS = "list_tasks" class MaintenanceStatus(StrEnum): """Status of a maintenance task.""" OK = "ok" DUE_SOON = "due_soon" OVERDUE = "overdue" TRIGGERED = "triggered" # v2.10.0: highest-precedence status. An archived task/object is retired but # retained — it reads `archived`, fires nothing, and counts for nothing # except budget/cost history (which is read from completion history, not # status, so it is archive-agnostic by design). ARCHIVED = "archived" # v2.20 (journey N3): seasonal pause. Tasks of a paused OBJECT read # `paused`: schedules freeze and nothing fires — but unlike archived the # object stays a first-class citizen in every view. Resuming re-anchors # recurring tasks like an object unarchive does. Object-wide only (no # per-task pause); injected by the coordinator, below ARCHIVED in # precedence. PAUSED = "paused" class MaintenanceTypeEnum(StrEnum): """Type/category of maintenance.""" CLEANING = "cleaning" INSPECTION = "inspection" REPLACEMENT = "replacement" CALIBRATION = "calibration" SERVICE = "service" # Record a value / take a reading (meter readings, level checks, spot # measurements) — requested in Discussion #83, generalised beyond meters. READING = "reading" CUSTOM = "custom" class ScheduleType(StrEnum): """How a task is scheduled.""" TIME_BASED = "time_based" SENSOR_BASED = "sensor_based" MANUAL = "manual" ONE_TIME = "one_time" # Schedule kinds expressed by the FLAT recurrence fields (interval/due_date), # as opposed to the nested calendar kinds (weekdays/nth_weekday/day_of_month) # or a sensor trigger. Used to decide flat-vs-calendar handling on task edit. FLAT_SCHEDULE_TYPES = frozenset({ScheduleType.TIME_BASED, ScheduleType.ONE_TIME, ScheduleType.MANUAL}) class TriggerType(StrEnum): """Type of sensor trigger.""" THRESHOLD = "threshold" COUNTER = "counter" STATE_CHANGE = "state_change" RUNTIME = "runtime" COMPOUND = "compound" class TaskPriority(StrEnum): """Priority level of a maintenance task.""" LOW = "low" NORMAL = "normal" HIGH = "high" # The default when a task doesn't specify a priority. DEFAULT_TASK_PRIORITY = TaskPriority.NORMAL class HistoryEntryType(StrEnum): """Type of history entry.""" COMPLETED = "completed" SKIPPED = "skipped" MISSED = "missed" RESET = "reset" TRIGGERED = "triggered" TRIGGER_REMOVED = "trigger_removed" TRIGGER_REPLACED = "trigger_replaced" class MaintenanceFeedback(StrEnum): """Feedback from user about whether maintenance was needed.""" NEEDED = "needed" NOT_NEEDED = "not_needed" NOT_SURE = "not_sure" class TriggerEntityState(StrEnum): """State of a trigger entity from the integration's perspective.""" AVAILABLE = "available" UNAVAILABLE = "unavailable" # Entity exists but state is unavailable/unknown MISSING = "missing" # Entity does not exist in state machine STARTUP = "startup" # Grace period after HA start # --- Dispatcher Signals --- SIGNAL_TASK_RESET = f"{DOMAIN}_task_reset_{{entry_id}}_{{task_id}}" SIGNAL_NEW_OBJECT_ENTRY = f"{DOMAIN}_new_object_entry" SIGNAL_OBJECT_ENTRY_REMOVED = f"{DOMAIN}_object_entry_removed" # Fired by the DocumentStore after any metadata/blob change so the storage # sensor (and any other listener) can refresh without polling. SIGNAL_DOCUMENTS_UPDATED = f"{DOMAIN}_documents_updated" # --- Trigger Completion Cooldown --- TRIGGER_COMPLETION_COOLDOWN_SECONDS = 600 # 10 minutes # Household double-complete window: two people tapping Complete on the same # task within this many seconds count as ONE real-world action (journey M1). # Short on purpose — a deliberate complete → reset → complete-again correction # flow must never be caught by it. MANUAL_COMPLETION_DEDUP_SECONDS = 30 # --- Input Validation Limits --- MAX_NAME_LENGTH = 200 MAX_TEXT_LENGTH = 2000 # notes, reason, feedback, description 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 MAX_TYPE_LENGTH = 50 # task_type, schedule_type MAX_CHECKLIST_ITEMS = 100 MAX_CHECKLIST_ITEM_LENGTH = 500 MAX_LABELS = 25 # cross-cutting tags per task MAX_LABEL_LENGTH = 40 # single label/tag MAX_GROUP_TASK_REFS = 200 MAX_ID_LENGTH = 64 # entry_id, task_id, group_id (uuid hex = 32) 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 MAX_SCHEDULE_TIME_LENGTH = 5 # "HH:MM" # --- Trigger Entity Availability --- STARTUP_GRACE_PERIOD_SECONDS = 300 # 5 minutes MISSING_ENTITY_THRESHOLD_REFRESHES = 6 # ~30 min at 5-min intervals