86 Commits

Author SHA1 Message Date
Tony C
770c7e2701 Snap-Docker install fallback, env-var URL diagnostics, and server.py tests (#396)
* Same release process doc tweaks/fixes.

* Changed install.sh (and related) to handle docker permission issues.

Docker installed via `snap` cannot read from dotfile directories.

* Warnings for bad URLs in environment variables. Tests for server.py

* Fixed two-story attic profile background SVG (missing floor).
2026-06-03 20:48:56 -05:00
Tony C
52999d9e99 Make docker compose the ongoing management surface (#382) (#394)
* Add docker compose templates and env-var drift check (#382)

Phase 1 of issue #382 (docker compose support):

Repo-root templates for users integrating HI into their own compose stack:
 - docker-compose.example.yml — published image, container_name: hi, $HOME/.hi
   volume defaults; no healthcheck stanza (Dockerfile's HEALTHCHECK applies)
 - local.env.example — generated from env-generate.py --example; preamble
   explicitly warns install.sh users not to drop it at ~/.hi/env/local.env

Drift prevention across the three env-var sources:
 - install.sh gains --list-env-vars, extracting names from its own heredoc via
   a unique terminator (INSTALL_ENV_FILE_EOF) so the listing cannot drift from
   what the script actually writes
 - env-generate.py gains a SETTING_SECTIONS canonical declaration that seeds
   self._settings_map and drives --example output; validate_settings() runs
   before _write_file() and fails the run on undeclared keys or unset values
 - deploy/env-drift-check.sh compares the three name sets and prints a clean
   labeled diff on mismatch
 - Wired into make env-drift-check, make check, and a CI step in
   django-tests.yml

install.sh SECRET_KEY charset narrowed to exclude characters that can confuse
docker compose's env_file parser (\", ', \\, \$, #, =, \`).

* Add docker compose path to install.sh and update.sh (#382)

Phase 2 of issue #382 (docker compose support):

install.sh:
 - check_docker_compose probes `docker compose version` (no install offer,
   no platform branching)
 - create_compose_file writes a fully-resolved compose file to
   ~/.hi/docker-compose.yml with container_name: hi so legacy
   `docker logs/stop/start hi` work identically across both code paths
 - Existing compose file is backed up to .BAK.<timestamp> before overwriting
   (protects hand-edits for reverse-proxy labels, custom networks, etc.)
 - start_container branches on HAS_COMPOSE: compose up -d when available,
   original docker run when not
 - show_success adds docker restart hi and the update.sh canonical update path

update.sh:
 - Same check_docker_compose pattern
 - update_via_compose runs `docker compose pull` then `up -d`
 - Branches when both compose is available AND ~/.hi/docker-compose.yml
   exists (pre-Phase-2 installs stay on the legacy recreation flow)

Container is named `hi` on both paths so post-install management commands
documented in `docs/Installation.md` work uniformly regardless of which
code path created the install.

* Split Installation.md into a simple-user doc and a Deployment.md (#382)

Phase 3 of issue #382 (docker compose support):

docs/Installation.md (290 → 123 lines):
 - Quick Installation → Next Steps (moved from the bottom) → Managing your
   installation → Updates → Environment Variable Changes → Removing your
   installation → Troubleshooting
 - "Managing your installation" fills the post-install management gap and
   uses only legacy `docker logs/stop/start/restart hi` commands, which work
   for both install-time code paths (the compose file is generated either
   way and container_name: hi is set on both paths)
 - Manual Installation section removed entirely; docs/dev/Setup.md already
   covers `make docker-build/run` for the from-source/developer audience
 - More Help points users at Deployment.md for advanced topics

docs/Deployment.md (new, 87 lines):
 - Network Access Configuration
 - Auto-Start on Reboot
 - User Management
 - Using docker compose directly (compose verbs as an equivalent alternative
   to legacy docker commands)
 - Integrating into your own compose stack (with the env-file format gotcha
   spelled out: no export, no shell quoting, no ${VAR} interpolation)
 - Pointer to the Integrations Guide

README.md:
 - "Need more control?" updated to point at both Installation.md and
   Deployment.md
 - Resources → Users list adds Deployment Options

* Code-review polish and add env-var ritual doc (#382)

deploy/env-generate.py:
 - validate_settings() moved from generate_env_file() into _write_file()
   so any future code path that writes the env file is guarded
 - Spacing fixes for project convention (inner spaces in update( { ... } ),
   sorted( extra ) / sorted( missing ))
 - "extra" error wording hints at typo in __init__ overlay as a possible
   cause, not just "add to SETTING_SECTIONS"
 - Drive-by colon spacing on the pre-existing HI_SUPPRESS_AUTHENTICATION
   overlay key

docs/dev/shared/environment-variables.md (new):
 - Documents the 4-place ritual when adding an env var dependency:
   EnvironmentSettings (server.py), SETTING_SECTIONS + value assignment
   (env-generate.py), install.sh heredoc, regenerated local.env.example
 - Explains what make env-drift-check covers (three sources) and
   what it deliberately does not (server.py — field names diverge by
   design; other os.environ.get callers — caught only by review)

src/hi/environment/server.py:
 - Pointer comment on EnvironmentSettings dataclass referencing the new doc

* Move dev init scripts into dev/ and make them self-locating

Two top-level scripts (init-env-dev.sh, init-claude.sh) carried hardcoded
personal paths that made them effectively unusable for other contributors,
and their visibility at the repo root suggested otherwise. Moved into
dev/ and rewritten to be portable:

dev/init-env-dev.sh:
 - Computes PROJ_ROOT via BASH_SOURCE so absolute paths to
   venv/bin/activate and .private/env/development.sh resolve regardless
   of the caller's working directory
 - Header comment now states the script must be sourced (the `return 1`
   failure paths only behave correctly when sourced)

dev/init-claude.sh:
 - Self-locates PROJ_ROOT the same way; the previous `cd ~/proj/hi`
   personal hardcode is gone
 - `export PATH="~/.local/bin:..."` → `export PATH="$HOME/.local/bin:..."`
   (tilde inside double quotes does not expand)
 - `gh auth status --hostname "$host"` → `--hostname github.com` (the
   prior `$host` was undefined)

Unrelated to issue #382; bundled on this branch for convenience.
2026-06-01 21:36:12 -05:00
Tony C
c30c0a99ac [Feature] External Reference framework — Linked Content tab (#392)
* Renamed "Attribute Reference" to "External Reference"

* New ExternalReference database models and integrations thumbnails.

* New external reference linking sans the UI display.

* Added thumbnail fallback path for paperless integration.

* First version of full new workflow for external reference linking

* Refactoring and fixes for new external references feature.

* Many refactors and code cleanup after implementation phase of #388

* Tests for new external referencer views.

* Simulator improvements for testing extrernal reference linking.

* External reference feature: bug fixes and UI refinements.

* Added hover for full thumbnail tooltip in reference picker modal.

* Added file attribute card reordering and tweaked UI.

* Refactoring of Entity/Locati0on edit modals for tabbed content.

* Added session var for external reference picker default.

* Comment cleanup for Issue #388

* Some fixes from pre-PR review of Issue #388.
2026-06-01 00:07:27 -05:00
Tony C
c752b41139 Add Immich integration (#381) + picker error-channel refactor (#383)
* First version of the Immich integration.

* Comment cleanup.

* Fixes from code review recommendations.
2026-05-30 18:59:23 -05:00
Tony C
156fb4c1d8 Comment cleanup -- guidelines, command/agent, backwards pass (#377)
* More comment fixes.

* Refined comments for: src/hi/integrations/importer/*

* Comment refinements for: src/hi/integrations/referencer/

* Comment improvement pass on integrations templates.

* Cleaned up comments in src/hi/apps/weather/*.py

* Comment cleanup for src/hi/apps/weather/weather_sources

* Comment cleanup for src/hi/apps/profiles

* Some cleanup around claude files.

* Fixed some typos in variable names in weather module.

* MInor code consistency cleanup.
2026-05-28 14:37:06 -05:00
Anthony Cassandra
b96bdb92dc First pass at comment cleanups. Many files. 2026-05-27 22:00:10 -05:00
Anthony Cassandra
fd62006c7b Added commenting guidelines and claude command/agent. 2026-05-27 19:15:01 -05:00
Tony C
40f932e088 Paperless-ngx integration (ATTRIBUTE_REFERENCE capability) (#375)
* ATTRIBUTE_REFERENCE capability + framework backend (#232 phase 1)

Adds the framework seam for the new ATTRIBUTE_REFERENCE
integration capability, parallel to the existing CONNECT and
IMPORT capabilities. Integrations advertising this capability
contribute a search-and-attach affordance to HI's attribute-edit
UI: the operator searches the upstream corpus, multi-selects
matching results, and HI creates one regular TEXT attribute per
selection on the host Entity or Location.

Pieces:

  - hi.integrations.enums.IntegrationCapability.ATTRIBUTE_REFERENCE
    joins CONNECT and IMPORT.
  - hi.integrations.referencer is the new package owning the
    per-capability abstract class (IntegrationAttributeReferencer),
    the picker-only result dataclass (AttributeReferenceResult),
    and the shared wire-field constants (WireField).
  - IntegrationGateway.get_attribute_referencer() returns None
    by default; integrations override to opt in.
  - Two POST endpoints under /api/integrations/referencer/:
    - search/<integration_id> dispatches a query to the
      integration's referencer and returns JSON results for the
      picker UI to render.
    - attach/<integration_id> takes a list of selections and an
      attribute-owning item (Entity or Location, keyed by
      ItemType) and creates one TEXT attribute per selection.
    Both views require edit-mode; the attach view rejects
    unsupported item types, malformed payloads, and stale
    references to integrations that no longer advertise the
    capability.

No UI yet — that's phase 2. With a stub referencer, the search
and attach endpoints are exercisable end-to-end and the test
suite covers both happy paths and error branches (empty query,
limit clamp/parse, upstream exception → 502, unknown integration
→ 404, unsupported item_type → 400, malformed selections → 400,
title truncation to model max_length, etc.).

* Phase 2: ATTRIBUTE_REFERENCE picker (HiModal + antinode)

Adds the operator-facing picker that searches an enabled
ATTRIBUTE_REFERENCE integration and attaches selected results as TEXT
attributes on an Entity or Location. The picker is a single
HiModalView with server-driven multi-select state; the picker URL
takes no integration_id and the form discovers / selects integrations
internally so the action-bar surface stays a single "Link" button
regardless of how many referencers are configured.

- Picker view (search re-render via antinode partial swap; attach via
  refresh_response). Multi-select state carried in the form across
  re-searches (selections_json + visible_url + result_url + remove_url).
- Picker modal + body templates; integration selector shown only when
  more than one referencer is enabled.
- has_attribute_referencers template tag; "Link" button wired into the
  Entity and Location edit content body action bars.
- View tests cover GET, search re-render, attach, multi-select state
  transitions, and stale-integration rejection.

* Phase 3: paperless-ngx simulator with parametric search responses

Adds a paperless-ngx stub simulator that responds to any documents
search with a synthetic, parametrically-shaped result list. Unlike
the entity-shaped simulators, paperless has no SimEntity rows — it
contributes only via ATTRIBUTE_REFERENCE, so its state is a small
ephemeral settings dataclass on the singleton (same lifecycle as
HomeBox's _api_version): result count, mime mix, thumbnails on/off,
snippets on/off, and optional artificial latency. Operator tunes
each knob from an extras-pane form that submits on change.

The result generator seeds its RNG from the query string so repeated
searches return the same set, and every title carries the query as
a visible breadcrumb so the developer can verify the picker is
wiring the search input through. /api/documents/<id>/thumb/ returns
an inline SVG when thumbnails are on and 404s when off, exercising
the picker's fallback-icon path; /documents/<id>/details/ stands in
for the per-document view paperless source URLs link to.

The shared services/pages/service.html now gates profile CRUD, the
ADD ENTITY menu, and the entity list on sim_entity_definition_list
being non-empty so ephemeral simulators don't render irrelevant
controls. Connect simulators with zero current entities are
unaffected — they still have definitions, so the empty-state alert
fires as before.

* Phase 4: paperless-ngx integration (ATTRIBUTE_REFERENCE)

First integration to declare only the ATTRIBUTE_REFERENCE capability.
Lets the operator search a paperless-ngx server from inside HI's
picker modal and attach matching documents as TEXT-attribute links
on existing Entity / Location records. No connector, no importer, no
monitors, no manager singleton — the gateway returns a referencer
and a single thumbnail proxy view bridges browser-embedded <img>
tags to the upstream API.

- PaperlessGateway, PaperlessAttributeReferencer, and a thin
  PaperlessClient over requests. Auth via Authorization: Token
  <value>. Trailing slash on the configured API URL is forgiving.
- ThumbnailProxyView fetches upstream with the configured token and
  streams bytes back so the picker's embedded thumbnails work
  without exposing the token to the browser. Document source URLs
  point directly at paperless's web UI — operators authenticate
  with paperless's own session when clicking the saved link.
- Snippet extraction is client-side (paperless returns full content
  per hit, no excerpt field): ~160-char window around the matched
  query with ellipses for clipped edges, leading-window fallback
  when the query does not match verbatim.
- Wire-format strings centralized in pl_models.PaperlessApi per
  project convention.
- Tests cover validation, client + factory (including disabled /
  missing-attribute paths), referencer translation + snippet
  extraction edge cases, gateway probe (200 / 401 / 5xx /
  connection error), and proxy view (unconfigured / upstream 404 /
  upstream auth failure / connection error / success).
- Per-integration docs added (user-facing + developer-facing) and
  linked from the integrations landing page; the lead-in copy is
  updated to acknowledge the new "attachable references" shape
  alongside the existing CONNECT / IMPORT integrations.

* Phase 5: Content Sources tab + CapabilityGateway abstraction

Adds a Content Sources config tab for ATTRIBUTE_REFERENCE-capability
integrations (paperless-ngx). Sibling to the renamed Connectors tab.
Each tab now belongs to exactly one capability — the two surfaces are
semantically different (live mirror vs. on-demand reference) and
sharing a page was forcing capability awareness into framework chrome.

Page model: per-integration sidebar nav, attribute form with a
single ENABLE/UPDATE primary action that always runs schema +
upstream-access validation, and a DISABLE button (when enabled) that
rides the same form via an ``action=disable`` POST. ENABLE flips
is_enabled to True only after the upstream probe succeeds (atomic
"nothing changes on failure" semantics). DISABLE skips validation
and just flips is_enabled to False. The framework re-renders the
form area in place after each action, so the status badge and the
primary-button label update without a full page reload.

Architectural refactor: introduce ``CapabilityGateway`` as the
shared base of ``IntegrationConnector`` / ``IntegrationImporter`` /
``IntegrationAttributeReferencer``. The three peers had nothing in
common before, leaving cross-capability concerns (description text,
action-bar template fragment) with no natural home. The new base
carries ``capability`` (class attribute), ``get_description()``, and
``get_attribute_actions_template_name()`` — each capability owns
its own defaults. The connector's existing sync-flow ``get_description
(is_initial_connect)`` is renamed to ``get_sync_description`` so it
does not collide with the general capability description.

``IntegrationAttributeItemEditContext`` now takes a
``CapabilityGateway`` instance instead of a capability enum and
exposes it in the template context, so
``integrations/panes/integration_edit_content_body.html`` is now
capability-agnostic: a single ``{% include %}`` of whatever
fragment the active capability returns. The previous inline
CONNECT-only health badge moves into
``connect_attribute_actions.html``.

attr.js threads the submit ``event.submitter`` into ``FormData``
so multi-submit-button forms (e.g., the new ENABLE / DISABLE form)
include the clicked button's ``name=value`` in the POST body. This
matches native browser behavior.

UI labels: the existing ``Integrations`` tab becomes ``Connectors``
(internal CONNECT routes / module names unchanged); the new tab is
``Content Sources`` (code naming uses ``reference`` to align with
the capability and the surrounding ``referencer/`` directory).

* Content Sources page: source description, label-stale fix,
history/restore view capability cleanup

- Per-source description below the integration title: "Find related
  content in <label> and link to it." Uses the integration label
  directly so future ATTRIBUTE_REFERENCE integrations get sensible
  copy automatically.
- Rebuild the attribute edit context after the first-time ENABLE
  flips is_enabled to True; otherwise the async response re-rendered
  with the stale "ENABLE" button label and disabled-state action bar
  even though the integration was now enabled.
- IntegrationAttributeHistoryInlineView / IntegrationAttributeRestoreInlineView
  pass capability_gateway=None instead of get_connector(). These ops
  target a specific attribute by id, so the capability filter inside
  the edit context is never consulted — the previous hardcoded
  CONNECT was a no-op that silently mismatched any non-CONNECT
  integration.

* Apply attribute action-bar button styling to anchor-styled buttons

The .attr-v2-action-buttons CSS targeted only <button> elements, so
the new LINK anchor on entity / location edit pages rendered smaller
and top-aligned instead of matching its <button> siblings (Add File,
Add Info). Extend the selectors to cover a.btn inside the action
bar and add explicit inline-flex centering so the icon and text
line up with the other buttons.

* Picker: JS-owned selection state, split search / attach endpoints

The previous picker maintained selection state on the server, which
meant ticking a result card did nothing visible until the next form
submit — operators saw "Add 0 References" stuck disabled after their
first check and assumed the picker was broken. Multi-selection now
behaves as one transaction: JS owns the in-memory selection set,
search calls are async within that state context, and the only POST
that touches the server is the final Add submit.

attr-picker.js (new, in js_hi_grid_header_content) owns:
  - The selection set (per-modal WeakMap keyed by the picker DOM).
  - Chip-row rendering (the one piece of HTML the JS generates).
  - The Add button's label and enabled state.
  - The async search request: POST to a new search endpoint that
    returns ONLY the result-cards partial; JS swaps it into the
    results container and re-applies checked state to any newly-
    visible result whose URL is already selected.
  - The Add submit: serialize the selection set into a hidden
    selections_json input, then route through antinode's public
    API (AN.hideModalIfNeeded + AN.post) so modal cleanup and the
    {refresh: true} response flow through the framework. Form
    deliberately omits data-async because antinode binds at <body>
    level and would otherwise race attr-picker's document-level
    submit handler.

Server endpoints split: picker GET stays, plus new search and
attach POST endpoints. Old multi-select bookkeeping
(_compute_selections, visible_url[], remove_url, action=attach
button) deleted along with the WireField class.

Client/server constant sharing now follows the project's
established DIVID pattern: ATTR_PICKER_* entries in
hi.constants.DIVID (server) and Hi.ATTR_PICKER_* in main.js
(client). Templates, view code, and JS all read from these — no
more magic strings to drift between sides.

Simulator fix: paperless's _generate_results used document_id =
index + 1, which meant different queries produced overlapping ids
(and thus URLs). The JS keys selections by source_url, so the first
result of query A and the first result of query B were treated as
the same document. Document id is now a hash of (query, index) so
different queries produce distinct ids while repeating the same
query stays stable — matching real paperless's "same document
re-found in multiple searches" behavior.

CSS: a.btn-styled buttons inside .attr-v2-action-buttons inherit
the same min-height / padding / font-size / border-radius / focus /
hover / icon-spacing rules as their <button> siblings, so the
anchor-styled LINK button on Entity / Location edit pages lines up
with Add File and Add Info.

* Picker: footer-anchored chips + Add, sticky search, seeded initial query

Layout reshape using Bootstrap's built-in ``modal-dialog-scrollable``
opt-in (only the picker modal opts in; other modals are unaffected):

- Footer is now the commitment zone. It holds the selection chip
  row above a Cancel/Add row. The footer stays anchored regardless
  of how far the operator scrolls through results, so the
  accumulated selections and the Add button are always visible
  together.
- Body holds only the search form and the results container. The
  search form sticks to the top of the body's scroll region
  (position: sticky) so refining the query stays one click away
  no matter how far down in the result list the operator is.
- The picker-root class moved up to the modal-dialog so JS handlers
  in both body (result cards) and footer (chip-X buttons, attach
  form) can ``closest()`` to the same picker instance.

CSS additions, scoped to ``modal-dialog-scrollable`` so the
flex-shrink contract works correctly with this project's custom
``.modal-title`` (vs. Bootstrap's default ``.modal-header``):

- ``.modal-dialog-scrollable .modal-title``, ``.modal-footer`` get
  ``flex-shrink: 0`` so the title and footer keep their natural
  height when the body grows. Without this the title squashed to
  near-zero height on tall result lists.
- ``.hi-attr-picker .modal-body { padding-top: 0 }`` so the sticky
  search form sits flush against the title — otherwise the
  modal-body's default padding-top showed scrolling content
  through the gap above the sticky element.

Initial-render seeded with owner's name:

- The picker GET now runs the same ``_search_upstream`` the search
  endpoint uses, with ``owner.name`` as the query, and seeds the
  results container with the resulting ``picker_results.html``
  partial. Operator opens the picker and sees relevant matches
  immediately — no retype, no extra click. Same template + same
  context variables (``query``, ``results``) as the search
  endpoint so initial-render and re-search-render are structurally
  identical.

* Picker: source banner always rendered, wider modal, "Link Content" copy

The picker UI now has the same shape for one source or many — the
modal doesn't change structurally when a second ATTRIBUTE_REFERENCE
integration is configured.

- Source banner: replaces the previous conditional ``<select>`` in
  the multi-source case + hidden input in the single-source case.
  Always-rendered Bootstrap dropdown whose button face shows
  ``[logo] [name] [caret]``. With one source the caret is hidden
  and the button is disabled (the visual stays consistent without
  inviting a no-op click). On selection: attr-picker.js updates the
  banner face, sets the hidden ``integration_id`` input, and
  re-submits the search form — natural for "search the current
  query against a different source."

- Modal width bumped to ``hi-modal-dialog-700`` so the title bar
  fits "Link Content" and the result cards have room to breathe.

- Operator-facing copy aligned with the "Content Sources" tab:
  modal title is now constant "Link Content" (was conditional
  "Link References" / "Add from <name>"); attach button reads
  "Add N Link(s)"; the chip-row empty hint reads "No content
  selected yet."; the action-bar button on Entity / Location
  edit pages reads "Link Content".

- DIVID + main.js gain matching ``ATTR_PICKER_SOURCE_*`` entries
  for the banner classes, the dropdown-item class, and the three
  data attributes the dropdown items carry (source id, logo URL,
  label) so the JS handler can read them without re-hardcoding
  any strings.

- CSS keeps the banner on white so source logos meant for
  light backgrounds render correctly (the modal title bar uses a
  colored background and would clash). Single-source modifier
  class hides the caret and removes the disabled-button visual
  fade.

* Align operator-facing copy with Connectors / Content Sources terminology

When the config tab was renamed from "Integrations" to "Connectors"
(and the new "Content Sources" tab was added for ATTRIBUTE_REFERENCE
integrations), several operator-facing strings continued to use the
older "Integration(s)" wording. Updated to match the page labels
operators now see:

- Connectors page sidebar button: "INTEGRATIONS" → "CONNECTORS"
- Empty-state copy on the Connectors page: "no integrations
  currently configured" / "CONFIGURE INTEGRATIONS"
- The "All Integrations" picker modal title → "All Connectors"
- Disable-confirm modal: "Disable {label} Integration?" /
  "This integration has..." / "detached from this integration" /
  "If you re-configure this integration later..." → use "connector"
  consistently.
- Pre-sync confirm modal: same "This integration has..." /
  "detached from this integration" → "connector".
- Data Import card note: "Also available as an Integration." →
  "Also available as a Connector."
- Data Import empty state: "No integrations support Data Import."
  → "No data importers currently defined." (the prior wording was
  technically accurate but ambiguous given the new tab labels;
  the new wording matches what the operator is looking at).
- "Data Import vs. Integration" help modal → "Data Import vs.
  Connector"; body text refers to the contrast as Data Import vs.
  Connector and uses "external system" instead of "upstream source".

Internal code identifiers (``IntegrationGateway``,
``integration_id``, ``ConfigPageType.INTEGRATIONS_CONNECT``, etc.)
unchanged — they were never operator-visible.

* Added new link icon and used for new attribute referrer integration.

* Lift get_metadata onto CapabilityGateway base

``IntegrationConnector``, ``IntegrationImporter``, and
``IntegrationAttributeReferencer`` all returned the integration's
``IntegrationMetaData`` constant but disagreed on the method name —
the connector used ``get_integration_metadata`` while the other two
used ``get_metadata``. Same fact, three implementations, two names.

Move the contract onto ``CapabilityGateway`` as a single abstract
``get_metadata()``, drop the duplicate declarations from the
importer / referencer bases, and rename the connector's variant.

- IntegrationConnector: ``get_integration_metadata`` →
  ``get_metadata`` (with the three internal call sites updated).
- IntegrationImporter / IntegrationAttributeReferencer: declaration
  removed; inherited from CapabilityGateway.
- HASS / ZoneMinder / Frigate / HomeBox connectors: method renamed
  to ``get_metadata``.
- test_integration_connector.py / test_sync_check.py: fixture
  stubs renamed to match.

The ``_get_integration_metadata`` private helper in
``integration_tags.py`` is unrelated (it takes an
``IntegrationDetailsModel``, not a gateway) and untouched.

* Some integration modal cleanup

* File name normalization for new integrations referencer.

* Refresh integration-guidelines.md for current capability model

The doc had drifted in several places:

- Said "two capabilities exist today." Three do — ATTRIBUTE_REFERENCE
  is the third (paperless-ngx). Added the entry plus a note that
  ATTRIBUTE_REFERENCE-only integrations land on the sibling "Content
  Sources" tab rather than "Connectors."
- Said the per-capability classes "don't share a base class —
  commonality is composed through shared helpers." They do now:
  ``CapabilityGateway`` (``hi/integrations/capability_gateway.py``)
  is the shared base, carrying ``capability``, ``get_metadata``,
  ``get_description``, and ``get_attribute_actions_template_name``.
- Setup steps 3-5 referenced an obsolete ``IntegrationType`` enum,
  an ``activate / deactivate / manage`` method surface, and an
  ``integration_factory.py`` that doesn't exist. Rewrote those steps
  to describe declaring ``IntegrationMetaData`` with the right
  capability set, implementing the actual gateway methods
  (``get_metadata``, ``get_<capability>``, ``validate_configuration``,
  ``validate_access``, ``notify_settings_changed``), and the
  auto-discovery flow that replaces factory registration.
- Gateway Implementation Patterns described the old
  activate/deactivate/manage contract and dict return shape — neither
  still exists. Replaced with a short summary of the current method
  contract.
- Error Handling listed exceptions that don't exist
  (``ConnectionError``, ``AuthenticationError``,
  ``DataValidationError``) and missed the ones that do
  (``IntegrationDisabledError``, ``IntegrationAttributeError``,
  ``IntegrationConnectionError``). Corrected.
- Key Base Classes & Modules had wrong dotted paths
  (``hi.integration.*`` → ``hi.integrations.*``,
  ``hi.utils.singleton`` → ``hi.apps.common.singleton``),
  referenced a non-existent ``IntegrationStatus`` enum and the
  obsolete factory module. Replaced with the current set, added
  ``CapabilityGateway`` and ``IntegrationManager``.
- Example Integrations listed three; added Frigate and paperless,
  and noted that paperless has a slimmer file layout because
  ATTRIBUTE_REFERENCE-only integrations have no monitors / sync /
  converter / manager.
- File layout section gained a short paragraph noting which
  role-files apply only to CONNECT/IMPORT-shaped integrations.

* Accept single-label intranet hostnames in attribute URL linkification

Django's URLValidator rejects hostnames without a TLD (e.g.
http://cassandra:4100/...), so URLs pointing at LAN hosts saved as
attribute values were not rendered as clickable links. Replace the
strict validator with a permissive urlparse-based check (http(s)
scheme + non-empty netloc) so intranet single-label hostnames are
linkified just like IPs and FQDNs.

* Refresh paperless-ngx docs for Content Sources tab and slim dev page

User-facing changes follow the post-#232 UI: paperless is configured
from the Content Sources tab (not Connectors), the action button on
item/Location edit pages reads 'Link Content', and the picker's
attach button reads 'Add N Links'. Integrations.md routes paperless
to its own walkthrough rather than folding a non-Connectors flow into
the shared steps.

Dev page refocused on design rationale (capability shape, thumbnail
proxy vs source-URL passthrough, no-manager, snippet strategy,
single-deployment assumption). Dropped module/test enumerations and
wire-format strings — those rot and a developer can read the
directory or pl_models.py.

* Connectors vocabulary sweep in capability-blocked modal and selector

The capability-block modal still emitted 'GO TO INTEGRATIONS' /
'disable the … Integration' / 'configured as Integration with',
contradicting the new three-tab operator vocabulary (Connectors /
Content Sources / Data Import). The empty state in the integrations
selector also still said 'No integrations found' under an 'All
Connectors' title. Aligned both to 'Connector(s)' and updated the
matching importer-view test assertion.
2026-05-27 12:22:47 -05:00
Tony C
e40521bd1c Rebalance EntityGroupType + hide delegates from edit sidebar (#367) (#369)
* Rebalance EntityGroupType buckets and close 18-type coverage gap (#367)

The prior bucket layout silently routed 15% of EntityTypes (FREEZER,
GARAGE_DOOR, LEAK_SENSOR, all pool types, etc.) through the OTHER
fallback because they had no explicit assignment, and conflated
heterogeneous types in OUTDOORS/UTILITIES/LIGHTS_SWITCHES. New layout
is 15 buckets organized by how a homeowner partitions their LocationView:
AUTOMATION (controllable end-points), SECURITY, SENSORS (new),
APPLIANCES (now subsumes HVAC), POOL (new), GENERAL (new — catchall
distinct from the silent OTHER fallback), and friends. Every EntityType
now has an explicit bucket assignment, pinned by a new invariant test.

* Hide delegate entities from edit-mode sidebar entity list (#367)

Placing a principal entity in a LocationView or Collection automatically
pulls its delegates along, and unplacing the principal removes them.
Showing the delegates as separately-toggleable rows in the same edit
sidebar invited confusing partial states (an Area added without its
proxy sensor, etc.).

EntityManager.create_location_entity_view_group_list and
CollectionManager.create_entity_collection_group_list now accept an
exclude_delegates kwarg; the two manage-items sidebars opt in. The
entity-pairing modal keeps the default and continues to surface
delegates so the relationship can be broken there.

* Refine EntityGroupType: drop OTHER, fold EntityType.OTHER into GENERAL (#367)

Three follow-up adjustments to the rebalance:
  - AREA moves from GENERAL to STRUCTURAL — areas are spatial
    regions of the home, structural by nature.
  - PUMP, MOTOR, SUMP_PUMP move from GENERAL to ELECTRICAL —
    they're electrically driven and don't fit a domain bucket.
  - EntityGroupType.OTHER is removed; EntityType.OTHER lives
    in GENERAL alongside AUTOMOBILE and CONSUMABLE. There's no
    longer a separate silent-fallback bucket — GENERAL is the
    explicit catchall, and the full-coverage invariant test
    ensures every EntityType has an assignment.
2026-05-26 07:43:52 -05:00
Tony C
ea991d691a Implement HomeBox Importer; refactor integration framework (#360)
* Phase 1 (#358): rename connect/ → connector/ and lift capability-agnostic files to umbrella root

Mechanical refactor with no behavior change, setting up the layout for
the new importer/ subpackage that arrives in Phase 2.

The connect/ subpackage is renamed to connector/ for symmetry with
the forthcoming importer/. Five files that were already capability-
agnostic are lifted from connector/ up to the integrations/ umbrella
root so neither capability subpackage will depend on the other:

  - placement_request.py
  - integration_attribute_edit_context.py
  - entity_operations.py
  - integration_data.py
  - integration_gateway.py

The remaining connector/ contents (views, synchronizer, monitors,
sync_check, sync_result, view_mixins, external_view_data, plus the
Connect-mode controller/manage-view-pane stubs and user_data_detector)
stay capability-specific.

view_mixins.py stays in connector/ as a single file; a future
refactor could split the capability-agnostic accessor methods up to
umbrella, but the call-site benefit is small and bundling that with
this rename would obscure the move.

The defensive Disable-side data_source predicate originally listed
in Phase 1 is deferred to Phase 7. The naive EXTERNAL filter would
break HA/ZM/Frigate Disable (their entities are INTERNAL), so the
guard needs to be conditional on the integration declaring both
capabilities — designing that alongside Phase 7's symmetric block-
modal capability-conditional detection.

* Phase 2 (#358): Importer protocol skeleton

Lands the abstract Importer base, transient models (CandidateItem,
IntegrationDiscardResult), and IntegrationImportResult dataclass.
Wires get_importer() into IntegrationGateway as the parallel of
get_synchronizer(), defaulting to None.

No live consumer yet — the HomeBox concrete implementation arrives
in Phase 3.

IntegrationImportResult is a parallel dataclass to
IntegrationSyncResult, not a subclass. Import-specific shape:
items_imported_count / items_skipped_count + imported_list +
info/error lists + placement_input. No reconnect/detach/remove
concepts (Import is add-only).

* Phase 3 (#358): HomeBox concrete Importer + capability-aware entity factory

Lands HomeBoxImporter as the first concrete consumer of the Phase 2
Importer protocol, plus the supporting infrastructure for it to share
HomeBox's entity-creation code with the CONNECT-mode synchronizer.

Capability-aware HbEntityFactory:
  - Moved from connector/ to shared/ (used by both Connect and
    Import now).
  - New ``capability`` parameter on create_models_for_hb_item routes
    to the correct flags: CONNECT keeps the read-only-mirror flags
    (can_user_delete=False, allow_internal_attributes=False,
    data_source=EXTERNAL); IMPORT sets HI-owned flags (deletable,
    editable, data_source=INTERNAL).
  - Fixes a latent post-#355 gap: data_source was never set at entity
    creation time. Phase 4 of #355 backfilled existing rows; new
    HomeBox-Connect entities created after #355 would have defaulted
    to INTERNAL until now.

HbConverter payload defaults flipped to CUSTOM + is_editable=True.
The Connect-mode copy-on-sync architecture (pre-#354) used these
methods to produce PREDEFINED read-only attributes. Post-#354, the
only live caller is the new Import code, which wants user-owned
attributes. The methods are now unambiguously Import-flavored.

HomeBoxImporter (services/homebox/importer/homebox_importer.py):
  - get_candidate_items: lightweight summary fetch, excludes archived.
  - run_import: per-entity transaction inside the existing
    integrations_sync exclusion lock; skip-if-already-imported by
    integration_name; per-item failures aggregate into error_list.
  - discard_imported_data: filters by data_source=INTERNAL so any
    coexisting Connect-mode entities are untouched.

Dormant hb_attribute_sync.py (224 lines) deleted. Replaced by a
~15-line populate_attributes_for_imported_entity helper next to the
existing HbImporter classmethods — Import is add-only so the
sync-style dispatch / update / remove logic no longer applies.

HbMetaData.capabilities now declares both CONNECT and IMPORT.
HomeBoxGateway.get_importer() returns the importer instance.

* Phase 4 (#358): Data Import config tab + Configure form (preview-side)

Lands the user-visible Data Import tab parallel to Integrations, plus
the configure-form workflow up through preview rendering. Confirm →
run is Phase 5.

New tab: ConfigPageType.DATA_IMPORT (url_name 'integrations_import_home',
label 'Data Import'). URL prefix is integrations_import_* — matches
the eventual rename of integrations_home → integrations_connect_home
the framework will adopt.

DataImportPageView: flat list, one row per IMPORT-capable integration.
Per-row CONFIGURE button (always) and DISCARD button (only when
imported entities exist for that integration_id with
data_source=INTERNAL). Dual-capability rows (HomeBox today) carry a
terse "Also available as an Integration" note linking to a bottom-of-
page explanation contrasting Data Import vs Integration (Connect).

ImporterConfigureView: credentials form (reuses
IntegrationAttributeItemEditContext with capability=IMPORT, action
button labeled IMPORT). POST validates configuration + access via
the same validate_attributes_extra_helper Connect uses, then fetches
candidates via Importer.get_candidate_items(), computes new vs.
skipped counts by integration_name match against existing HI
entities, and renders the preview modal.

Preview modal: "Would import N new items. M existing items would be
skipped." CONFIRM IMPORT and CANCEL actions. CONFIRM posts to the
integrations_import_run URL — stub view returns 501 in this phase;
Phase 5 wires the actual run.

ImporterRunView and ImporterDiscardView land as 501-returning stubs
so the URL reversals from this phase's templates resolve cleanly;
Phase 5 and 6 fill in the implementations.

view_mixins.py moved from connector/ up to umbrella root so both
Connect and Import views can extend IntegrationViewMixin. The
render_sync_result method on it is only called by Connect views;
Import views never touch it. No import-time Connect coupling — the
synchronizer.sync() call inside render_sync_result is runtime-only.

* Phase 5(a) (#358): Confirm → Run → Result modal + placement reuse

Wires the IMPORT workflow end-to-end. After the preview modal's
CONFIRM IMPORT, the importer runs, the result modal renders with
imported / skipped counts, and a "Place N new items" CTA hands off
to the existing placement flow when entities were created.

Importer base gains group_entities_for_placement (default ungrouped,
mirrors IntegrationSynchronizer's same-named method). HomeBoxImporter
stays with the default — HomeBox has no inherent grouping at v1
(Labels and Locations are metadata-on-entity).

HomeBoxImporter._run_import_locked now tracks created entities and
populates result.placement_input via group_entities_for_placement,
mirroring HomeBoxSynchronizer's _sync_impl pattern.

ImporterRunView.post:
  - Runs importer.run_import() inside a try/finally that invalidates
    the metadata + sensor-response caches afterward (mirrors
    Connect's render_sync_result).
  - Builds the placement URL via the existing PlacementUrlParams.
  - Reuses the existing integrations_placement URL — HomeBox is
    dual-capability so its synchronizer is available for the
    placement view's group_entities_for_placement call.

import_result.html: counts hero (Imported / Skipped), per-item
list, errors. "Place N new items" CTA on success; OK button on
the no-imports / errors paths. Modeled on sync_result.html but
trimmed for Import's narrower outcome shape.

Manual verification: end-to-end Import works after the manager
picks up newly-saved credentials. Phase 5(b) addresses the
is_enabled / manager-reload race that surfaces this timing issue
and the symmetric one in the Connect-enable flow.

* Phase 5(b) (#358): decouple Connect-mode is_enabled from credential loading

The HomeBoxManager (shared/) was checking the Connect-mode is_enabled
flag inside _load_attributes and raising IntegrationDisabledError to
nullify hb_client. That coupling produced two related bugs:

  1. Import workflow: after the user disables HomeBox-Connect,
     HomeBoxImporter.get_candidate_items found hb_client=None and
     returned empty list — preview showed 0 items even though the
     upstream simulator had 25.

  2. Connect-enable race: after IntegrationEnableView flipped
     is_enabled=True via enable_integration, the manager's
     post_save signal was deferred by ~0.1s
     (DelayedSignalProcessor). The synchronous synchronizer.sync()
     that immediately followed saw hb_client=None and failed with
     "integration is disabled" even though the user had just
     enabled it.

Fix:

  * Strip the is_enabled gate out of HomeBoxManager._load_attributes
    (and the matching except IntegrationDisabledError branch from
    _reload_implementation). The Connect-mode enable lifecycle is
    not a shared/ concern — credentials load whenever they are
    structurally valid so the IMPORT capability works regardless
    of Connect-mode state. Connect-side guards (manage view,
    disable, pause, resume) check integration.is_enabled directly
    at the view layer, where the capability context is known.

  * IntegrationManager.enable_integration: synchronously call
    gateway.notify_settings_changed() after the save+refresh,
    before the monitor launch. Closes the delayed-signal race so
    the synchronizer immediately sees fresh credentials.

  * ImporterConfigureView.post: same synchronous notify after
    post_attribute_form succeeds, before invoking the importer.
    Credentials just changed; the manager needs to pick them up
    before run_import.

HASS, ZM, Frigate managers are Connect-only and are left alone —
their is_enabled gate is fine because they have no other capability
that needs to bypass it.

Manual verification: Import workflow shows correct candidate count
with Connect disabled; Connect-enable runs sync successfully on the
first click.

* Phase 6 (#358): Discard flow

Wires the DISCARD button on the Data Import page. GET renders a
confirmation modal showing the count of imported items; POST runs
the importer's discard_imported_data and redirects back to the
Data Import page.

Single-action confirm (no SAFE/ALL variants). Imported items ARE
the user data — the Connect-side preserve-with-user-data semantic
doesn't apply.

Defensive scoping is in HomeBoxImporter.discard_imported_data
(Phase 3): the deletion query filters by data_source=INTERNAL, so
any coexisting Connect-mode (EXTERNAL) entities for the same
integration_id stay untouched. Confirmed via test.

Post-discard cache invalidations mirror the Connect-side Disable
cleanup: IntegrationMetadataCache + SensorResponseManager. Sensor-
response cache invalidation is symmetric with sync's post-write
behavior even though imported items don't have sensors today —
keeps the discard path defensive for future Importers that might
attach sensors.

* Clarify EntityDataSource semantics: every Connect-mode entity is EXTERNAL

The Phase 4 (#355) migration used `allow_internal_attributes=False`
as the proxy for EXTERNAL, which conflated two separate concerns:
"data is sourced from upstream" and "user can edit HI-side
attributes." Under the conflation, HA/ZM/Frigate-Connect entities
were marked INTERNAL even though their state is sourced from
upstream — only HomeBox-Connect (allow_internal_attributes=False)
got EXTERNAL.

The clarified semantics: data_source=EXTERNAL means data is
actively being sourced from an external upstream system, which is
true of EVERY Connect-mode integration regardless of whether HI
permits user-edited attributes on top. data_source=INTERNAL means
no live upstream link — Import-mode (HI-owned post-import) and
native entities.

| integration       | mode    | data_source |
|-------------------|---------|-------------|
| zm/frigate/ha     | connect | EXTERNAL    |
| homebox           | connect | EXTERNAL    |
| homebox           | import  | INTERNAL    |
| (native)          | n/a     | INTERNAL    |

Changes:

* Migration 0021_entity_data_source.py: backfill broadens to mark
  every integration-attached entity EXTERNAL. Adjusted in place
  rather than as a fix-up migration since 0021 hasn't been released
  outside local environments and has no downstream migrations.

* HassConverter, FrigateSynchronizer, ZmSynchronizer (both service
  and monitor entity creation): explicitly set
  data_source=EXTERNAL on new Connect-mode entities. Closes the
  post-#355 gap where the field wasn't being set at creation time
  for these integrations — the migration backfilled existing rows
  but new rows fell back to the INTERNAL default.

* HomeBoxConnector.get_external_view_data: gates on
  data_source=EXTERNAL. Import-mode HomeBox entities are HI-owned
  after import, so the live HomeBox view isn't authoritative for
  them and would mislead the operator.

* test_models.py: migration-backfill test renamed and assertions
  updated to reflect the broadened predicate (HA-like entity now
  becomes EXTERNAL too).

* test_hb_connector.py: HomeBox entity fixture sets
  data_source=EXTERNAL so the connector's new gate doesn't suppress
  the connector's existing test surface.

Required for Phase 7 (#358 block modal): the modal queries
data_source to distinguish Connect-mode entities (EXTERNAL) from
Import-mode entities (INTERNAL) for an integration. The old
semantics would have miscategorized future dual-capability HA/ZM/
Frigate.

* Phase 7 (#358): block modal for cross-capability mode switching

When an operator starts an integration's initial-Configure flow for
one capability while existing data from the other capability is
still present, surface a block modal that routes them to remediate
before proceeding. Connect ↔ Import switches are intentionally made
harder — the user navigates explicitly, performs the cleanup
themselves, then re-clicks CONFIGURE. No combined transition flow.

CapabilityBlockViewMixin (in hi/integrations/view_mixins.py):
  - Single helper render_capability_block_if_conflict that takes
    only integration_data + capability_being_initiated. The mixin
    composes all user-facing copy (title, body clauses, navigation
    link) from per-capability config dicts so block modals read
    uniformly across both directions and any future capabilities.
  - Detects the OTHER capability automatically. Only fires when
    the integration declares both capabilities; single-capability
    integrations get None.
  - Counts entities of the OTHER capability's data_source for the
    integration; zero means no conflict.

Connect side: IntegrationEnableView.get checks the block on the
initial-Connect path (is_enabled=False). Re-Configure of an
already-enabled integration always proceeds — that's not a new
mode being initiated.

Import side: ImporterConfigureView.get checks the block when no
Import entities yet exist for the integration. Re-import (entities
already present) always proceeds — that's the standard incremental-
import flow.

capability_blocked.html: generic block modal template parameterized
by title + body clauses + link. Extends modals/action_2_with_cancel
so the inherited CANCEL just dismisses.

Single-capability integrations (HA/ZM/Frigate today) never trigger
the block: the mixin's "other capability not in capabilities" guard
short-circuits before any DB query.

* Phase 8 (#358): documentation for the Data Import capability

User-facing:
  * docs/DataImport.md (new) — landing page for the Data Import
    feature; importing/discarding walkthrough; lists supported
    integrations. Concept-level prose only; no hardcoded button
    labels so the doc doesn't drift if UI strings change.
  * docs/Integrations.md — Integrations vs. Data Import comparison
    pointing at the new landing page.
  * docs/integrations/homebox.md — added Data Import section
    describing the alternative mode and when to choose it.

Developer-facing:
  * docs/dev/integrations/data-import.md (new) — framework
    orientation for the IMPORT capability. Lists where code lives
    (importer/, view_mixins.py, services/homebox/importer/), how a
    gateway opts in, and the key design decisions (per-entity
    transaction, skip-by-integration_name, shared lock,
    data_source as capability-state signal). Reference-style;
    points at the code rather than restating API.
  * docs/dev/integrations/integration-guidelines.md — Capability
    Model subsection refreshed: CONNECT and IMPORT now both have
    concrete pointers; the #355 "reserved for #358" placeholder
    is gone.
  * docs/dev/integrations/homebox.md — updated stale module paths
    to reflect the connector/importer/shared split landed in #355
    Phase 3 + #358 Phase 1. Added a short Importer section
    pointing at the framework doc.

* Integration framework cleanup: capability-aligned naming and template structure

Rename the Connect/Import vocabulary uniformly across the integration
framework now that HomeBox is the dual-capability archetype.

Class renames:
- IntegrationSynchronizer -> IntegrationConnector (+ Hass/Zm/Frigate/HomeBox subclasses)
- Importer -> IntegrationImporter
- IntegrationEnableView -> ConnectorConfigureView
- HomeBoxConnector (the per-entity external-view resolver) -> HomeBoxExternalViewResolver

Gateway method:
- IntegrationGateway.get_synchronizer() -> get_connector()

File renames (git mv, history preserved):
- integration_synchronizer.py -> integration_connector.py
- importer.py -> integration_importer.py
- homebox/connector/hb_sync.py -> homebox_connector.py
- homebox/connector/hb_connector.py -> hb_external_view_resolver.py
- corresponding test files

URL name renames:
- integrations_home -> integrations_connect_home
- integrations_enable -> integrations_connect_configure

Enum renames (ConfigPageType):
- INTEGRATIONS -> INTEGRATIONS_CONNECT
- DATA_IMPORT -> INTEGRATIONS_IMPORT

View mixin:
- IntegrationViewMixin.get_integration_data(integration_id) -> (request, *args, **kwargs)
  Mirrors EntityViewMixin.get_entity; pulls integration_id from kwargs,
  raises BadRequest if missing and Http404 if not registered. ~20 call
  sites adopted the uniform pattern.

Template restructure (src/hi/integrations/templates/integrations/):
- import/ -> importer/ (avoids Python reserved word, matches sub-package convention)
- importer/ now has modals/ subdir matching the pages/ convention
- New connector/{modals,pages,panes}/ subtree for Connect-specific templates
- Top-level modals/, panes/ now hold only genuinely shared cross-capability
  templates (capability_blocked, placement flow, attribute-form body)
- Editor backup .html~ files removed

Misc:
- IntegrationImportResult folded into importer/transient_models.py
  (single-class file with no module-level documentation)

* Remove dormant IntegrationManageViewPane seam; rename to ConnectorManageView

The per-integration "manage view pane" extension point on
IntegrationGateway was dead machinery: all four service subclasses
returned templates containing only a {% comment %} block. The seam
also pre-dated the capability model — its return type lives under
connector/, but the hook was on the capability-agnostic gateway.

Rather than relocate the hook to IntegrationConnector, delete the
seam entirely (base class, 4 subclasses, 4 stub templates, gateway
method, view context plumbing, template include). It can be
reintroduced correctly on IntegrationConnector when an integration
actually needs per-integration content in the manage page.

Also rename the view to reflect that the Manage page is Connect-
specific:
- IntegrationManageView -> ConnectorManageView
- integrations_manage URL name -> integrations_connect_manage

* Push Connect-specific hooks from IntegrationGateway to IntegrationConnector

IntegrationGateway accumulated eight Connect-mode hooks over time, all
returning types that already lived under connector/. Now that the
capability model has IntegrationConnector and IntegrationImporter as
peer capability classes (cf. the recent IntegrationManageViewPane
cleanup), these hooks belong on the connector — not on the
capability-neutral gateway. Import-only integrations should not be
forced to stub them out.

Methods moved from IntegrationGateway to IntegrationConnector:
- get_monitor()
- get_controller()
- get_health_status_provider()
- get_entity_video_stream()
- get_entity_video_snapshot()
- get_sensor_response_video_stream()
- get_sensor_response_event_snapshot_url()
- get_external_view_data()

Per-service implementations relocated from each *Gateway (integration.py)
to the corresponding *Connector.

Production call sites updated to chain through get_connector() with
None-guards where the caller might encounter an Import-only or
connector-less integration. The three Connect-side view sites trust the
structural invariant. The do_control path raises a clear RuntimeError
if no connector or controller is available.

File renames to match the relocated class names:
- frigate_sync.py -> frigate_connector.py (FrigateConnector)
- hass_sync.py -> hass_connector.py (HassConnector)
- zm_sync.py -> zm_connector.py (ZmConnector)
- corresponding test files

IntegrationGateway's surface is now its capability-neutral core:
get_metadata, notify_settings_changed, validate_configuration,
validate_access, get_connector, get_importer.

* Fix re-Configure credentials race; remove redundant block-check pre-gates

Two related issues in the Configure flows:

1. ConnectorConfigureView.post relied on enable_integration to fire
   notify_settings_changed(), but enable_integration only fires it on
   the disabled->enabled transition. Re-Configure of an already-enabled
   integration left the singleton manager holding stale credentials,
   and the subsequent sync ran against them. The post_save signal would
   eventually nudge via DelayedSignalProcessor, but the 0.1s delay
   races the immediate sync. Lift the synchronous notify out of
   enable_integration and into the view, right after post_attribute_form,
   so both initial-Connect and re-Configure get the nudge.

   The matching ImporterConfigureView.post already did this; the
   Connect side was the asymmetric outlier.

2. Both Configure views had a pre-gate that suppressed
   render_capability_block_if_conflict in certain re-entry paths
   (is_enabled=True for Connect, has_imported=True for Import).
   Under the mode-switch invariant the pre-gate is redundant — the
   block check itself short-circuits to None when there's no
   conflicting data. The pre-gate's only effect was to silently
   bypass the block check when the invariant was violated (mixed
   INTERNAL+EXTERNAL state), letting the user compound the
   corruption. Remove both pre-gates so the block fires in any
   mixed-state scenario and the user is told to remediate.

Two tests deleted that asserted the now-removed bypass behavior:
- test_get_import_not_blocked_when_already_imported
- test_already_enabled_does_not_trigger_block

The canonical block-fires-on-conflict and re-Configure-works paths
remain covered by neighboring tests.

* Extract CapabilityConfigureView shared base for both Configure modals

ConnectorConfigureView and ImporterConfigureView shared a 5-step GET
recipe (block check → ensure_all_attributes_exist → build edit context
→ render modal) and a 3-step POST recipe (post_attribute_form → error
short-circuit → success handler). Pull the shared structure into a
new framework-level CapabilityConfigureView in
hi/integrations/views.py. Subclasses now declare four class constants
(capability, button_label, template_name, error_title) and override
handle_post_success() with the per-capability behavior.

ConnectorConfigureView.handle_post_success: enable_integration, then
sync or redirect-to-manage.
ImporterConfigureView.handle_post_success: fetch candidates, compute
new-vs-skipped, render preview.

Each subclass owns the timing of notify_settings_changed() because
the right moment is capability-specific:
  * Connect-side managers (Frigate/Hass/ZM) gate client (re)build on
    integration.is_enabled, so the notify must fire AFTER
    enable_integration — otherwise the manager reloads with
    is_enabled=False, nulls its client, and the immediately-following
    sync sees no client.
  * Import flows fire the notify before reading candidates.

The unconditional call in both subclasses also covers the re-Configure
race (already-enabled integration), where enable_integration
early-returns without nudging and the singleton manager would
otherwise keep stale credentials.

* Flatten homebox/shared/ up one level

The capability-layout convention is: inter-capability files live at
the integration's top level (alongside integration.py / apps.py /
enums.py), while capability-specific files live under connector/ or
importer/. HomeBox's shared/ predated the convention; it's the last
holdout. Move the six modules up:

  homebox/shared/hb_client.py         -> homebox/hb_client.py
  homebox/shared/hb_client_factory.py -> homebox/hb_client_factory.py
  homebox/shared/hb_converter.py      -> homebox/hb_converter.py
  homebox/shared/hb_entity_factory.py -> homebox/hb_entity_factory.py
  homebox/shared/hb_manager.py        -> homebox/hb_manager.py
  homebox/shared/hb_models.py         -> homebox/hb_models.py

shared/__init__.py removed; shared/ directory gone. 21 absolute
import-site rewrites plus 2 @patch strings updated. Docs and simulator
docstring path references brought in line.

* Data Import UI/UX refinements

Tab order
- Move Data Import to the right of Triggers in the Config tabs (matches
  natural read order: settings -> integrations -> trigger setup ->
  data import -> system info).

Import preview modal (HomeBox confirm)
- Title becomes "<Label> Import" when there's nothing to confirm
  (instead of the awkward "Import from HomeBox?" question).
- Body now handles the no-new-items cases explicitly: "All N upstream
  items have already been imported." or "No items found to import."
  instead of "Would import 0 new items."
- Single centered DONE button replaces the lone left-aligned CANCEL
  when the preview is informational only.
- Add data-async="modal" to the CONFIRM IMPORT form so it submits
  through the modal pipeline like every other action on the page.
- Show a contextual "Currently N items imported from <Label>." line
  whenever the integration has prior imports — signals re-import
  vs. first-time state. View filters the count by data_source=INTERNAL.

Data Import page layout
- Replace sparse rows with a responsive Bootstrap card grid
  (col-12 col-sm-6 col-md-4 col-lg-3), 4 columns on large screens.
  Logo centered above the integration name, action buttons stacked
  at the card bottom (CONFIGURE always; DISCARD only when has_imported).
- New hi-integration-logo--card modifier (72px) for the card-prominent
  size.
- Drop btn-control width:100% from the action buttons (the row layout
  no longer benefits from it; btn-block within the card stack is what
  fills card width now).
- Remove the always-below-the-fold "Data Import vs. Integration"
  footer block.
- Add DataImportInfoView (HiModalView) at integrations_import_info
  rendering a static info modal with the same explanation content.
- On dual-capability rows, the entire "Also available as an
  Integration. <info-icon>" message is now a single clickable link
  that opens the info modal.

Capability-blocked modal
- Title was getting truncated ("Cannot configure <Label> as
  <Capability>" doesn't fit a narrow modal). Title is now just the
  integration label; the "Cannot configure as <Capability>." line
  moves into the body as an h5 sub-heading above the existing
  explanatory paragraph. View context drops the pre-composed title
  key and passes my_label so the template composes the sub-heading.

* Tighten data_source scoping at mode-switch invariant seams

The mode-switch invariant (a given integration's entities are all
EXTERNAL for Connect, or all INTERNAL for Import, never mixed) was
enforced at the Configure entry point but several downstream code
paths filtered only on integration_id, leaving data-source-blind
seams. The same code review also surfaced that the disconnect/
reconnect path mishandles data_source when transitioning between
attached/detached states.

POST block-check (CapabilityConfigureView.post)
- GET ran the block check; POST didn't. A direct POST (cached form,
  replayed request) would bypass the invariant. POST now re-invokes
  render_capability_block_if_conflict before delegating to the
  capability-specific handle_post_success.

disable_integration seed (IntegrationManager.disable_integration)
- Seed query now filters by data_source_str=EXTERNAL. A Connect-side
  Disable can no longer silently delete imported INTERNAL data if the
  invariant ever leaks. Native and detached entities (integration_id
  NULL) were already outside the seed.

HomeBoxConnector sync queries
- _get_current_integration_keys (sync-check probe) and
  _get_existing_hb_entities (sync run) both filter by EXTERNAL.
  INTERNAL imports under the same integration_id no longer risk
  being adopted as Connect rows and routed through mass-remove or
  auto-reconnect.

HomeBoxImporter preview/run symmetry
- _run_import_locked's existing_integration_names set now filters by
  INTERNAL to match ImporterConfigureView.handle_post_success's
  preview predicate. A stale EXTERNAL row can no longer turn an
  advertised new candidate into a skip at run time.

Disconnect/reconnect data_source transition
- EntityIntegrationOperations.preserve_with_user_data now flips
  entity.data_source to INTERNAL on detach. A detached entity is no
  longer constrained by an upstream system — HI owns the editable
  representation. The reconnect path is already correct: all four
  service converters set data_source=EXTERNAL during the rebuild
  invoked by _rebuild_integration_components. Pre-migration detached
  rows are also correct: migration 0021's column default of
  'internal' applied to rows with no integration_id.

Test fixtures
- Four entity-creation sites in test_integration_manager.py and
  test_homebox_connector.py now explicitly set
  data_source_str=EXTERNAL for integration-attached test entities,
  since the new EXTERNAL filters no longer match the implicit
  default.

* Move IMPORT to previous_integration_id; introduce named entity-state queries

The IMPORT path was overloading integration_id (the live-ownership
column) to mean "where this entity was sourced from." That conflation
forced data_source_str as a discriminator and made post-import code
paths brittle: any query that scoped by integration_id and a data
source filter could quietly diverge.

Under the model in this commit, the entity's lifecycle state is
encoded entirely by which integration_* column is populated:

  - integration_id set                -> EXTERNAL (live Connect)
  - integration_id NULL, previous set -> imported OR detached
                                         (HI-owned, has provenance)
  - both NULL                         -> INTERNAL (native)

IMPORT now writes to previous_integration_*. integration_id stays
NULL for imported rows because no live integration owns them.

Manager methods (EntityModelManager) give call sites a vocabulary
matching the data-source semantics:

  - external_for(integration_id)         active-Connect rows
  - imported_for(integration_id)         import-side intent
  - detached_for(integration_id)         reconnect-candidate intent
  - with_integration_provenance(id)      any row with this integration's
                                          identity in either column

imported_for / detached_for share an implementation today (the
column shape is the same); each name documents intent at the call
site so a future discriminator could differentiate without touching
callers.

Block check: only IMPORT initiation can collide (it would create
new rows; an active-Connect row already exists for the upstream
key). CONNECT initiation never needs blocking — sync's reconnect-
then-create order adopts any pre-existing provenance entities into
the live Connect session. CapabilityBlockViewMixin loses its
symmetric dispatch and the _CAPABILITY_* dicts.

Post-import placement bug: EntityPlacementService.query_unplaced
filtered by integration_id, so imports (integration_id=NULL) were
invisible to the placement modal — the user saw "No items found"
after a successful import. Switched to with_integration_provenance
so both EXTERNAL and imported rows are eligible.

Other writes / queries swept to the new managers:
  - HbEntityFactory.create_models_for_hb_item IMPORT branch
  - HomeBoxConnector._get_existing_hb_entities / _get_current_integration_keys
  - IntegrationManager.disable_integration seed
  - EntityIntegrationOperations.find_reconnect_candidates / get_removal_entity_ids
  - DataImportPageView.has_imported per-row check
  - ImporterConfigureView preview's skip-detection
  - HomeBoxImporter._run_import_locked / discard_imported_data
  - ImporterDiscardView count

No migration: IMPORT has never shipped in production; the
data_source_str column stays for now and will be retired in the
follow-up commit that introduces the derived data_source property.

One test deleted (test_initial_connect_blocked_by_existing_import_data)
because CONNECT-init no longer blocks under the unified model;
the canonical IMPORT-init block direction is still covered by
test_get_import_blocked_by_existing_connect_data.

* Derive data_source from columns; drop EntityDataSource and data_source_str

The data_source_str column was a stored discriminator that became
fully redundant once IMPORT moved to previous_integration_id: an
entity's state is now encoded entirely by which integration_*
columns are populated. Removing the column eliminates a class of
"stored flag drifted from column reality" bugs and gives the
codebase one fewer thing to keep consistent.

Model:
- Drop the data_source_str field from Entity and the
  EntityDataSource enum.
- Delete migration 0021_entity_data_source (the migration that
  introduced the column). Import has never shipped, so there is no
  production data dependency. Dev environments drop the column
  manually.
- Add three derived properties on Entity that name the questions
  call sites actually ask:
    * is_external — actively attached to an integration
    * has_integration_provenance — carries record of an integration
      it came from (imported or detached)
    * is_imported / is_detached — synonyms today (same column shape);
      separate names document intent at the call site, parallel to
      the manager methods external_for / imported_for / detached_for.

Sweep:
- Remove every entity.data_source = ... write site. The four
  service converters and HbEntityFactory no longer need to set it —
  the columns ARE the state. preserve_with_user_data likewise.
- Replace the one production read site in hb_external_view_resolver
  with `if not entity.is_external`.
- Test fixtures: drop data_source_str=... kwargs from
  Entity.objects.create calls (the column is gone). Two legacy
  "integration_id + data_source_str=INTERNAL" mixes converted to
  provenance form (previous_integration_id + previous_integration_name).
- Replace data_source assertions with property checks.

UI rename: "Detached from X" -> "From X". The unified model treats
imported and detached as the same state, so the badge text needs to
read accurately for both origin stories. Two templates plus comment
references.

Sync-result reporting fixes (surfaced during manual testing of the
above):
- ZM _create_zm_entity now appends the service entity to created_list
  (was hidden in info_list). The user-facing "created" count and
  detail now match what got persisted. Placement input remains
  unaffected — _sync_impl builds it from monitor entities only.
- ZM _create_monitor_entity and Frigate _create_camera_entity gate
  created_list.append on is_fresh_create. Auto-reconnect rebuilds
  no longer double-list the row in both created_list and
  reconnected_list (HomeBox/HASS did not have this bug; their
  _rebuild_integration_components calls the converter directly,
  bypassing the per-connector _create_entity).
- Both result-modal placement CTAs now read "Place new items"
  without a count. The previous count came from created_list length,
  which includes reconnected entities and (for ZM) the service
  entity — but the placement modal correctly excludes those. Action
  prompt > misleading count.
2026-05-25 18:56:50 -05:00
Tony C
35c6f8532c Integration Capabilities framework (#355) (#359)
* Rename Connect vocabulary across the integrations framework

Three coordinated rename groups (no behavior change):

- test_connection -> validate_access on IntegrationGateway and all
  service overrides + call sites. The generalized verb covers both
  network probes (today) and access checks for future Import-capability
  integrations.
- is_initial_import -> is_initial_connect across the synchronizer
  protocol, view layer, templates, and tests. Internal sync /
  synchronize vocabulary is preserved.
- UI labels updated: IMPORT -> CONNECT (in live-discovery contexts),
  REFRESH -> UPDATE (button) / Update (inline link), Initial Import ->
  Initial Connect, Refresh Result -> Update Check Result. The
  no-items hero copy reads 'No items found.' with non-redundant
  sub-text.

Prerequisite for the broader integration-capabilities work in #355.

* Restructure HomeBox importer/ — Connect code to connector/, dormant preserved

The reachable Connect-mode synchronizer code moves from
services/homebox/importer/ to connector/:

- HomeBoxSynchronizer relocates to connector/hb_sync.py.
- New HbEntityFactory in connector/hb_entity_factory.py carries the
  two entity-create/update classmethods previously on HbImporter.

The dormant attribute-population code stays in importer/ for #358:

- HbImporter retains only its four attribute-side classmethods with a
  narrowed docstring noting their dormant status.
- The synchronizer's removed attribute-orchestration methods extract as
  HbAttributeSync in importer/hb_attribute_sync.py with a docstring
  marking them preserved for the HomeBox Importer in #358.

No behavior change. (#355)

* Phase 3 (#355): split integrations app into umbrella + connect sub-package

Relocate the Connect-capability machinery (gateways, synchronizers, sync-check,
views, view mixins, entity operations, attribute edit context, sync-result, and
related transient models) from hi/integrations/ into hi/integrations/connect/.
Umbrella-level modules (models, forms, enums, manager, urls, exceptions, etc.)
stay at hi/integrations/ as the framework root.

This re-shapes the package along capability lines ahead of the Import
capability being added by #358: the umbrella holds capability-agnostic
framework code; each capability sub-package holds the per-capability machinery.

No behavior changes. All call sites updated to the new module paths; all 3141
tests pass and lint is clean.

* Tolerate unreadable sync-check cache entries

The sync-check probe state is stored in Redis as a pickled
SyncCheckResult dataclass. Any class rename, module move, or shape
change can leave stale entries that fail to deserialize on read,
which previously propagated ModuleNotFoundError / AttributeError /
UnpicklingError straight out through the manage page.

Wrap the cache.get in get_state with a broad except: log the failure,
evict the bad key so the next request runs clean, and degrade to a
cache miss. The next probe cycle (or a Refresh) writes fresh state.

The cache is informational drift state, not load-bearing — a soft
miss is the right failure mode.

* Phase 4 (#355): add EntityDataSource enum + Entity.data_source field

EntityDataSource is the entity-wide provenance signal (INTERNAL when HI
owns the representation; EXTERNAL when an upstream system does).
Surfaces via Entity.data_source — a typed accessor over the new
data_source_str CharField, following the existing entity_type pattern.

Migration 0021 backfills integration-attached entities that disallow
internal attributes to EXTERNAL. Today this picks out only HomeBox-
Connect entities; HA / ZM / Frigate / native entities stay INTERNAL.

No UI impact yet. Phase 6 will consume data_source for cross-capability
transition prompts and Import-discard scoping.

* Phase 5 (#355): add IntegrationCapability enum + metadata declaration

IntegrationCapability declares what an integration brings to HI:
CONNECT (live mirror) or IMPORT (one-shot pull). IntegrationMetaData
now carries a `capabilities` frozenset declaring which capability set
the integration participates in; defaults to {CONNECT} since that is
the existing behavior for all four current integrations. An empty
set raises at construction time so a future integration cannot
silently register with no capability.

ALL_CAPABILITIES is provided as a frozenset convenience over all
enum members for callers that want "available everywhere" semantics.

No UI impact yet. Per-attribute capability filtering deferred until
Phase 6 introduces the consumer.

* Phase 6 (#355): make capability filtering explicit at integration list and attribute formset sites

Extend IntegrationAttributeType to accept an optional 8th tuple element
declaring which IntegrationCapabilities the attribute applies to
(defaults to ALL_CAPABILITIES, so existing declarations are unchanged).

Convert the previously-implicit "everything is Connect" assumption into
explicit capability filters at four sites that all run in Connect-mode
context today:

  * IntegrationSelectView (Enable Integrations picker modal)
  * IntegrationManageView (Config tab — enabled-integrations list and
    default-integration selector)
  * IntegrationAttributeItemEditContext (formset construction, used by
    both the Configure modal and the Config tab attribute pane)

IntegrationManager.get_integration_data_list() and
get_default_integration_data() gain an optional `capabilities`
frozenset kwarg. IntegrationAttributeItemEditContext gains a required
`capability` __init__ arg; its formset queryset is now filtered to
attribute rows whose AttributeType includes that capability. The same
class can be reused for the Import edit context in #358 by passing
IntegrationCapability.IMPORT.

Behavior unchanged today (all integrations are Connect, all attributes
default to ALL_CAPABILITIES) but the filter rules are now testable
with mixed-capability fixtures.

The Importer protocol, workflow/views/templates, and cross-capability
transition prompts originally listed in Phase 6 absorb into #358 (the
HomeBox Importer) — they are easier to design with a concrete consumer
driving them.

* Phase 7 (#355): collapse Configure + Pre-Sync + Sync into one CONNECT click

The initial-connect flow becomes a single workflow under
IntegrationEnableView: the user fills the configure form once and
clicks CONNECT, which validates access, saves attributes, enables
the integration, and runs the synchronizer in one pass — landing
directly on the sync-result modal. The previous Configure → Pre-Sync
confirm → Sync handshake is gone from this path, along with the
REVIEW CONFIG round-trip affordance.

Extracted IntegrationViewMixin.render_sync_result so the
synchronizer-invocation + cache-invalidation + sync-result rendering
is shared between IntegrationEnableView (initial-connect) and
IntegrationSyncView (update-check). Action button label flips from
the conditional CONFIGURE/UPDATE to a fixed CONNECT.

IntegrationPreSyncView and its template still serve the manage-page
UPDATE / CONNECT-when-no-entities paths unchanged; the
removal_summary policy choice (RETAIN MISSING / REMOVE MISSING) for
user-data-bearing integrations is preserved end-to-end.

Net behavior change: one fewer modal click on first-time setup.

* Phase 8 (#355): align documentation with the new Connect/Update vocabulary

Update the user-facing button-label references in the per-integration
docs (Refresh / REFRESH → Update / UPDATE; Import action → Connect)
and the developer-facing references where they were tied to user
surfaces. Internal developer vocabulary stays as "sync" — that is
the implementation-side term.

Add a Capability Model subsection to integration-guidelines.md
covering IntegrationCapability, the optional per-attribute capability
restriction, and EntityDataSource. Points at the enum modules; keeps
the explanation brief since the code is the authoritative source.

Update the per-integration doc template's vocabulary instruction so
new integrations start with the right terms.

* Address review feedback: sharpen vocabulary and add IntegrationEnableView POST tests

Four fixes from the pre-PR review pass:

- Sharpen EntityDataSource enum descriptions to disambiguate from raw
  value provenance: "HI owns the editable representation" /
  "Upstream constrains HI-side edits" (the actual semantics codified
  by migration 0021's allow_internal_attributes-based backfill).
- Replace the self-contradicting "first **Import**" / "post-import
  state" in docs/integrations/_template.md with the **Connect**
  vocabulary the same template instructs authors to use.
- Reword frigate.md troubleshooting heading "Update imports zero
  cameras" → "Update finds zero cameras" to stop mixing the UPDATE
  button label with the "imports" verb.
- Add two IntegrationEnableView.post() tests covering the Phase 7
  collapse: the happy path verifies enable + synchronizer.sync()
  fire and the sync-result modal renders; the synchronizer-less
  path verifies enable still flips but no sync-result is returned.
2026-05-24 16:58:18 -05:00
Anthony Cassandra
3c418653ca Entity state panels doc: clarify the purpose of the extras mechanism
The existing "Display contract" framing risked an unintended reading
— that omitting a role from the declared set is a valid way to opt
into the framework's extras rendering for "free". Add a focused note
that names the actual purpose: extras is a safety net for unusual
EntityType assignments (EntityType is user-adjustable, so an entity
can carry roles its type wouldn't normally expect). Declarations
should cover the roles the EntityType reasonably carries; the
fallback/state_row.html include is the escape hatch for panels that
want the standard rendering for a declared role.
2026-05-21 09:15:19 -05:00
Tony C
0eee15864c Frigate NVR integration (v1) (#347)
* Scaffold Frigate integration (issue #233)

Lays down the directory + plumbing for hi.services.frigate and
hi.simulator.services.frigate so both apps load under their respective
Django settings, the FrigateGateway is auto-discovered by
IntegrationManager, and the Frigate simulator module appears in the
simulator's services-tab dispatch. All concrete methods stubbed
(test_connection returns 'not yet implemented (scaffolding)',
do_work records a healthy heartbeat without polling, etc.).

Includes user-facing and developer-facing doc stubs structured around
the v1 scope, and adds Frigate to the integrations landing page.

* B1: Frigate simulator Camera entity definition

Adds FrigateCameraSimEntityFields plus two SimState subclasses —
FrigateCameraMotionState (MOVEMENT) and FrigateCameraObjectPresenceState
(DISCRETE). Registers a single Camera SimEntityDefinition in
FRIGATE_SIM_ENTITY_DEFINITION_LIST so the simulator's services-tab UI
exposes 'ADD Frigate ENTITY' -> Camera.

The object-presence choices carry raw Frigate-style labels (person /
car / truck / dog / cat / package) plus a deliberately unmapped
'unicorn' label so the HI converter's 'other' bucket stays on the
demo path. Canonical OBJECT_PRESENCE bucketing happens later on the
HI side, not at the simulator boundary.

* B2: Frigate simulator /api/config endpoint

Adds FrigateSimCamera accessor wrapper (mirrors ZmSimMonitor),
FrigateSimulator.get_sim_cameras(), and a Frigate-shape ConfigView at
GET /services/frigate/api/config that returns the active profile's
cameras keyed by camera_name. Real Frigate emits a much larger config
document; HI's integration only needs the cameras map plus a stable
JSON shape to recognize as Frigate.

* Fix Frigate attribute lookup to use integration_key, not integration_attr_type

The IntegrationAttribute model exposes integration_key (an
IntegrationKey wrapping integration_id + integration_name), not a
direct integration_attr_type field. The lookup pattern from
HassManager._build_hass_attr_type_to_attribute_map is the canonical
shape: build {attr.integration_key: attr} once, look up by
constructing IntegrationKey(integration_id, str(attr_type)).

* C1: FrigateClient.ping() + get_cameras() real implementations

Both methods route through a private _get_config() helper that hits
/api/config, validates status code + content-type, and parses JSON
with boundary-level diagnostic errors (same shape as HassClient).
ping() returns None on success; get_cameras() returns one dict per
entry in the response's 'cameras' map carrying the camera name and
raw per-camera config.

14 unit tests covering init / auth header / success / non-2xx /
HTML body / malformed JSON / network error / empty cameras /
multi-camera / config passthrough / missing or malformed
'cameras' field.

* C2: Wire FrigateManager.test_connection to FrigateClient.ping

Builds a temporary FrigateClient via FrigateClientFactory from the
proposed attributes and exercises ping() against /api/config.
ValueErrors from the client (configuration or response-shape
failures) surface as failure messages verbatim; other exceptions
get prefixed 'Connection error: '. Bounded by the provided
timeout_secs so the Configure form fails interactively.

* C3: Frigate sync creates camera entities with Movement + ObjectPresence sensors

Adds the new EntityStateType.OBJECT_PRESENCE (canonical value
range: NONE / PERSON / CAR / ANIMAL / PACKAGE / OTHER) plus the
matching EntityStateValue members, type-default EntityStateRole,
role-ordering entry (placed after MOVEMENT/PRESENCE), and a
HiModelHelper.create_object_presence_sensor factory.

FrigateManager grows a real _reload_implementation that builds the
FrigateClient from current integration attributes, a frigate_client
property gated on integration-enabled state, plus _to_integration_key
and _frigate_integration_key helpers for stable key construction.

FrigateSynchronizer._sync_impl mirrors the ZM pattern: fetch cameras
from /api/config via the client, reconcile against existing HI
camera entities by integration_key, create CAMERA entities for new
upstream cameras (with Movement + ObjectPresence sensors), preserve
the user-editable name on update, and remove entities whose upstream
camera disappears. check_needs_sync wires the Issue #283 drift probe.

6 sync tests cover client-missing, create, idempotent re-sync,
remove on upstream drop, user-name preservation, and multi-camera
placement input.

* Use camera friendly_name for HI entity display name

Real Frigate cameras carry a friendly_name on each camera's config
for display; the camera key itself is the snake_case technical
identifier. Sync now prefers config.friendly_name when present and
falls back to the camera key otherwise. Simulator's /api/config
endpoint emits friendly_name from the camera sim_entity's
display name field (the operator-set 'name' on the
FrigateCameraSimEntityFields), keeping the simulator faithful to
Frigate's response shape.

* D1: Frigate simulator events synthesized from motion-state toggles

Mirrors the ZM event-manager pattern: per-camera FrigateSimEventHistory
ring buffers held by a FrigateSimEventManager singleton, keyed by
camera_name. Events are pure transient FrigateSimEvent dataclasses
(no DB, no CRUD UI) — the operator drives them by toggling the
camera's existing Motion sim-state.

FrigateSimulator.set_sim_state intercepts Motion-state changes and
calls into the event manager. The event's label comes from the
camera's current ObjectPresence value at the moment motion-ON
fires (label fixed at first detection, matching real Frigate's
behavior closely enough). ObjectPresence changes during an open
event don't relabel it; operator toggles motion off-then-on to
start a new event with a new label.

Following the project convention of no simulator-internal unit
tests — verification is via manual hands-on testing through the
running simulator. A smoke run confirmed motion-ON opens a labeled
event and motion-OFF closes it.

* D2: Frigate simulator /api/events list + detail endpoints

GET /api/events returns the active profile's events as a top-level
JSON array (Frigate's convention), sorted newest-start_time-first.
Supports the 'after' query parameter (epoch seconds) used by the HI
polling cursor; other Frigate filters (before / cameras / labels /
zones) are accepted and silently ignored — they're not yet on the
HI integration's read path.

GET /api/events/<id> returns a single event object; 404 for unknown
ids so the HI client can distinguish 'event doesn't exist' from a
broken Frigate.

Verified end-to-end via the test client: motion-state toggles
synthesize events that surface through both endpoints with correct
ordering, filtering, and 404 behavior.

* E1: FrigateClient.get_events() + get_event() real implementations

Both methods route through a shared _get_json(path, params=None)
helper that handles status-code, content-type, and JSON-parse
validation in one place (the prior _get_config now delegates to it).
get_events accepts the polling cursor 'after' and an optional limit
as query params; get_event maps the URL onto /api/events/<id>.

Non-2xx responses (including 404 from get_event for unknown ids)
surface as ValueError with the status code in the message so
monitor / test_connection paths can record meaningful diagnostics.

11 new unit tests covering get_events (empty / parsed list / 'after'
+ 'limit' query params / no-params / not-a-list response / non-2xx)
and get_event (parsed dict / URL targeting / 404 / not-a-dict
response). End-to-end smoke against the live simulator verifies
both methods return data toggled in via the Motion sim-state.

* E2: FrigateMonitor polls events and emits MOVEMENT sensor responses

Mirrors the ZoneMinder monitor's order of operations exactly (those
invariants took several debugging rounds to settle on the ZM side):

  1. Fetch events with after=<polling-cursor>.
  2. Two-phase collate: filter out fully-processed events, then
     partition the rest into open vs closed.
  3. Per-camera aggregation: pick ONE canonical event per camera
     (earliest open -> ACTIVE; latest closed -> IDLE). Prevents
     multiple events on one camera from overwriting each other in
     the sensor-response map.
  4. Emit one MOVEMENT SensorResponse per aggregated state with
     correlation_role (START/END) + correlation_id set.
  5. Emit explicit IDLE SensorResponses for cameras that produced
     no events this cycle so their state doesn't go stale.
  6. Cursor advance: hold back to the earliest open event's start
     when any open events remain; otherwise advance to the latest
     closed event's end; with NO events do NOT advance (would risk
     missing an event that started right after the poll).

_fully_processed_event_ids TTLCache prevents closed events from
being re-emitted on subsequent polls. Open events are deliberately
NOT marked fully-processed — the cursor keeps them visible so we
can detect the close transition.

FrigateEvent.from_api_dict parses one /api/events JSON record into
the typed wrapper. AggregatedCameraState (in frigate_models.py for
parity with ZM's AggregatedMonitorState) carries the canonical
event + all_events for cache bookkeeping. FrigateManager grows
sync + async get_events / get_cameras shims used by the monitor.

21 unit tests cover aggregation across the open x closed
cross-product, sensor-response correlation, the no-event IDLE
case, cache bookkeeping, FrigateEvent.from_api_dict, and the
four cursor-advance invariants (no events / all closed / has open
/ fully-processed re-skip).

* E3: FrigateMonitor emits OBJECT_PRESENCE alongside MOVEMENT

FrigateConverter.to_canonical_object_class gets the real lookup
table: person -> OBJECT_PERSON; car/truck/bus/motorcycle/bicycle ->
OBJECT_CAR; dog/cat/bird/horse/sheep/cow/bear/deer/raccoon/fox/
squirrel/rabbit -> OBJECT_ANIMAL; package -> OBJECT_PACKAGE; anything
else -> OBJECT_OTHER. Lookup is case-insensitive. Empty / unknown
labels deliberately bucket into OBJECT_OTHER (not OBJECT_NONE) so a
custom-model class that hasn't been bucketed yet still surfaces in
HI as 'something is here'.

FrigateMonitor's per-camera sensor-response generation now emits
TWO responses per aggregated state: the MOVEMENT response that
already existed (ACTIVE/IDLE with correlation_role/id) plus a new
OBJECT_PRESENCE response carrying the canonical class. IDLE camera
states emit OBJECT_NONE — once an event closes, nothing is being
detected. The idle-for-unseen-camera pass also pairs MOVEMENT IDLE
with OBJECT_NONE so the object-presence state stays fresh even on
quiet poll cycles.

7 converter tests + expanded monitor tests cover the table
mapping, case-insensitive lookup, the unknown -> OTHER fallback,
and per-cycle OBJECT_PRESENCE emission across ACTIVE / IDLE /
no-events paths.

* Frigate: collapse MOVEMENT + OBJECT_PRESENCE into a single per-camera state

Frigate's data model couples motion to object detection — its events
API never emits motion-without-class — so a separate MOVEMENT sensor
on the HI side would always mirror OBJECT_PRESENCE. Removing it also
eliminates the simulator's ordering trap where the operator had to
toggle Motion before picking a label or the synthesized event landed
with no object class.

OBJECT_PRESENCE is now the single per-camera state across the
integration. Any non-OBJECT_NONE value implies motion is currently
happening and names the class; OBJECT_NONE means no current detection.

Simulator: drop the Motion sim-state entirely; the ObjectPresence
discrete control drives the event lifecycle directly. Transitioning
to a new label closes the current event and opens a new one;
transitioning to 'none' closes the current event.

* Frigate: snapshot URLs (live + event) and gateway VideoSnapshot plumbing

Real Frigate exposes still images at two paths the HI integration now
consumes:

- /api/<camera>/latest.jpg — the most recent decoded frame, used by
  the presentation layer as a live snapshot via the gateway's
  get_entity_video_snapshot method.
- /api/events/<id>/snapshot.jpg — the frame captured at the time of
  detection, attached to the OBJECT_PRESENCE SensorResponse as
  source_image_url so the alert / history views can show what the
  camera saw.

URL helpers on FrigateManager build both with a cache-bust timestamp
so a re-rendered <img> tag with the same src refetches rather than
showing the prior frame. Both helpers return None when the client
isn't built yet, propagating "no snapshot capability" through the
gateway.

Simulator endpoints serve synthesized placeholder JPEGs stamped with
the camera or event identifiers so artifacts viewed inside HI are
obviously coming from the simulator. /api/events/<id>/snapshot.jpg
is routed ahead of the existing /api/events/<id> JSON route so the
trailing path segment dispatches correctly.

MP4 clip endpoint deferred — v1 displays use snapshot-as-stream
(via video_snapshot_stream_fps) rather than recorded-event playback.

* Frigate: switch camera entities to snapshot-as-stream and clear manager health UNKNOWN

Two bugs surfacing on the Live Feed page:

(1) Frigate cameras were flagged with has_video_stream=True even
though v1 has no native MJPEG/RTSP — without go2rtc, Frigate exposes
only JPEG endpoints. The Live View template routed to the native-
stream branch which fell through to "Live Stream Unavailable"
because no gateway implementation existed. Flip the canonical sync
shape to has_video_snapshot=True + video_snapshot_stream_fps=1.0
(matching the HASS camera pattern) so the pane routes through the
snapshot-as-stream branch and consumes the snapshot URL plumbing
that already exists. _update_entity self-heals existing rows by
flipping the stale flag off and backfilling the missing fps.

(2) The integration banner showed "Frigate: Warning" while the
FrigateMonitor was reporting Healthy. The manager registers itself
as an API health provider, but its api_health_status default
(UNKNOWN) leaks into the aggregate — UNKNOWN counts as not-healthy
under ALL_SOURCES_HEALTHY, dragging the aggregate to WARNING. Set
the slot explicitly on every _reload_implementation: HEALTHY on a
clean client build, DISABLED when the integration row is off or
absent, UNAVAILABLE on a build failure.

* Frigate: wrap manager API methods in api_call_context

FrigateManager.get_cameras and get_events were making bare HTTP calls
without feeding the manager's API-health slot. Result: total_calls
stayed at 0, the slot stayed UNKNOWN, and the aggregate health stayed
at WARNING. Wrap both methods in api_call_context so each successful
poll increments the slot and moves it to HEALTHY — matching how
ZoneMinderManager.get_zm_states/monitors/events handle the same
concern.

The reload-time update_api_health_status calls are retained so the
slot also reflects a clean configuration even before the first poll
lands.

Comment cleanup pass on Phase F additions.

* Frigate: seed simulator profiles (empty, baseline, baseline-changed, volume)

Adds the standard four-profile catalog for the Frigate sub-app so the
sync result modal can be exercised through created / removed /
detached / reconnected transitions, plus a 10-camera stress profile.

* OBJECT_PRESENCE styling: collapse onto Movement vocabulary, give priority over MOVEMENT

OBJECT_PRESENCE is conceptually MOVEMENT with a larger value space.
For visual purposes the two collapse onto the same status_value
vocabulary (active / recent / past / idle), reusing existing CSS
rules. Where a camera has both — uncommon but possible —
OBJECT_PRESENCE wins because it carries finer-grained information.

Changes:
- EntityStateDisplayData gains _get_object_presence_status_style:
  same decay logic as MOVEMENT, discriminator is value != OBJECT_NONE
  (any detected class counts as active).
- DEFAULT_ENTITY_STATE_ROLE_ORDER and ConsoleManager.STATUS_ENTITY_STATE_PRIORITY
  put OBJECT_PRESENCE ahead of MOVEMENT so it wins priority pickers.
- EntityPairingManager.CREATE_BY_DEFAULT_MAP gets OBJECT_PRESENCE so
  a Frigate camera auto-creates an AREA delegate on first placement,
  matching MOVEMENT behavior.
- Camera panel declares OBJECT_PRESENCE in optional_roles with an
  object_data alias. Templates prefer object_data over motion_data
  for the chip, and render OBJECT_PRESENCE as a standard state row
  (via the fallback row template) so the user can see the current
  class and click through to history.

Tests cover all four OBJECT_PRESENCE decay states (active / recent
/ past / idle) plus the "other class is still active" branch.

* Frigate: auto-create OBJECT_PRESENCE EventDefinition on sync (gated by Add Alarm Events)

Brings Frigate to parity with ZoneMinder and Home Assistant on the
default alarm-wiring concern. Sync now creates an OBJECT_PRESENCE
EventDefinition per camera when the operator opts in via the new
ADD_ALARM_EVENTS integration attribute.

The default rule is the conservative EQ OBJECT_PERSON — person-only
alarms — to sidestep the EventClauseOperator vocabulary's lack of
NEQ/IN (tracked separately in #346). Operators wanting broader
detection rules can author additional EventDefinitions in the UI.

Changes:
- FrigateAttributeType.ADD_ALARM_EVENTS: BOOLEAN attribute,
  default off.
- FrigateManager._attribute_map: populated during reload from the
  current integration attributes; backs the new
  should_add_alarm_events property.
- HiModelHelper.create_object_presence_event_definition: new
  helper, mirrors create_movement_event_definition with the
  conservative OBJECT_PERSON default value.
- FrigateManager.OBJECT_PRESENCE_EVENT_PREFIX: integration-key
  prefix for the auto-created event definitions.
- frigate_sync.py: creates the EventDefinition when
  should_add_alarm_events is true.

Tests cover should_add_alarm_events (default off / true / false
attribute values) and the sync's create / skip / idempotent-on-
refresh paths.

* Frigate: implement per-camera Detect on/off control end-to-end

Outbound (HI -> Frigate):
- FrigateClient.set_camera_detect issues POST /api/<camera>/detect/set
  with the Frigate-side state value ('ON' / 'OFF'). New _post helper.
- FrigateConverter.hi_control_to_detect_state — explicit HI on/off ->
  Frigate ON/OFF mapping. Independent vocabularies; no string
  transforms.
- FrigateManager.set_camera_detect wraps the client call in
  api_call_context.
- FrigateController.do_control dispatches the detect-controller
  integration_name shape; surfaces translation and manager errors as
  IntegrationControlResult.error_list entries.
- frigate_sync creates an on/off controller per camera paired with
  the OBJECT_PRESENCE sensor.

Inbound (Frigate -> HI):
- FrigateConverter.detect_enabled_to_hi_value reads /api/config's
  cameras.<name>.detect.enabled boolean and maps to HI's on/off.
- FrigateMonitor emits a detect SensorResponse per camera each
  poll cycle so the panel display reflects the actual upstream
  state (closes the prior gap where the toggle would snap back
  for lack of confirming sensor data).

Simulator:
- New FrigateCameraDetectState sim-state per camera, defaulting to
  'ON' (real Frigate's startup default). Independent of the
  object-presence event lifecycle so each signal exercises HI's
  wiring on its own.
- /api/config exposes cameras.<name>.detect.enabled from the
  sim-state value.
- POST /api/<camera>/detect/set writes the value into the
  sim-state via the simulator so the operator observes the
  round-trip in the simulator UI.

* Rename sensor / event video-clip / video-snapshot fields to disambiguate from entity-level live-feed vocabulary

The Sensor/SensorResponse/SensorHistory/EntityStateHistoryValue
fields named has_video_stream / provides_video_stream / source_image_url
predated the entity-level live-feed model (has_video_stream /
has_video_snapshot / video_snapshot_stream_fps), and the name overlap
made it hard to tell at a call site whether you were dealing with
live observation (entity level) or historical event playback
(sensor/event level).

Rename to make the level explicit and reserve "event_" prefix for
historical playback concepts:

| Old                                          | New                              |
|----------------------------------------------|----------------------------------|
| Sensor.provides_video_stream                 | Sensor.provides_event_video_clip |
| (new)                                        | Sensor.provides_event_video_snapshot |
| SensorHistory.has_video_stream               | SensorHistory.has_event_video_clip |
| SensorHistory.source_image_url               | SensorHistory.event_video_snapshot_url |
| SensorResponse.has_video_stream              | SensorResponse.has_event_video_clip |
| SensorResponse.source_image_url              | SensorResponse.event_video_snapshot_url |
| EntityStateHistoryValue.has_video_stream     | EntityStateHistoryValue.has_event_video_clip |
| EntityStateHistoryValue.provides_video_stream | EntityStateHistoryValue.provides_event_video_clip |

Entity-level has_video_stream is unchanged — it remains the
live-feed capability flag, distinct from the per-event flags above.

DB migration uses RenameField only (no column drops); the lone new
column is the Sensor.provides_event_video_snapshot capability flag
to mirror provides_event_video_clip on the snapshot side.

* Frigate: event video clip playback (and HI render-layer MP4 support)

HI's Video Browse rendered event recordings via <img> alone — fine for
ZM (multipart MJPEG) but breaks Frigate's MP4 clip semantics. Add an
explicit media-type axis on VideoStream and a parallel <video> render
branch so MP4 event recordings play natively.

VideoStreamType vocabulary:
  URL  ->  MJPEG  (multipart, <img> renders)
  (new)    MP4    (recording, <video> renders)
  OTHER    OTHER

Render layer:
- entity_video_sensor_history.html branches on stream_type: <video>
  for MP4 (with controls + autoplay + muted + playsinline), <img>
  for MJPEG (existing path, unchanged).
- video-timeline.js replay button: <video> branch seeks to 0 and
  plays; <img> branch keeps the cache-bust URL trick (ZM).
- ZoneMinder gateway updated from VideoStreamType.URL to MJPEG to
  match the new explicit naming.

Frigate side:
- FrigateApi.EVENT_CLIP_PATH_TEMPLATE + FrigateManager.get_event_clip_url.
- FrigateEvent.has_clip / has_snapshot parsed from /api/events
  payload; default True for backward compat with older Frigate.
- FrigateMonitor propagates has_clip from the canonical event into
  the OBJECT_PRESENCE SensorResponse's has_event_video_clip.
- FrigateGateway overrides get_sensor_response_video_stream to return
  a VideoStream(stream_type=MP4, source_url=<clip url>).
- FrigateSync flags the OBJECT_PRESENCE sensor with both
  provides_event_video_clip and provides_event_video_snapshot.

Simulator side:
- New POST endpoint /api/events/<id>/clip.mp4 serves a pre-generated
  H.264 baseline placeholder (~14 KB) with labeled frame counter and
  ticking clock so the operator can verify the playback round-trip.

Tests cover the wire-to-model has_clip parsing, the monitor's
propagation, the gateway's MP4 stream return shape, the sync's
sensor flags, and the controller's URL routing.

* Replace SensorResponse event_video_snapshot_url with has_event_video_snapshot flag

Bring event snapshot URLs in line with the clip URL pattern: gateways
build the URL on demand from the event id rather than storing it.
Robust to operator-side base URL changes — an integration host
relocation no longer leaves historical SensorHistory rows with stale
URLs pointing at the previous host.

Symmetry now between the two affordances:

  Clip      has_event_video_clip       (flag)
            gateway.get_sensor_response_video_stream  (URL builder)

  Snapshot  has_event_video_snapshot   (flag, new)
            gateway.get_sensor_response_event_snapshot_url  (URL builder, new)

Migration:
  0013_replace_event_video_snapshot_url_with_flag adds the boolean,
  backfills True for every row that had a non-empty URL, then drops
  the URL column.

Frigate side:
  - has_event_video_snapshot propagates from FrigateEvent.has_snapshot
    through the monitor to the SensorResponse.
  - FrigateGateway.get_sensor_response_event_snapshot_url builds the
    URL from the response's correlation_id.

ZoneMinder side:
  - has_event_video_snapshot set True on START + END responses when
    detail_attrs carries an event_id.
  - ZoneMinderManager.get_event_snapshot_url returns the portal
    view=image URL fresh; gateway override builds per render.

Render side:
  - Templates that displayed event_video_snapshot_url now use the new
    sensor_response_event_snapshot_url template tag.
  - alert.py's get_first_visual_content dict no longer carries the
    URL — templates resolve via the tag.

* Drop misleading "video may be processing" caption from snapshot fallback

The caption fired unconditionally on the snapshot-fallback branch,
but the code has no actual signal distinguishing "clip is being
processed" from "no clip exists for this event". For an event that
will never have a clip (operator opted out, integration didn't
capture one), the caption was misinformation. The surrounding
heading + timestamp already communicate what the image is.

* Camera modal: render MOVEMENT state row alongside chip

The camera modal already rendered an OBJECT_PRESENCE state row
below the stream so Frigate users could click through to event
history. ZM-style MOVEMENT was driving the chip but had no row
treatment, leaving the operator with no path to the history /
video-browse view. Render both roles' rows when present.

* Rename event-playback "recording" vocabulary to "clip" for consistency with model fields

The model layer settled on "clip" (Sensor.provides_event_video_clip,
SensorResponse.has_event_video_clip, etc.) but the CSS / JS / template
layer was still named around "recording" from an earlier iteration.
Future readers had to do an unnecessary translation step between the
two halves of the same concept.

View-layer renames:
  CSS  .hi-video-recording             -> .hi-video-clip
       .hi-video-recording-replay      -> .hi-video-clip-replay
       .hi-video-recording--mp4        -> .hi-video-clip--mp4
  HTML data-video-recording (attr)     -> data-video-clip
  Tmpl video_recording_replay_button   -> video_clip_replay_button
  JS   videoRecordingReplayBuster()    -> videoClipReplayBuster()
       img.dataset.videoRecordingSrc   -> img.dataset.videoClipSrc

Plus prose / docstring / aria-label / alt-text updates from "video
recording" to "video clip" in the same view-layer files.

Unchanged (different meanings of "recording"):
  HassApi.CAMERA_STATE_RECORDING — Home Assistant wire-value verbatim.
  TestFrigateManagerHealthRecording — recording-of-health concept.
  auto-view.js "interaction recording" — UI tracking concept.
  weather/daily_weather_tracker — weather-domain concept.

* Sensor Response Details modal: add STATUS / HISTORY footer nav

The modal is reached from the status and history modals, so a
back-nav path was missing. Add STATUS / HISTORY / DONE footer
buttons keyed on the underlying entity, mirroring the Entity Edit
modal's footer shape.

* Document correlation_id as sensor-scoped, not globally unique

Make explicit that correlation_id pairs START/END readings within a
single sensor's history but is not guaranteed unique across sensors
or integrations. The integration's upstream event_id namespace is
opaque to us; ZM and Frigate could theoretically produce
overlapping strings. Every lookup MUST be scoped by sensor.

* Integration config: seed and repair attribute order_id from enum definition order

IntegrationAttribute rows had order_id=0 across the board, so the
config page render relied on row-id tiebreak — definition-order
preserved only by coincidence. Frigate's mid-life addition of
ADD_ALARM_EVENTS exposed the brittleness when row creation ordering
diverged from the operator-facing enum order.

LabeledEnum auto-numbers members 1, 2, 3, ... in definition order,
so attribute_type.value is exactly the order_id we want. Set it on
new rows in _create_integration_attribute and self-repair existing
rows in ensure_all_attributes_exist so any installed environment
heals on next startup without a separate migration.

* Frigate: pack event metadata into SensorResponse detail_attrs

Mirror ZoneMinder's pattern of attaching event metadata to the
SensorResponse so HI's event-detail UI surfaces it as label/value
rows. Per FrigateDetailKeys: event id, start time, object class,
score, sub-label, zones; plus duration on END responses (omitted
on START since the event is still open).

The pre-existing SNAPSHOT_URL / CLIP_URL keys in FrigateDetailKeys
were never populated — URLs are generated by the gateway on demand.
Drop them from the constants class so future readers don't expect
them in detail_attrs.

* Frigate: switch detect toggle to PUT /api/config/set (real Frigate's actual endpoint)

The previous implementation called POST /api/<camera>/detect/set,
which does not exist in real Frigate's HTTP API (checked against
v0.17.1 source). Real Frigate exposes detect toggling only through
the admin-only PUT /api/config/set runtime config update; the
HTTP detect-toggle our simulator was honoring was a fabrication.

Wire change:
  Old (fabricated): POST /api/<camera>/detect/set?state=ON
  New (real):       PUT /api/config/set?cameras.<name>.detect.enabled=true

The new endpoint requires admin role on real Frigate — the
operator must supply an admin-scoped Authorization header in the
existing FrigateAttributeType.AUTH_HEADER attribute.

HI vocabulary translation:
  HI 'on'  <-> Frigate 'true'    (was 'ON')
  HI 'off' <-> Frigate 'false'   (was 'OFF')

Simulator endpoint replaced: CameraDetectSetView removed, ConfigSetView
added at PUT /api/config/set. It recognizes the
cameras.<name>.detect.enabled key in the query string and updates the
camera's detect sim-state accordingly. Other config keys are
accepted but no-op (real Frigate would persist them; the simulator
has no config to mutate).

* Frigate sim-state: correct the stale "wire representation" comment

The previous comment claimed the sim-state's "ON"/"OFF" values
WERE Frigate's wire vocabulary. After the detect-toggle switch to
PUT /api/config/set, Frigate's wire vocabulary is "true"/"false".
The sim-state values are now an internal-only representation that
the simulator's ConfigSetView / ConfigView translate to/from
Frigate's wire format. The values stay as-is (no data migration
concern — sim-state is runtime, not persisted).

* Frigate: drop the per-camera Detect on/off control surface from v1

Real Frigate has no transient detect-toggle endpoint reachable over
HTTP — the only mechanism is PUT /api/config/set, which is a
persistent config edit. Mapping a HI control surface onto a config
edit creates surprising operator-facing behavior (the toggle
permanently rewrites Frigate's YAML, with last-writer-wins between
HI and Frigate's own UI). HI controls have generally meant
transient state.

Removing v1's Detect controller end-to-end:
- FrigateClient.set_camera_detect, _put, _post all gone (none used
  elsewhere).
- FrigateConverter.hi_control_to_detect_enabled and
  detect_enabled_to_hi_value mappings removed.
- FrigateManager.set_camera_detect and DETECT_CONTROLLER_PREFIX
  removed.
- FrigateController is back to a stub returning "no control
  mapping" for any incoming key.
- frigate_sync no longer creates the on/off controller per camera.
- FrigateMonitor no longer emits the detect SensorResponse on each
  poll cycle.
- Simulator's FrigateCameraDetectState and ConfigSetView removed;
  /api/config no longer emits detect.enabled.

Future revision can expose the transient detect toggle through
Frigate's MQTT frigate/<cam>/detect/set topic, which is what
Frigate's own UI button uses for the live update path.

* Frigate: replace cursor-hold polling with per-id open-event tracking

Frigate's /api/events?after=T filters strictly on start_time > T, so
a cursor-hold approach (cursor pinned at an open event's start_time
to keep it visible across polls) excludes that event from subsequent
scans even after it closes — the END transition was never observed,
the SensorHistory correlation pair was never completed, and the
operator saw a bare OBJECT_NONE heartbeat instead.

The replacement keeps the cursor strictly monotonic over each event's
start_time, tracks open events by id in a dedicated set, and refreshes
each via GET /api/events/<id> until closed (or force-closed on 404 or
the MAX_OPEN_EVENT_AGE_SECS timeout). FrigateClient._get_json now
raises Http404 directly so callers can distinguish a vanished event
from a transport failure. Removes the AggregatedCameraState model and
the per-camera open/closed aggregation that the old pipeline needed.

Simulator's get_events_after switched to strict > to match real Frigate
so behavior validated against the simulator matches production.

Frigate user-facing docs gain CSP / event-clip-codec / authentication
caveat sections; dev docs document the new polling model and Frigate's
strict-> filter semantics.

* Frigate monitor: post-review cleanups and rename for clarity

Code-review follow-ups on the polling pipeline rewrite. Mostly
refactor and naming polish; no behavior change.

Refactor:
- Phases return (response_map, touched_cameras) tuples instead of
  threading a mutable cameras_touched set as an out-parameter.
- Extract _force_close() helper consolidating the three identical
  emit-response / mark-camera-touched / drop-from-tracking call
  sites in the refresh phase.
- Drop trailing dead `continue` at end-of-loop bodies.
- Cursor-regression guard kept (and its comment rewritten) as
  defensive protection against upstream contract violations.

Rename for clarity:
- OpenFrigateEvent → TrackedFrigateEvent (the wrapper holds HI
  tracking metadata, not just "openness").
- _open_events → _tracked_events; _refresh_open_events_phase →
  _refresh_tracked_events_phase; matching test class/method names.
- Local variable `tracker` → `tracked_event` (stale leftover from
  the original OpenEventTracker dataclass name).
- In the refresh phase, local `event` → `frigate_event` to avoid
  shadowing the wrapper's `.event` field at the assignment site.

Tests:
- _PipelineTestBase switched from TestCase to AsyncTaskFastTestCase
  so per-method asyncio.run() / `import asyncio` are replaced with
  self.run_async() — picks up the project's event-loop tracking.
- Add coverage for cursor no-regression invariant, duplicate-id
  defensive guard, malformed phase-2 refresh payload, and
  camera-list fetch failure in the heartbeat phase.

Dev doc updated for the renamed symbols.
2026-05-21 09:11:20 -05:00
Tony C
a878fb15f1 Conform existing state panels to ROW/TILE size budgets (#343)
* Phase 1: Fix thermostat CSS typos, defer budgets to CSS, enlarge steppers

Three small fixes ahead of the per-panel ROW redesigns:

- thermostat.css:119, :208: tile-template-columns -> grid-template-columns.
  The substitution was introduced inadvertently by the #339 Phase 2
  grid-temp -> tile-temp rename (which matched grid-template-columns as
  a substring). Browsers silently dropped the unknown property, so the
  thermostat modal modes-grid (intended 2-column layout) and the
  thermostat row dial-meta grid (intended 96px 1fr layout) have been
  rendering as default block layouts since #339 merged.

- Framework guide stops duplicating the panel size-budget numbers.
  The doc now references the --hi-panel-tile-* and --hi-panel-row-*
  CSS variables in main.css as the source of truth, keeps the
  qualitative shape and aspect-ratio contracts that aren't in the CSS,
  and adds a note that the height side of the TILE budget is a target
  rather than a clamp (camera live feeds being the canonical case).

- Thermostat modal stepper buttons bumped from 32x32 to 44x44 to meet
  the touch-target rule documented in the framework guide.

* Phase 2: Thermostat ROW redesign with setpoint controls

Replaces the old row layout (96x96 SVG dial + vertical-stacked text
metadata, carried over from the pre-#339 list.html) with a horizontal
strip designed for the ROW context: entity name + mode-status label
on the left, color-tinted current temperature, setpoint stepper(s),
and mode / fan / preset selects across the row width. The dial is
retained in tile and modal contexts.

Antinode's div[data-async] click handler now skips when the click
originated inside an interactive descendant (a, button, input, select,
textarea, [role="button"]). Previously, the wrapping card click
handler fired before any panel-author stopPropagation could
intervene, opening the modal on every stepper or select interaction.
This factoring also retires the "controllers must stop propagation"
contract from the framework guide — panel authors no longer need to
think about it; antinode bows out automatically when the click hit an
inner control.

The pre-existing controller-stopPropagation calls in thermostat.js
were removed as redundant; the corresponding pitfall in the framework
doc was replaced with the touch-target-sizing reminder (still
relevant) and the new "antinode handles it" model.

Progressive disclosure via container queries (@container collection-row)
on .collection-row-list: as the actual content width narrows, hide
right-most lower-priority controllers — preset first, then fan, then
mode select, then the mode-status label. Container queries (not @media
viewport queries) so the breakpoints work correctly regardless of
sidebar / layout-chrome width.

* Phase 3: Smoke detector ROW horizontal-strip layout

Replaces the prior 3-column-grid-with-vertical-meta-stack (badge | meta
column | aux) with a single horizontal flex row. The badge sits left,
the entity name takes the flexible middle, the status label sits next
to the name, and the "Triggered X ago" timestamp + optional battery
readout right-align via margin-left:auto. Status-driven visibility of
the status-label variants and the last-event line is preserved through
the existing CSS rules; the template change is purely structural.

Badge shrunk from 60x60 to 44x44 so the row fits the budget cleanly
(~56px tall with padding).

* Phase 4: Camera ROW redesign without live feed

Drops the 160x90 stream embed in the camera row context — a tiny live
feed is not useful, and the modal already carries the full-size live
view. Replaces it with a status-only horizontal strip: 80x45 static
snapshot thumbnail (or the camera entity icon when no snapshot is
available), entity name, optional motion chip, and an optional
"Motion N min ago" timestamp right-aligned.

The snapshot thumbnail uses the existing entity_video_snapshot
template tag and cache_bust_url helper for cache freshness. Renders
the snapshot as a static <img> only — no polling, no synthetic
streaming markers — because the row is for at-a-glance status, not
live monitoring.

The motion chip's CSS is overridden in ROW context to flow inline
(position: static; flex-shrink: 0) since there's no stream to overlay.
TILE and MODAL contexts keep the absolute-positioned overlay
behavior.

* Phase 5 part 1: Card-fill + ROW status-tint conformance

- .entity-card--tile and --row become ``display: grid`` so the single
  panel-root child auto-stretches to fill the card. Previously the
  card's min-height didn't establish a resolvable parent height for
  the panel's ``height: 100%``, leaving the card's white background
  visible around the colored panel content.

- Smoke detector and thermostat ROW templates now carry the same
  status-tinted backgrounds as their TILE counterparts (smoke decay
  palette: idle / active / recent / past; thermostat HVAC palette:
  heating / cooling). Camera ROW intentionally has no tint — the
  snapshot is the visual.

* Phase 5 part 2: Fallback ROW/TILE redesign + status-aware icon

The fallback row/tile previously rendered the entity icon + name
plus the full per-state list using state_row.html (Bootstrap card
chrome per state). Inside the new .entity-card wrapper this produced
card-within-card-within-card chrome, and entities with many states
overflowed any compact container.

Replaces with a compact-by-design layout:

- ROW: icon + name on the left, then up to N state widgets stacked
  horizontally, growing to fill available row width (capped at
  280px each). flex: 1 1 0 so widgets share remaining space.

- TILE: icon centered atop name, then up to N state widgets stacked
  vertically.

- Per-state widget uses controller_data.html directly (without the
  surrounding row chrome / history button) so sliders, selects,
  switches, etc. remain functional in compact contexts. Sense-only
  states fall back to latest_display_label text.

- N is bounded by Django's |slice filter (currently :3 for ROW,
  :2 for TILE). Easy to tune: search for "|slice:" in the two
  templates. No "more states" indicator — fallback's "show a useful
  subset" matches what typed panels do, and the modal carries the
  full state list.

Status-aware icon (entity_status_svg_icon.html, new):

- Standalone polling-hooked SVG icon template. Renders its own <svg>
  wrapper plus an inner <g> that carries data-state-id / data-status /
  status="..." when a primary_state_data is passed. The <g>-wrap is
  deliberate — the project's existing g[status="..."] CSS rules in
  main.css (~line 720+) author for the LocationView's <g>-wrapped
  icons; placing the polling attrs on the <g> makes the fallback icon
  participate in those rules without needing duplicate svg[status=...]
  selectors.

- Fallback row/tile use the new template, passing the first state of
  state_status_data_list (role-ordered primary) as primary_state_data.
  Stateless entities get no polling attrs (graceful degrade — icon
  still renders, just doesn't animate from polling).

- entity_display_only_svg_icon.html is unchanged; it remains the
  truly-display-only variant for legend / example renderings.

Antinode interactive-descendant skip (asyncClickHandler):

- Added ``label`` to the list of interactive selectors. Toggle-style
  on/off controllers use a <label> wrapping <input type=checkbox> +
  <span class=switch-slider>; the user clicks the visible span,
  which has no other interactive ancestor — closest() needs to find
  the wrapping label to recognize the click as controller-targeted.
2026-05-18 15:21:51 -05:00
Tony C
d7d2d0e67c CollectionView interaction-intent model + panel ROW/TILE rename (#341)
* Phase 1: Spec doc for ROW/TILE rename and CollectionView integration

Update entity-state-panels.md to describe the new framework shape before any
code lands. Renames the LIST/GRID DisplayContext vocabulary to ROW/TILE,
documents per-context size budgets and CSS variables, and adds a
CollectionView integration section covering the four CollectionViewType
values, the whole-card click contract, and the controller pointer-event
convention. Acts as the spec the implementation commits in later phases
will target.

* Phase 2: Rename DisplayContext LIST/GRID to ROW/TILE

Pure rename, zero functional change. Updates the panel-framework
vocabulary to match what authors actually design for (shape), not what
consumers do with the result (layout):

  DisplayContext.LIST  -> DisplayContext.ROW
  DisplayContext.GRID  -> DisplayContext.TILE

Cascade:

- Enum values in entity/enums.py.
- All panel.py declarations (fallback, camera, smoke_detector, thermostat):
  panel object names (*_list -> *_row, *_grid -> *_tile), display_contexts,
  template_name paths.
- Template file renames via git mv: list.html -> row.html,
  grid.html -> tile.html across all four panel directories.
- CSS class hooks and content classes per panel: --list -> --row,
  --grid -> --tile, list-meta -> row-meta, grid-overlay -> tile-overlay,
  grid-temp -> tile-temp.
- Interim mapping in collection_manager (still binary is_grid vs is_list;
  Phase 3 expands to four-value dispatch).
- Test fixture maps in test_state_panel_framework.py.

All 2974 tests pass; lint clean.

* Phase 3: Expand CollectionViewType to four values; rewire camera bypass

Adds DEFAULT and SECURITY values to CollectionViewType (in addition to
existing GRID and LIST), placing DEFAULT first so it becomes the default
for new collections. CollectionType is untouched.

The wrapper-template camera bypass (entity_card_list.html /
entity_card_grid.html) and the grid_css_class branch in transient_models
are rewired from collection.collection_type.is_cameras to
collection.collection_view_type.is_security. Functional behavior is
unchanged at this phase; the trigger shifts from CollectionType-driven
to ViewType-driven, and Phase 4 will retire the bypass altogether in
favor of the panel framework path.

Migration 0003 is data-only: any existing CAMERAS-typed collection has
its collection_view_type_str set to 'SECURITY' to preserve the camera-
montage rendering through the new SECURITY-driven bypass.

Tests cover the four-value classification (every value classifies as
exactly one of is_default/is_grid/is_list/is_security) and persistence
through the model property accessor for DEFAULT and SECURITY.

All 2976 tests pass; lint clean.

* Phase 4: CollectionView wrapper rewrite + adaptive CSS + GRID_LARGE

Replaces the old GRID/LIST card wrappers with four per-CollectionViewType
templates, drops the is_security camera bypass entirely, and switches to
adaptive CSS column counts driven by panel size budgets.

CollectionViewType expands to five values: DEFAULT (icon+name index, no
panel), GRID (adaptive tiles), GRID_LARGE (bigger tiles for camera
montages and dial-heavy panels — 2 cols on tablet, 3 on standard desktop,
4 on wide desktop), LIST (full-width rows), SECURITY (aspirational
placeholder, renders like GRID today).

Whole-card click model: each wrapper is a <div data-async data-href>.
Antinode picks up the click via div[data-async] (extended to read URL
from data-href when href is not valid HTML5 on the element). Inner
antinode-managed links (camera live view) stop propagation naturally,
so the outer card click does not fire when the user clicks an inner
action target. Non-antinode interactive elements (controllers) must
stop propagation per the documented panel-author convention.

Edit mode disables inner interactivity globally via a CSS rule
([hi-edit="True"] .entity-card * { pointer-events: none }) so card
clicks always reach the outer wrapper and open the edit pane —
panel authors do not need to gate their own handlers on edit state.

Fallback panel TILE/ROW templates now render entity icon + name + the
state list, making them self-contained. Stateless entities no longer
produce blank cards.

Migration of CAMERAS-typed collections to SECURITY (from Phase 3)
stands; users who want the original 2-col montage visual can opt into
GRID_LARGE on a per-collection basis.
2026-05-18 12:52:14 -05:00
Tony C
4a7d6d9507 Complete the three initial EntityStatusPanel implementations (#340)
* Phase 2: Entity status panel framework spec

Rewrite docs/dev/frontend/entity-status-panels.md to describe the new
panel framework before any code lands. The spec is the target the
Phase 3 implementation commits will produce. Key concepts:

- Panels are Python declarations under
  src/hi/apps/entity/state_panels/<name>/panel.py with templates and
  static assets in mirrored directory trees. Autodiscovery at app-ready
  time; adding the panel.py file is the entire registration.
- Each declaration carries name, entity_type, display_contexts (Set),
  priority (required int), required_roles + optional_roles (disjoint
  Sets of EntityStateRole), and template_name. No filename convention
  on templates.
- Dispatch is a view-layer concern: a resolver helper selects the
  matching panel from (EntityType, DisplayContext, EntityStateRoles
  present), ordered by priority. Multiple panels per EntityType are
  the supported way to express layout variants; templates stay
  context-agnostic.
- Display contract: declared roles must be displayed by the panel;
  roles outside the declared set are "extras" owned by the framework.
- Modal context auto-appends an expandable "Other states" section for
  extras, giving the user a framework-level invariant that every
  EntityState is reachable in the modal view. Grid and list ignore
  extras silently.
- Required-role access in templates needs no defensive {% if %}.

Also extend entity-status-display.md with one paragraph clarifying
that the polling apply pass tolerates state-set changes across polls
(missing ids are no-ops; new ids picked up on subsequent ticks),
formalizing the contract panel JS handlers already assume.

No code change in this commit. The render_entity_status_panel
template tag, today's three monolithic panels, and the current
fallback-chain dispatch remain in place until Phase 3 lands.

* Tweaks to Claude /pickup command.

* Phase 3a: Entity status panel framework core

Lands the new framework's Python primitives. No callers wired yet: the
existing render_entity_status_panel template tag (and its
resolve_panel_template helper) stay in place until Phase 3b removes
them and migrates the wrapper views.

- DisplayContext enum (MODAL / LIST / GRID) added to entity.enums.
- EntityStatusPanel as a plain @dataclass with __post_init__ validation
  only. No registry coupling; required vs optional fields are
  expressed by dataclass defaults.
- EntityStatusPanelRegistry singleton: discover() walks
  state_panels/<name>/panel.py and registers every EntityStatusPanel
  instance found at module scope (one panel per module is typical;
  multiple are supported). Identity-based dedup in register().
- resolve_panel(entity_type, display_context, present_roles) returns a
  PanelResolution carrying the chosen panel, the computed extras
  (present_roles - declared_roles), and a trace for ?debug_panel=1.
  Type-specific matches win over entity_type=None fallback panels;
  within either group, lower priority wins, name is the alphabetical
  tiebreaker.
- entity.apps.DeviceConfig.ready() triggers discovery.
- 18 unit tests cover declaration validation, registry register/dedup,
  and the full dispatch decision tree (typed wins, priority ordering,
  alphabetical tiebreak, missing-required disqualification, context
  mismatch, extras computation, no-fallback raise).

Doc: rewrite docs/dev/frontend/entity-status-panels.md so the
authoring contract matches the implementation (dataclass instance in
panel.py, not subclass; module-scope scan, not __init_subclass__).

* Phase 3b: retrofit panels onto new framework + wire view layer

Cuts the entity status modal and the collection card wrappers over to
the new EntityStatusPanel framework. Each existing panel becomes a
set of EntityStatusPanel declarations (one per DisplayContext, since
templates are context-agnostic under the new framework), and the
view layer resolves which panel to render before the template runs.

Panel declarations under hi/apps/entity/state_panels/<name>/panel.py:
- thermostat_{modal,list,grid}: required THERMOSTAT_CURRENT_TEMPERATURE,
  optional THERMOSTAT_TARGET_TEMPERATURE + HVAC_ACTION. An entity
  marked THERMOSTAT without a current-temperature state now falls
  back to the flat state list rather than rendering a sparse dial.
- smoke_detector_{modal,list,grid}: required SMOKE, optional
  BATTERY_LEVEL.
- camera_{modal,list,grid}: no required roles (panel renders against
  has_live_view).
- fallback_{modal,list,grid}: entity_type=None catch-all.

Dispatch layer (state_panel_dispatch.py) gains EntityPanelData — a
single dataclass that encapsulates everything a wrapper template
needs (entity_status_data, panel_template, panel_context, trace, plus
forwarded entity / display_category / display_only_svg_icon_item /
extras_state_data_list). build_entity_panel_data(entity_display_data,
display_context) is the single helper both EntityStatusView and
CollectionManager call; both flow the same object into the same
{% include_panel entity_panel_data %} tag. The old per-panel
{% render_entity_status_panel %} dispatch tag and its supporting
legacy resolver are gone.

build_panel_context filters state_status_data_list and
state_status_data_by_role to the panel's declared (required +
optional) roles. Roles outside that set are extras; the modal wrapper
auto-appends an "Other states" expandable section for them, grid and
list ignore them silently. Fallback panels (entity_type=None) see the
unfiltered state list, so the fallback path behaves like today.

Dead {% if current_data %} / {% if smoke_data %} guards on required
roles are removed from the affected panel templates — the framework
guarantees those roles are present whenever the panel renders.

Append ?debug_panel=1 to the entity status modal URL to see the
dispatch trace as an HTML comment.

Test isolation: EntityStatusPanelRegistry gains snapshot_for_tests /
restore_for_tests so framework tests can spin up an isolated registry
and restore production state in teardown.

* Refactor entity state panel naming + dispatch class

Two coupled refactors on the new panel framework, no behavior changes.

1. Wrap the panel dispatch module-level functions in a
   ``StatePanelDispatcher`` class with classmethods, matching the
   codebase convention of namespacing related operations on a class
   rather than at module scope. ``PanelResolution`` and the renamed
   ``EntityStatePanelData`` stay module-level as result types.

2. Disambiguate "Status" vs "State" in the framework's class names.
   The framework renders only the *state* subset of an
   ``EntityStatusView``; the directory / module / dispatcher names
   already use "state_panel," but the class names drifted back to
   "Status." The classes now match the file layout:

   - ``EntityStatusPanel``         -> ``EntityStatePanel``
   - ``EntityStatusPanelRegistry`` -> ``EntityStatePanelRegistry``
   - ``EntityPanelData``           -> ``EntityStatePanelData``
   - method ``build_entity_panel_data`` -> ``build_state_panel_data``
   - variable ``entity_panel_data`` / ``entity_panel_data_list``
     -> ``state_panel_data`` / ``state_panel_data_list``
   - doc ``entity-status-panels.md`` -> ``entity-state-panels.md``

   ``EntityStatusView`` / ``EntityStatusData`` keep their names: those
   genuinely scope to the broader status view that *contains* the
   state panel.

* Address pre-Phase-4 review feedback on state panel framework

Fixes a real bug and applies a batch of framework hardening + cleanup
items surfaced by the four-agent review of the state panel work.

Bug fix:
- ``CollectionViewType.is_grid`` / ``is_list`` were methods, not
  properties. The Phase 3b dispatcher read ``.is_grid`` without
  calling it (truthy bound method), so collections always resolved
  through the GRID context regardless of view type. Convert to
  ``@property`` for consistency with ``CollectionType.is_cameras``;
  templates auto-call either form.

Framework hardening:
- ``EntityStatePanelRegistry.discover()`` catches ``Exception``
  per-module and sets ``_discovered`` only after a successful pass,
  so a malformed third-party panel can't poison Django startup.
- ``{% include_panel %}`` tag passes ``request=`` to ``Template.render``
  so the inner panel renders under a fresh ``RequestContext``,
  re-running context processors.
- ``StatePanelDispatcher.resolve_panel`` gains a ``debug`` parameter;
  trace string formatting is skipped unless ``debug=True`` or
  DEBUG-level logging is enabled. The collection hot path (one
  dispatch per card) no longer pays the trace cost.
- ``EntityStatusView`` opts into trace construction only when
  ``?debug_panel=1`` is set.

API and naming:
- Rename ``EntityStatePanelData.entity_status_data`` field to
  ``entity_display_data`` so it matches the ``EntityDisplayData``
  type it holds; collection card wrappers updated accordingly.
- Drop ``TYPE_CHECKING`` forward-reference; ``EntityDisplayData`` is
  imported directly. Type-annotate ``entity_display_data`` parameters
  on the dispatcher methods.
- Forward ``entity_for_video`` on ``EntityStatePanelData`` to match
  the other entity-level forwards.
- Promote ``extras_state_data_list`` to a typed dataclass field
  (was derived through a dict-key lookup on ``panel_context``).
- Extract role-filtering to ``EntityDisplayData.filter_to_roles``;
  the dispatcher's ``build_state_panel_data`` is now pure assembly.

Cleanup:
- Delete ``EntityDisplayData.to_template_context``; production paths
  flow through the dispatcher, and the two tests that referenced it
  now assert on the dataclass's properties directly.
- Drop ``EntityStatePanelRegistry.reset_for_tests``; ``snapshot``/
  ``restore`` with an empty snapshot covers the same need with one
  fewer public method reaching into private state.
- Sweep ``EntityStatusPanel`` stragglers in JS / CSS comments.
- Drop dead ``{% with sensor_response %}`` in
  ``fallback/state_row.html`` and ``{% if state_status_data_list %}``
  in ``camera/modal.html`` (camera declares no roles, so its
  filtered state list is always empty).
- Add NaN guard in ``thermostat.js`` so an empty ``data-temp-value``
  hides the marker rather than parking it at the dial's top.
- Standardize validation error prefixes on
  ``EntityStatePanel(<name>): ...``.

Tests added:
- ``EntityStatusView`` falls back to the framework fallback panel
  when a typed entity is missing its required roles (integration).
- ``discover()`` skips a broken panel module and continues
  registering the others.
- ``{% include_panel %}`` merges ``panel_context`` over the parent
  context (panel values win on key collision).

* Phase 4a: thermostat panel completion + role-data template aliases

Brings the thermostat EntityStatePanel up to the original mock and adds
a framework convenience for panels with many role lookups.

Framework extension — role_data_template_aliases:
  EntityStatePanel gains an optional ``role_data_template_aliases``
  declaration: a dict mapping template-context variable names to
  declared EntityStateRoles. The dispatcher resolves each alias against
  the entity's by-role map at render time and seeds ``panel_context``
  with the named variables, so templates use ``{{ current_data.X }}``
  directly instead of chaining ``{% with %}`` blocks (which Django
  cannot parse across newlines, making chains of more than a few
  bindings unwieldy). Validated at construction: every aliased role
  must be in ``required_roles | optional_roles``; values must be
  ``EntityStateRole`` members. Recommended threshold is 5+ named
  roles per panel — smaller panels keep the lighter ``{% with %}``
  form.

Thermostat panel completion:
- Required role unchanged: THERMOSTAT_CURRENT_TEMPERATURE.
- Optional roles expanded to cover dual setpoint
  (THERMOSTAT_TARGET_TEMPERATURE_LOW + _HIGH), HVAC_MODE, FAN_MODE,
  PRESET_MODE, HUMIDITY (plus the prior TARGET / HVAC_ACTION).
  SWING_MODE intentionally excluded — not exposed by the HA simulator.
- Nine role_data_template_aliases declared so the modal / list / grid
  templates read at the top level (current_data, target_data, low_data,
  high_data, action_data, mode_data, fan_data, preset_data,
  humidity_data). Each template opens with a comment listing the
  aliases.
- Modal template gains a controls grid with a setpoint stepper
  (single-row or dual-row depending on which setpoint roles are
  present) plus Mode / Fan / Preset rows that delegate to the existing
  controller-widget machinery when controllable and fall back to
  read-only labels otherwise. Humidity surfaces as a secondary
  readout. Custom −/+ stepper widget posts each adjustment through
  the same async-form mechanism the slider uses, but routes the
  response into a hidden sink so the buttons survive across clicks;
  polling refreshes both the visible label and the hidden form value.
- List card shows dual-or-single setpoint summary + humidity line.
- Grid card shows dual-or-single setpoint in the mode summary line.
- CSS extended for the modal controls grid, stepper widget, and
  secondary readout row.
- JS gains a stepper click handler that bumps the form's hidden
  value by the button's data-stepper-delta and submits.

Sense-vs-controller asymmetry: each interactive widget checks
``<role_data>.controller_data_list`` to decide whether to render the
controllable variant or a plain readout, so a future integration
exposing thermostat target as sense-only still renders correctly.

Spec doc: ``role_data_template_aliases`` added to the declaration
table and a two-paths section explains when to prefer the alias form
over a single ``{% with %}``.

* Phase 4a polish: thermostat dynamic UI + unified status-apply path

Wraps up the thermostat panel work with the runtime polish that
emerged from exercising it in the browser, plus a framework-level
refactor that benefits every panel.

Thermostat polish:

- Mode-driven single/dual setpoint visibility. HA exposes target /
  target_low / target_high simultaneously, so role-presence alone
  can't pick the active setpoint shape. The panel root carries
  ``data-hvac-mode`` (initial value rendered server-side, kept in
  sync at runtime by the JS update handler against the polled mode
  state); CSS rules at end-of-file show the dual block under
  ``heat_cool`` / ``auto`` and the single block otherwise. Both
  blocks render so the toggle is a CSS swap, not a re-render.

- Layout stability across the toggle. Setpoints occupy their own
  centered row; Mode / Fan / Preset live in an independent 2-col
  grid below. Setpoint count no longer reflows the modes row.

- Custom thermostat select widget that bypasses antinode. The
  standard ``controller_data.html`` form-submit replaces the entire
  widget with the server response on every change, which conflicts
  with the dial / panel-root state the thermostat owns. The
  thermostat-local ``role_control.html`` renders the same
  ``.discrete-select`` widget (preserving the global styling) but
  drops ``onchange-async`` / ``data-async`` and lets the panel's
  own JS handler POST to the controller endpoint.

- Single-line ``{% with %}`` everywhere. Django's tag lexer doesn't
  span newlines despite the ``re.DOTALL`` apparent suggestion;
  multi-line ``{% with %}`` silently parses as text and orphans
  ``{% endwith %}``.

Framework: unified status-apply path
(``src/hi/static/js/entity_state_status.js``)

- Generic ``change`` listener on every
  ``[data-state-id][data-controller-value]`` element synthesizes a
  one-entry statusMap from the new value and runs it through the
  existing ``Hi.entityStateStatus.apply`` pipeline. All dependent
  DOM (display labels, marker positioning, panel-root attributes
  like ``data-hvac-mode``) updates immediately on user interaction,
  not on the next polling tick. Polling reconciles or corrects.

- The server-bound submit path (antinode's ``onchange-async``, or a
  panel's custom ``$.post``) is unchanged and runs independently of
  this handler — error responses still surface through the normal
  mechanism; rejected changes are reverted by the next poll.

- Optimistic format preservation. Numeric synthesis reads the
  decimal precision and unit suffix from the paired
  ``[data-display-text]`` element and applies them to the new value,
  so an optimistic ``73.0°F`` matches the canonical ``72.0°F``
  formatting until the next poll. Heuristic, but covers the common
  decimal + unit case; a follow-up issue should add a server-driven
  format spec to the polling payload for the general case.

Collection cards:

- The entity-name link in collection cards now opens the entity
  status modal in non-edit mode (matching LocationView click
  behavior). Now that cards render a per-type panel rather than the
  full flat status list, the modal is where complete status lives.

* Phase 4b: smoke detector panel completion + low-battery framework status

Brings the smoke detector EntityStatePanel up to the mock and adds
a framework-level low-battery status derivation that any panel
exposing a BATTERY_LEVEL state can opt into.

Smoke detector panel:

- All three contexts (modal / list / grid) restructured to the
  thermostat-era patterns: ``role_data_template_aliases``
  (smoke_data, battery_data), top-of-template alias comment, and
  ``svg_status_style.status_value`` for initial render so the
  decay-aware token (``idle`` / ``active`` / ``recent`` / ``past``)
  matches the vocabulary the polling pipeline emits — no dual
  ``smoke_clear`` / ``smoke_detected`` CSS rule set anymore.
- Four-state badge hero per mock: shield-check (idle), exclamation
  (active), and two clock variants (recent / past). Color tints
  follow the existing decay-color palette. Active-state pulses
  via CSS keyframes.
- Grid card gains a tinted background per state and a dedicated
  ``Low Battery`` warning shown only in low-battery state.
- The status detail line (``Triggered just now`` / ``Alarm 14m
  ago`` / ``No events in the last 30 days``) is intentionally
  deferred — those text variants need recent-sensor-history data
  that the framework doesn't yet expose. Captured for a follow-up
  framework discussion after Phase 4c.

Framework: BATTERY_LEVEL status derivation:

- ``EntityStateDisplayData._get_svg_status_style`` now dispatches
  to ``_get_battery_level_status_style`` for BATTERY_LEVEL states,
  emitting ``StatusStyle.BatteryLow`` when the value is below
  ``BATTERY_LOW_THRESHOLD_PCT`` (20%) and ``StatusStyle.BatteryOk``
  otherwise. The polling pipeline carries the token through the
  existing ``data-status`` opt-in, so any panel can react with
  CSS rules on ``[status="low"]``. Threshold lives on the class so
  it's adjustable in one place.

Simulator:

- New ``HassSmokeDetectorWithBatteryFields`` combo entity (smoke +
  battery states), modeled on the existing leak-sensor +
  battery shape. Zoo profile gets a second smoke detector entry
  named ``Zoo Smoke Detector (Battery)`` to exercise the
  battery aux readout and low-battery escalation paths.

* Phase 4c: camera panel completion + motion chip + extras inline

Brings the camera EntityStatePanel up to the polish bar set by the
earlier panels, and adjusts the framework extras section so its
modal presentation matches the "every state reachable" invariant.

Camera panel:

- One declaration per DisplayContext (no role-based variant split —
  the motion treatment is a small visual enhancement, not a layout
  fork, so the optional-role pattern from the smoke detector
  applies). MOVEMENT is declared optional with a ``motion_data``
  alias; the template guards the chip with ``{% if motion_data %}``.
- Motion chip overlay on the stream corner when the camera entity
  carries a MOVEMENT role. Uses the framework's decay-aware status
  tokens (active / recent / past / idle) so the chip color
  matches the rest of the app's motion treatment, including the
  ``--on-status-*-color`` companion tokens for foreground contrast.
  Pulses on ``active``. 36px in modal, 24px in compact contexts.
- Per the user's redirection of the mock, the live feed continues
  to flow through the existing ``entity_live_view_w_link.html``
  template — the panel handles sizing and the chip overlay, not
  the complications of stream rendering / offline placeholders.
- Connectivity, "Last seen X min ago", and ZM-Function-specific
  layout from the mock intentionally deferred: per-camera
  connectivity isn't tracked anywhere today, recent-history
  timestamps require framework work that hasn't landed, and the
  ZM Function controller falls cleanly into the framework's
  extras section without bespoke handling.

Framework: extras section inline:

- ``extras_section.html`` no longer wraps content in
  ``<details><summary>Other states</summary>``. The collapsible
  predated the framework decision that modal must surface every
  state unconditionally; with that invariant in place, the
  collapsible defeats the contract. The "Other states" heading
  is also dropped — "state" isn't user-facing nomenclature, and a
  section label adds nothing of value.

* Extract shared partials for smoke detector badge and camera motion chip

The badge SVG icons (idle/active/recent/past) and the camera motion chip
markup were duplicated across modal/list/grid contexts. Move each into a
single included partial so the icon/chip definitions live in one place
and the per-context templates only carry their layout-specific wrapping.

* Phase 5+6: Expose recent state-value history to panels; adopt in smoke detector

SensorResponseManager already caches the last 5 deduped sensor values
per state, and EntityStateDisplayData already holds that list. Promote
it from internal threshold input to a first-class, display-ready
accessor — recent_state_value_summary — so panels can surface "how
recent" detail without DB queries.

Each cached entry's value flows through the same ConsoleConverterHelper
pipeline as latest_display_label, so history rows render in the user's
preferred unit. The summary is server-render-only (deliberately not in
the polling payload) and makes no completeness claims about the cache
window.

Smoke detector modal and list contexts now render "Triggered N minutes
ago" when status decays to recent or past; idle and active suppress.
Grid is intentionally left tight — the badge color carries the in-glance
signal and the modal carries the detail.
2026-05-17 21:19:30 -05:00
Thiago Trautwein Santos
e2c0867cb0 Feature/82 Implement Media Thumbnail Previews for File Attributes (#267)
* Implement media thumbnail previews for file attributes

* Replace PyMuPDF with pdf2image for PDF thumbnail generation

* create utility file for thumbnail generation

* Refactor thumbnail generation logic

* Add backfill command for missing file thumbnails and update docker_entrypoint script

* Added poppler dependency (needed for pdf2img) to dev setup docs.

* Fixed file card grid alignment with uniform thumbnail heights.

* Fix thumbnail backfill command test: filename desync via direct path

The test saved a file at a hardcoded source_path, then created an
EntityAttribute with file_value=source_path. AttributeModel.save() calls
generate_unique_filename() to add a timestamp suffix to file_value.name,
which desyncs the attribute's stored path from the on-disk path —
file_value.size then raises FileNotFoundError inside the backfill command.

Pass the file bytes via ContentFile(..., name='...') instead. The model
now owns the full filename-generation lifecycle and the on-disk and
in-database paths stay in sync.

Applied symmetrically to the dry-run test for consistency, even though
its assertions happened to pass without seeing the underlying mismatch.

* Switch thumbnail backfill from Docker entrypoint to render-time lazy gen

The Docker entrypoint's call to ``backfill_attribute_thumbnails`` had
unbounded startup cost for users with many file attributes, paid
overhead on every container restart even when nothing needed
generation, and was a permanent ongoing entry point for what's really
a transient migration need.

Replace with synchronous lazy generation triggered the first time a
file card is rendered for an attribute that lacks a thumbnail. Each
file attribute pays the generation cost once, on first view, spread
across actual usage instead of forced into startup.

- ``package/docker_entrypoint.sh``: drop the backfill invocation.
- ``AttributeModel.ensure_thumbnail()``: model method that generates
  if missing, no-op otherwise.
- ``{% ensure_thumbnail attribute %}``: template tag wrapper for
  call-from-template ergonomics.
- ``file_card.html``: invoke the tag once at the top, before any
  ``has_thumbnail`` / ``thumbnail_url`` reads.

The ``backfill_attribute_thumbnails`` management command stays
available for users who want to warm the cache explicitly. Its
counters remain accurate because the new render-time path is
isolated to ``ensure_thumbnail()`` — ``has_thumbnail`` stays a
pure existence check.

* Harden PDF thumbnail generation against pathological input

The 20MB pre-render byte cap was the only protection against expensive
thumbnail generation, but PDF rendering cost doesn't correlate well
with file size — a small PDF can produce a multi-GB pixel buffer at
pdf2image's 200-DPI default, and a crafted PDF can hang the underlying
pdftoppm subprocess indefinitely.

Three protections, all on the PDF path:

- ``size=(640, 640)`` passed to ``convert_from_bytes()``: caps the
  rasterized output dimensions, keeping pre-resize memory bounded.
  640 gives roughly a 2x oversample of the 320x320 thumbnail for
  LANCZOS quality without runaway buffers.
- ``timeout=30`` threaded through to the pdftoppm subprocess so
  pathological content can't hang generation.
- 10MB per-mime-type source-byte cap for PDFs, separate from the
  existing 20MB cap for images.

Constants exposed as class attributes on ``AttributeThumbnail`` so
they're discoverable and tunable in one place.

* Install poppler-utils in CI for pdf2image-dependent tests

pdf2image shells out to pdftoppm (from the poppler-utils package) to
rasterize PDFs. The Docker image already installs poppler-utils, but
the GitHub Actions workflow runner did not, so
test_generate_thumbnail_best_effort_pdf_success passes locally and in
Docker but failed in CI on the ``assertTrue(generated)`` check.

Add an apt-get install step for poppler-utils so CI exercises the
same PDF-rendering surface as production.

* Add tests for AttributeModel.ensure_thumbnail() lazy-generation hook

ensure_thumbnail() wraps AttributeThumbnail.generate_thumbnail_best_effort()
with three branches that aren't transitively covered by the existing
generator tests or by the backfill command tests:

- supported file, thumbnail missing -> generates
- thumbnail already present -> short-circuits before instantiating the
  generator (verified by mocking AttributeThumbnail)
- unsupported mime type -> short-circuits at the supports check

Co-located with the existing generate_thumbnail_best_effort_* tests
so a future reader finds the lazy hook's coverage in the same place
as the generator's direct coverage.

---------

Co-authored-by: Anthony Cassandra <github@cassandra.org>
2026-05-15 14:08:27 -05:00
Tony C
3c33cf29cf EntityStatusPanel framework, dispatcher, and three pilot panels (#328)
* Add EntityStatusPanel framework: dispatch tag, presentation helper, JS hook

Lays the foundation for per-EntityType panels in the three display contexts
(modal, list, grid). No panels yet — just the plumbing the panels will use.

The render_entity_status_panel template tag resolves a panel along a three-
step chain: panel's context-specific template, panel's required default
(modal.html), framework fallback. Framework fallbacks are guaranteed to
exist and currently delegate to the existing flat-list / EntityStatus
default partials so behavior is preserved for every EntityType without a
panel.

The panel_state template tag looks up an EntityState by EntityStateRole
for panel templates that need to reach a specific state by semantic
purpose (e.g., a thermostat panel reaching for its current-temperature
state vs. the setpoint). Walks the entity's own states then delegations;
own-states win on role collision.

The JS dispatcher in entity_state_status.js gains two small additions:

  - The status forwarding selector relaxes from div[status] to [status]
    so panels can use any element tag for the status carrier.
  - After the existing apply pass, registered EntityStatusPanel handlers
    are notified with the statusMap. Panels that need to react beyond
    CSS-driven variant switching register a handler via
    Hi.statePanels.register; panels that don't pay nothing.

Dispatch is wired into the three call sites: EntityStatusView modal body,
collection list card, collection grid card. The legacy entity_status_<type>
modal templates and the collection list/grid is_cameras branches are no
longer in the new resolution chain but stay in place — their migration is
deferred to Phase 3 and Phase 6 respectively.

Tests cover the dispatch fallback chain (per-context resolution, panel
overrides, modal-as-default for list/grid, unsupported context errors)
and the panel_state role-lookup (case-insensitive match, missing role,
delegated states, own-state wins on collision).

* Add smoke detector EntityStatusPanel; rework dispatch tag to take entity_status_data

The smoke detector panel is the CSS-only reference panel. Templates emit
the four visual variants (clear / alarm / recent / past) as separate
elements with class-modifier suffixes; CSS keyed on the panel root's
``status`` attribute shows the one matching the current state. Two
status vocabularies land on the root over a panel's lifetime — the raw
sensor value (smoke_clear / smoke_detected) on initial server render,
and the decay-aware value (idle / active / recent / past) once the first
polling refresh lands. CSS comma-groups them so both render the right
variant.

The dispatch tag's signature changes to take ``entity_status_data``
rather than ``entity``. The tag calls ``entity_status_data.to_template_
context()`` and merges those fields into the panel template's context,
so panels use top-level names (entity, state_status_data_list,
state_status_data_by_role, etc.) regardless of whether the dispatch fires
from the modal, list card, or grid card call site. This resolves the
prior asymmetry where the modal context had fields unpacked at top level
but collection card contexts only had ``entity_status_data`` as a single
variable.

EntityStatusData gains a ``state_status_data_by_role`` property — the
role-keyed lookup panel templates use to pull a specific state by
semantic role (e.g., ``state_status_data_by_role.smoke`` on a smoke
detector, ``state_status_data_by_role.battery_level`` for the battery
auxiliary). The dispatch tag exposes it via ``to_template_context()``,
which now also self-references ``entity_status_data: self`` so the
modal's own template can pass the dataclass to the dispatch tag.

Pipeline registers the smoke detector CSS in the ``css_head`` bundle.
Per-panel asset auto-loading is deferred per the v1 spec.

* Add camera EntityStatusPanel and framework-level identifying chrome

Camera panel composes existing video-stream and state-status partials:
the existing entity_video_stream_w_link.html for the live frame
(click-through to the full-page video pane preserved), and the existing
entity_state_status_list.html for any controllers/sensors the entity
exposes (so ZoneMinder Function dropdown and the like render through
the established controller_data machinery). No new JS — connection
management, MJPEG rendering, and integration-gateway lookup all stay
where they live. The is_cameras collection branches are unrelated
(montage view) and stay in place.

EntityStatus modal chrome reworked to provide identifying information
at the framework level rather than each panel template repeating it.
Modal title carries the entity name. A new subheader strip inside the
modal body shows entity-type icon + label on the left, integration
logo centered (when present), and the "Status" modal-type label on
the right. Panels render their body below this strip and don't
duplicate any of this identifying chrome. The smoke detector and
camera panel templates lose their per-panel headers accordingly.

* Tighten polling-update scope and make the flat-list fallback a panel

Polling-update dispatch in entity_state_status.js previously expanded its
scope from the matched CSS-class element to its descendants when looking
for ``display-text`` / ``[status]`` markers. That worked for the flat-
list partials' wrapper-with-class + child-with-attr structure but bled
across state boundaries when a state's CSS class anchored a panel root
containing other states' display elements.

Tightens the contract: an element receives a polling update only when
it has BOTH the matching CSS class AND the marker attribute (``status``,
``display-text``, etc.) on the same element. No descendant propagation.
``filter('[attr]')`` replaces ``find('[attr]').addBack(...)``; the
``else if (attrName == 'status') { find('div[status]') }`` legacy path
is gone.

The sensor flat-list templates that previously wrapped the value div in
a class-bearing wrapper are restructured: the value div carries the
class itself, and the outer wrapper is removed.

entity_state_status.js absorbs the universally-useful pieces of the old
controllers.js (active-slider tracking, value-to-element dispatch,
slider-display sync, checkbox value coercion, slider drag input mirror)
and combines attribute / display / controller-value application into
one pass per CSS class — the prior two-stage apply with a separate
``Hi.controllers.applyValueMap`` callout dropped.

The flat-list-specific bits that remained in controllers.js — the
``.on-off-control`` / ``.status-text`` caption mirror and the
continuous-slider preset-button click handler — move into a new
``state_panels/fallback/fallback.js`` and register with the post-apply
panel hook like any other panel JS. The fallback is now its own panel
(matching the existing fallback templates in state_panels/fallback/).

controllers.js deleted; pipeline updated accordingly (entity_state_
status.js promoted into the always-loaded ``js_before_content`` bundle
so the fallback panel's ``Hi.statePanels.register`` call has the
framework's panel-handler registry in scope).

* Self-describing polling dispatcher and thermostat panel

Dispatcher rewritten around a data-state-id anchor plus per-element
declaration attributes (data-status, data-display-text/magnitude/unit,
data-controller-value, data-svg-style). Replaces the class-based
join that forced authors to fuse a CSS class and an update marker
onto the same element and shared the status attribute namespace with
the global div[status] paint. Each element now opts in directly;
no descendant traversal, no propagation leaks.

Server-side statusMap keyed by entity_state.id with nested
{status, controller, display, svg_style}. SVG icons opt in via
data-status (singular push); SVG paths use data-svg-style (full
bundle) so each element only receives the attributes it rendered.

Global div[status] rule rescoped to .hi-status-display[status],
freeing the status attribute for panel use. EntityStateDisplayData
delegates raw accessors via __getattr__ so mainline templates that
took raw status data also accept the display wrapper.

Panel framework adds registerInit alongside registerUpdate. Init
handlers fire at jQuery ready and after async content insertion;
antinode gets a dedicated afterModalRender hook so JSON-delivered
modals get their init pass once the modal is in the DOM (the
existing afterAsyncRender stays at its earlier position to keep
scroll-bar restore ahead of resetScrollbar/scrollTo).

Adds thermostat EntityStatusPanel (modal/list/grid) with SVG dial
markers positioned via the registered panel JS.

Renames monitor/transient_models.py to status_data.py and
status_display_data.py to display_data.py for symmetric naming;
EntityDisplayData wraps raw EntityStatusData with display
projections (CollectionData and EntityStatusView adopt it).

* Document the icon vs path polling-update asymmetry

SVG icons opt into polling updates via data-status while paths use
data-svg-style; an existing bug fix made the choice mechanical but
the rationale lived only in conversation. Adds a short subsection
under Client-Server Status Updates explaining why icons can't
safely receive the full attribute bundle (SVG inheritance fights
child-specific styling) and why paths can (single element with a
Python-parameterized palette).

* Consolidate fallback panel templates into the panel directory

The EntityStatusPanel fallback was structured as three thin
wrappers that included historical pre-framework templates living
elsewhere. The asymmetry forced new readers to chase indirection
to understand what the fallback actually renders, and left
chrome-rendering duplicated between the legacy default body and
the modal framework subheader.

Inlines the fallback's modal/list/grid templates and moves the
flat-list partials (now state_list.html and state_row.html) into
state_panels/fallback/, since fallback is where those partials
were first authored. Camera and thermostat panels — which embed
the flat list inside their own modal layouts — reference the
partials from their new home.

Drops the duplicated identifying chrome (entity name, integration
logo, type label) from the fallback modal body; the framework's
subheader block in entity_status.html already renders it.

Deletes orphan legacy templates: entity_status_default.html, the
unused per-type wrappers entity_status_thermostat.html and
entity_status_ceiling_fan.html, and the collection-card list/row
pair (collection/panes/entity_state_{list,row}.html) which had
diverged from entity/panes/entity_state_status_{list,row}.html on
short-name vs full-name with no real justification — both contexts
display the entity name in surrounding chrome, so the short-name
form is correct in both.

* Remove dead in_modal_context flag from controller form rendering

``in_modal_context`` was originally a hint to controller_data.html
about whether to emit ``data-stay-in-modal="true"`` (which keeps
the entity status modal open across control submissions) and a
hidden ``response_context`` input the server read back to preserve
the flag across the form-replace round trip.

The configurability was already dead: ``controller_data_row.html``
hardcoded ``in_modal_context=True`` at the only template boundary
where the value reached ``controller_data.html``, so every
controller form already emitted both signals. Outside a modal,
``data-stay-in-modal`` is a no-op because antinode's modal-close
check gracefully handles the no-modal case.

Drops the flag everywhere: hardcodes ``data-stay-in-modal="true"``
on the controller form, removes the hidden ``response_context``
input and its server-side parse, drops ``in_modal_context`` from
``controller_data_response``'s signature, and strips the unused
``in_modal_context=True`` from the panel templates that propagated
it (camera/modal, thermostat/modal, fallback/{modal,list,grid}).
No observable behavior change.

* Reorganize and reconcile entity display frontend docs

The three existing docs (entity-status-display, entity-status-panels,
entity-visual-configuration) overlapped on the polling-update
contract while none gave a top-level mental model. The two older
docs also contained fictional code samples that pre-dated the
data-state-id dispatcher refactor and actively misled readers.

Adds entity-display-overview as a one-page architecture entry
point: the sensor-to-pixel flow, the four display surfaces
(LocationView SVG icons, paths, status modal, collection cards),
the polling contract as connective tissue, and a topic-keyed
index into the focused docs.

Rewrites entity-status-display as the polling-update contract
reference: server payload shape, the data-state-id anchor and
declaration grammar, status-value vocabulary, color palette, and
the icon-vs-path asymmetry. Removes the fictional CSS-class
mapping examples and the fictional thermostat workflow that
pre-dated the dispatcher refactor.

Trims entity-status-panels' polling-contract section to defer to
the contract doc rather than restating it.

Slims entity-visual-configuration to its accurate core: SVG icon
and path asset authoring, EntityStyle registration, and per-type
StatusStyle binding. Removes the fictional Template Integration
section and the two fictional workflow walkthroughs that invoked
methods (get_status_css_classes, get_path_styling) that don't
exist.

Each doc now cross-links to all three others.

* Surface integration health and improve controller error chrome

Adds two failure-mode UX improvements to the entity status modal:

1. Integration health banner. When an entity's owning integration
   reports WARNING / ERROR / DISABLED, the modal renders an alert at
   the top of its body indicating the integration's status and that
   displayed values may be stale and controls may not respond. The
   banner is a snapshot at modal-open time (no live tracking); it
   renders nothing for healthy integrations.

   Implemented as a reusable inclusion tag
   ``integration_health_banner`` in the integrations app, so any
   surface that depends on an integration's data can drop in the
   banner with a single template line plus an optional context
   message describing how the degraded state affects that surface.

2. Controller error chrome. Replaces the bullet-list red-text error
   block on failed controller actions with a Bootstrap alert-danger
   that uses an icon, a compact small-text body, and one line per
   error message. The styling matches the rest of the application's
   error UI; the message content channel is unchanged.

* Use exclamation-circle for critical health status icons

Aligns the icon for FAILING / critical health status with the
existing icon for ERROR / UNAVAILABLE — both states are attention
signals, and a shared exclamation-circle glyph reads more
consistently across the health-status UI than the previous
times-circle. Updates ApiHealthStatus.status_icon,
HealthStatus.status_icon, and the test-UI page that mirrors the
critical-state icon.

* Fix Firefox video stream connection leaks via service worker and lifecycle hooks

Three reinforcing changes that together resolve Firefox failures when
loading pages with multiple MJPEG video streams (live camera streams
and event-video playback). Chrome was unaffected; Firefox's service
worker handling of concurrent multipart/x-mixed-replace responses
combined with no explicit teardown on async content swap caused
connections to leak across page navigations and exhaust the per-host
pool.

Service worker scope. The fetch handler now intercepts only the
pre-cached static assets list, returning early for everything else.
MJPEG streams, polling, and dynamic pages flow through the browser's
native fetch path, avoiding the multipart-via-SW edge that Firefox
mishandles when multiple streams are concurrent.

Antinode before-render hook. Adds addBeforeAsyncRenderFunction
mirroring the existing after-render hook. Antinode fires it with the
outgoing $target before each HTML content swap, giving subscribers
the chance to clean up resources tied to elements about to be
orphaned. Generic; antinode no longer knows about video.

Video connection manager rewrite. Replaces the single-current +
small-previous-cache design (which used querySelector and could only
ever see one element) with a Set tracking every <img> opted in via
data-video-stream. Registers a beforeAsyncRender callback that
force-closes streams in the outgoing subtree by swapping src to a
transparent GIF, terminating the fetch before the browser orphans
the element. Reconciles on each afterAsyncRender / afterModalRender
to keep the set honest. Adds the marker to all six stream <img>
templates: live stream, event playback, alert pane and modal video
elements, sensor history modal.

* Address post-review feedback: lifecycle, caching, SW scope, hook isolation

Four follow-up fixes from the pre-PR review pass:

1. Modal-dismiss video stream leak. Renamed antinode's
   ``addBeforeAsyncRenderFunction`` to ``addBeforeContentRemovalFunction``
   so the name reflects what it really fires for — any subtree about to
   be detached from the DOM by an antinode-driven operation. Added a
   second call site in ``handleModalHiddenEvent`` before the modal is
   removed, so VideoConnectionManager force-closes streams on modal
   dismissal in addition to HTML content swap. Antinode has no direct
   reference to the manager; both call sites just invoke registered
   callbacks.

2. Antinode hook isolation. The three lifecycle loops
   (``beforeContentRemoval``, ``afterAsyncRender``, ``afterModalRender``)
   now wrap each callback invocation in try/catch and log on error.
   Matches the pattern already in place in entity_state_status.js'
   panel-handler loops. A throwing handler logs and yields rather than
   skipping siblings or bubbling into antinode's caller.

3. EntityDisplayData per-state wrapping memoization. Adds a
   ``state_display_data_map`` field initialized in ``__post_init__``
   that wraps each contained state in ``EntityStateDisplayData``
   exactly once. The two projection properties
   (``state_status_data_list``, ``state_status_data_by_role``) now
   look up wrappers from the map. Each per-state cost (the
   ConsoleConverterHelper lookup and ``_get_svg_status_style`` dispatch)
   is paid once per render even when both projections are touched
   (the typical ``to_template_context()`` path).

4. Service worker rewrite. The fetch handler now intercepts only
   paths under Django's STATIC_URL prefix; the enumerated
   STATIC_ASSETS list goes away entirely so the SW doesn't need
   manual maintenance as the static asset surface grows. Cache-first
   with on-demand population on miss; bumped CACHE_VERSION to wipe
   any stale state from the previous SW. STATIC_URL is exposed to all
   templates via the existing constants_context processor.
2026-05-14 20:27:56 -05:00
Tony C
d2449ef0fc Issue #319: EntityStateRole for semantic role per EntityState (#322)
* Introduce EntityStateRole for semantic role per EntityState

Adds a first-class semantic-role concept to EntityState so the data
model can express what each state means within its enclosing entity.
Resolves the multi-of-same-type case (a thermostat's four
TEMPERATURE substates; a fan's two DISCRETE substates) and replaces
the legacy EntityStateType-priority pre-filter that bucketed icon
display by type rather than per-entity primary state.

Schema and model
- New EntityStateRole(LabeledEnum) in hi/apps/entity/enums.py with
  two tiers: type-default roles (one per EntityStateType, name-
  matched) and domain-prefixed refinements (THERMOSTAT_*, FAN_*,
  LIGHT_*, HVAC_*).
- New EntityStateType.default_role() method.
- New EntityState.role_str field + entity_state_role property /
  setter. Save() defaults role_str to the type's default when not
  set explicitly, so direct EntityState.objects.create() paths get
  a role automatically.
- Migration 0016 backfills existing rows with their type-default
  role; behavior pre-refinement is unchanged.

Factory and HA integration
- HiModelHelper.create_sensor / create_controller gain an optional
  entity_state_role parameter; threaded through to EntityState
  creation when provided.
- HassConverter._SubstateSpec renamed _StateSpec; new role field on
  the spec. Climate / fan / light substate spec lists assign
  domain-prefixed roles for the multi-of-same-type cases.

Modal listing order
- New EntityStateRoleOrdering class + module-level
  ENTITY_STATUS_VIEW_ORDERING instance in
  hi/apps/entity/entity_state_role_order.py with per-EntityType
  override maps.
- EntityStatusData.to_template_context() emits state_status_data_list
  (sorted by role priority); the underlying field stays order-
  neutral for non-modal consumers.

LocationView icon primary-state selection
- ENTITY_PRIMARY_STATE_ORDERING (separate instance) selects the
  primary state for entity-level visual representation.
- LocationViewData._get_latest_entity_state_status_data_map switched
  from timestamp-sort to role-priority sort.
- StatusDisplayManager.get_entity_to_entity_state_status_data_list
  no longer pre-filters by EntityStateType priority; returns all
  states (including delegations). Shared
  _all_entity_states_including_delegations helper introduced.
- LocationViewData._get_css_class_map now emits exactly one
  hi-entity-state-* class per entity (the primary state's), so
  per-state polling updates don't clobber the entity's status
  attribute via shared CSS classes.

EntityStatusView template dispatch
- entity_status.html outer wrapper dispatches via include_with_fallback
  to per-EntityType templates; entity_status_default.html carries
  the existing flat-list body. Placeholder thermostat / ceiling_fan
  templates establish the customization pattern.

Decision-point markers
- LocationViewType and OneClickControlService._find_controller carry
  comments noting the legacy entity_state_type_priority_list is
  retained for one-click, pending a parallel
  ENTITY_CONTROL_STATE_ORDERING follow-up.

Docs
- integration-guidelines.md gains "EntityStateType vs.
  EntityStateRole" section explaining the two axes and the
  integration's role-declaration responsibility.

Closes #319 (with manual verification of thermostat / fan / icon
status behavior). Audit of EntityStateType vs role conflation
(MOVEMENT vs PRESENCE, POWER_LEVEL vs LIGHT_DIMMER) and one-click
ordering consolidation are out of scope and tracked as
follow-ups.

* Migrate OneClickControlService to role-based selection; drop legacy type-priority filter

Completes the role-mechanism conversion: one-click control no
longer consults LocationViewType.entity_state_type_priority_list
and instead walks ENTITY_CONTROL_STATE_ORDERING for the entity's
EntityType. The legacy type-priority pre-filter on
LocationViewType is removed entirely along with its dead
consumers in StatusDisplayManager.

OneClickControlService
- execute_one_click_control and _find_controller no longer take
  a location_view_type kwarg. The view-level "only AUTOMATION
  invokes one-click" gate remains at LocationItemStatusView.
- _find_controller walks ENTITY_CONTROL_STATE_ORDERING.order_for
  strictly: only states whose role is listed are eligible. Unlisted
  roles (e.g., a thermostat's THERMOSTAT_TARGET_TEMPERATURE) are
  not one-click targets even when controllable.
- _is_toggle_eligible helper extracted to keep the picker honest
  (only states with toggle_values within ONE_CLICK_CHOICE_LIMIT
  qualify).

ENTITY_CONTROL_STATE_ORDERING
- New EntityStateRoleOrdering instance with a curated default list
  of universally safe-to-toggle roles: ON_OFF, OPEN_CLOSE,
  OPEN_CLOSE_POSITION, POWER_LEVEL, LIGHT_DIMMER. Binary roles
  come first so they win when both binary and continuous variants
  exist.
- Per-EntityType overrides for LIGHT (LIGHT_ON_OFF + LIGHT_BRIGHTNESS),
  CEILING_FAN / EXHAUST_FAN (FAN_SPEED), GARAGE_DOOR_OPENER (OPEN_CLOSE).
  Switches / outlets / locks rely on the default ON_OFF fallback.

Legacy removals
- LocationViewType.entity_state_type_priority_list and its __init__
  override are gone; the enum collapses to plain DEFAULT / SECURITY
  / AUTOMATION members carrying just label and description.
- StatusDisplayManager.get_entity_state_list_for_status and
  get_entity_state_type_for_status removed (their last consumer
  was one-click; the icon path stopped using them earlier in #319).

Tests
- New test_one_click_control_service.py covers: default fallback
  for unrecognized EntityTypes; LIGHT override preferring
  LIGHT_ON_OFF; LIGHT_BRIGHTNESS fallback for fully-modeled color
  bulbs; speed-only fan via POWER_LEVEL default; cover with
  OPEN_CLOSE_POSITION via default; sensor-only and thermostat
  cases correctly NotSupported; toggle-value-limit guard.

* Fix collection card sensor polling updates and apply role ordering to collection cards

Audit of CollectionView rendering surfaced two issues that #319's
work touched indirectly:

Sensor values not updating from polling
- Collection card sensor renders did not wrap the value template in
  the per-EntityState CSS class, so the entity_state_status.js
  dispatcher had no DOM target for `.hi-entity-state-{id}` polling
  updates. Controllers worked because controller_data.html carries
  the class itself; sensor-only states (thermometer, humidity, etc.)
  silently never updated.
- entity_state_row.html now wraps the value include in a div with
  sensor_response.css_class, matching the modal's pattern in
  sensor_response_status_row.html.

State ordering inconsistency between modal and collection cards
- EntityStatusData.state_status_data_list promoted from a local in
  to_template_context() to a public property. The modal continues
  to consume it via the template context; collection card templates
  now access it directly on the dataclass instead of reading the
  unordered entity_state_status_data_list field. Single source of
  truth for "display order" across both surfaces.
- entity_card_list.html and entity_card_grid.html updated to pass
  entity_status_data.state_status_data_list into the partial.

* New urlib3 version with security patch

* Rename "wire value" to "EntityState value" in canonical-value docstrings

The term "wire value" is appropriate for integration-boundary code where
values genuinely travel over a wire (HA REST/WS payloads). Internally, once
an EntityState has stored its value, it is the canonical EntityState value,
not a wire-format string. Updating docstrings, parameter names, and local
variable names in EntityStateValue.to_display_label, EntityState.choices,
value_label tag, and associated tests to match.

* Promote delegations helper, dedup result, prefetch hot path, add review-cycle test

Review-cycle follow-ups for the EntityStateRole work:

- Rename ``StatusDisplayManager._all_entity_states_including_delegations``
  to ``all_entity_states_including_delegations`` (public). It is called
  from OneClickControlService and reaching across module boundaries to
  a private helper is an antipattern.

- Deduplicate the helper's return list to guard against entities that
  delegate one of their own states or that have two delegations
  resolving to the same EntityState.

- Add a one-shot ``prefetch_related_objects`` in
  ``get_entity_to_entity_state_status_data_list`` covering the entity
  states, delegations, sensors, and controllers walked downstream. On
  a LocationView with 20 entities of ~3 states each this collapses
  from ~160 queries per render to ~6. Drop the now-defeated
  ``select_related('entity_state')`` inside the helper so prefetched
  callers actually use the cache; the FK fetch cost for non-prefetched
  callers is bounded.

- Add comments documenting two intentional choices: EntityStateRole's
  label collisions between bare and domain-refined roles, and the
  intentional absence of sensor-only roles from
  ``DEFAULT_CONTROL_STATE_ROLE_ORDER``.

- Add ``execute_one_click_control`` end-to-end tests pinning the full
  pipeline (find_controller → get_current_state_value →
  determine_control_value → ControllerManager.do_control) for both
  the "toggle from known state" and "no sensor history" branches.
2026-05-12 18:04:44 -05:00
Tony C
9e3502312e Issue #316: Threshold-based EventClause for continuous-value alarms (#318)
* Add EventClause operator dispatch and BATTERY_LEVEL low-battery alarm

Implements phases 1-4 of #316:

Phase 1 — Schema
- New EventClauseOperator enum (EQ, LT, LTE, GT, GTE; default EQ).
- New EventClause.value_operator_str field (default 'eq'); property +
  setter sourced from EventClauseOperator.
- Migration 0008 with default 'eq' applies to every existing row;
  backward-compatible by construction.

Phase 2 — Matching
- EventManager._clause_matches() helper dispatches on the clause's
  operator. EQ is the historical string equality; LT / LTE / GT / GTE
  parse both sides as float() and silently no-op on parse failure so
  a transient malformed reading never raises into the matcher.
- Single-line swap at the call site in _create_event_if_detected().
- Boundary tests (LT vs LTE off-by-one, GT vs GTE, non-numeric
  defensive case).

Phase 3 — Factory plumbing
- EventManager.create_simple_alarm_event_definition gains optional
  value_operator parameter (defaults to EQ); written to the clause.
- HiModelHelper.create_battery_level_sensor mirrors the alarm-bearing
  factory pattern (smoke / moisture / CO / gas): with
  add_default_alarm=True it also wires the threshold alarm.
- HiModelHelper.create_battery_level_event_definition with
  EventClauseOperator.LT, MAINTENANCE event type, INFO/INFO levels,
  24-hour dedupe window, threshold default 20 percent.

Phase 4 — HA wiring
- HA converter's BATTERY_LEVEL branch now uses create_battery_level_sensor
  with add_default_alarm=add_alarm_events, so every imported
  `sensor + battery` device gains a low-battery alarm when the
  integration's add_alarm_events flag is on.

Deferred to follow-up: Phase 5 (UI/form work for user-editable
threshold clauses); Phase 6 (docs). Alert-grouping behavior
(multiple alarms with the same event-type label sharing one Alert
bucket) is by design for anti-fatigue; the deceptive Alert title
when grouped is a separate UI concern.

* Expose EventClause operator in the edit UI and document the pattern

Phases 5-6 of #316.

UI
- EventClauseForm gains a required value_operator_str ChoiceField
  drawn from EventClauseOperator.choices (defaults to EQ). Form
  clean() rejects a non-numeric value when operator is non-EQ so
  users get immediate feedback instead of a clause that silently
  never fires.
- Template adds an input-group for the operator dropdown between
  entity_state and value, matching the surrounding row pattern.
- JS gains Hi.setEventClauseValueOperatorWidget: on operator change
  to LT / LTE / GT / GTE, swaps the value element to
  <input type="number" step="any"> preserving the entered value.
  EQ leaves the existing widget alone so the entity_state-driven
  choice swap stays authoritative for discrete-state clauses.
- Admin's TabularInline picks up the new field automatically.
- Existing event-view tests updated to include
  event-clause-0-value_operator_str='eq' in posted form data.

Docs
- Augment the existing "Default alarm wiring" section in
  integration-guidelines.md with one paragraph on the
  EventClauseOperator pattern for continuous-value thresholds and
  a pointer to create_battery_level_event_definition. Light touch
  on purpose; the existing section already covers the discrete
  pattern and the event-definition modal is slated for a separate
  UI overhaul.

Closes #316.

* Address review feedback: form validation test, debug log, template typo

- Add focused test for EventClauseForm.clean(): non-numeric value
  with a numeric operator is rejected; numeric value is accepted.
  Closes the silent-failure path where a user could otherwise save
  a clause that never fires (matcher silently no-ops on non-numeric
  values under numeric operators).
- Log threshold clause skips at DEBUG inside _clause_matches's
  except branch so a misconfigured clause is diagnosable. Kept at
  DEBUG so transient 'unknown' / 'unavailable' wire values don't
  spam at higher levels.
- Fix pre-existing label for-attribute typo in event_clause_form.html
  (event_clause_form.entity.id_for_label → entity_state.id_for_label).
2026-05-11 22:45:43 -05:00
Tony C
6a2533f635 Issue #294: HA cleanup and alarm coverage expansion (#317)
* Cleanup #1: non-Insteon switch / outlet / motion sim entities

Switches, outlets, and motion sensors were the only device
categories lacking non-Insteon variants in the HA simulator —
adding them closes the gap left after #300 / #301.

New sim entities:
- HassMotionSensor (binary_sensor.x, device_class=motion)
- HassSwitch (switch.x, no device_class)
- HassOutlet (switch.x, device_class=outlet)

Each is a single ON_OFF SimState mirroring the binary-sensor
shape from #300; HA's switch / motion paths already handle them
in the converter without changes.

Zoo profile updates:
- Six Insteon entries renamed with "Insteon" in the name so the
  new non-Insteon entries can take the clean labels ("Zoo
  Switch", "Zoo Motion", "Zoo Outlet").
- Standard hass profile entries unchanged.

End-to-end verified: HI imports the new entities with
EntityType.ON_OFF_SWITCH / ELECTRICAL_OUTLET / MOTION_SENSOR.

* Cleanup #2: centralize HA wire-format strings in HassApi

Adds ~36 HassApi constants for the HA wire vocabulary that was
still inline in hass_converter.py and hass_service_composer.py:

- Climate attributes: current_temperature, current_humidity,
  target_temp_low, target_temp_high, temperature (setpoint),
  hvac_mode/hvac_modes/hvac_action, fan_mode/fan_modes,
  temperature_unit, and the heat_cool dual-setpoint mode value.
- Light attributes: brightness, brightness_pct, color_mode,
  color_temp_kelvin, hs_color, supported_color_modes.
- The 10 color_mode wire values (unknown, onoff, brightness,
  color_temp, hs, rgb, rgbw, rgbww, white, xy).
- Fan attributes: percentage, oscillating, direction,
  preset_mode/preset_modes, and the forward/reverse direction
  wire values.
- Cover attributes: current_position, position (service param).
- Media-player: volume_level (service param).

A typo in any of these now surfaces at name resolution rather
than silently mismatching at runtime; future HA API changes can
be addressed in one place.

Kept as literals: HI-internal format hints in parameter-shape
dicts, HI-internal substate suffix names that diverge from HA
attributes, and HI's flexible boolean / action input parsing.

* Cleanup #1 follow-up: combo motion sensor + battery/illuminance mapping

A real-world Z-Wave / Zigbee motion sensor exposes three entities
sharing a device: motion (binary_sensor), battery (sensor.x with
device_class=battery, numeric %), and illuminance (sensor.x with
device_class=illuminance, lux). HA's converter previously mapped
only the binary sensor; the numeric battery and illuminance
sensors fell through to BLOB.

Adds:
- EntityStateType.BATTERY_LEVEL with label "Battery".
- HassApi constants for illuminance device_class and id suffix.
- Converter mappings: sensor.x + battery → BATTERY_LEVEL,
  sensor.x + illuminance → LIGHT_LEVEL (which already existed
  in HI but had no integration mapping).
- Units captured verbatim from the wire ('%' for battery,
  'lx' for illuminance, with sensible defaults).
- HassComboMotionSensor sim entity with three states sharing a
  short_name so the converter collapses them into one HI Entity.
- Zoo Smart Motion seed entry.

DisplayValue.__str__ inserts a space between magnitude and unit
symbol when the unit is alphabetic (120 lx, 85 kg) per NIST/SI
convention; symbol-style units (°F, %) attach directly.

* Cleanup #4 Stage A: HA-docs validation findings (Option A wins)

Audited HI's HA binary_sensor and sensor coverage against the
official device-class enumerations from HA's developer docs.
Found 18 binary_sensor + 47 sensor device classes unmapped;
implemented the highest-value subset (Option A from the stage
discussion). Remaining gaps documented as meta-issue #315.

Binary sensor additions (mapping table + value translation):
- occupancy / presence → MOTION-like ACTIVE/IDLE (occupancy →
  EntityType.MOTION_SENSOR, presence → EntityType.PRESENCE_SENSOR).
- opening → OPEN_CLOSE (added to OPEN_CLOSE_DEVICE_CLASS_SET).
- moisture → ON_OFF (interim — water-leak alarms warrant the
  full SMOKE-style treatment in a separate change).

Sensor additions (mapping table + EntityState creation with
units captured from the wire):
- power → ELECTRIC_USAGE
- pressure → AIR_PRESSURE
- wind_speed → WIND_SPEED

These all reuse pre-existing HI EntityStateTypes — one-liner
mapping additions plus a generic numeric-sensor creation
branch in hass_converter that captures unit_of_measurement.

New HassApi.MOTION_LIKE_DEVICE_CLASS_SET groups motion /
occupancy / presence so _binary_sensor_value can branch off a
single set rather than three parallel comparisons.

New EntityStateType.PRESENCE creation branch — the type
existed and StatusDisplayData handled it, but no converter
path created entities with it before now.

unknown / unavailable wire-value handling: HA emits these
special states when an entity is offline or hasn't reported.
hass_state_to_sensor_value_map now short-circuits on them so
sensor_history doesn't accrue placeholder records that would
later surface as "Unavailable" text in the polling refresh.
HassStateValue gains UNKNOWN, UNAVAILABLE, and a
NO_VALUE_STATES frozenset.

End-to-end verified: 7 new device classes import with correct
EntityType, state-type, and units; unknown/unavailable
produce empty value maps.

Meta-issue #315 inventories all remaining unmapped device
classes with prioritization signals and per-pattern effort
estimates.

* Cleanup #4 Stage B: climate preset, color-temp Kelvin bounds

Climate preset_mode substate. HA documents preset_modes (Eco /
Away / Home / Sleep / Boost / Comfort / Activity) as a separate
controllable axis from hvac_mode; HI's climate path previously
ignored it. Now exposed as a DISCRETE controllable substate
when the thermostat declares preset_modes, with set_preset_mode
service dispatch on the HA side and a matching dispatcher branch
on the simulator side.

Light color-temperature device bounds. The COLOR_TEMPERATURE
slider was hardcoded to 2000-6500K; real bulbs have narrower
device-specific ranges (e.g., 2700-5000K for many warm-white
LEDs). hass_converter now reads min_color_temp_kelvin /
max_color_temp_kelvin from the live state with the broad
fallback when not declared.

HI substate suffix rename: preset → preset_mode. Eliminates the
one substate suffix in the codebase that diverged from its HA
attribute name (HA's attribute is preset_mode, fan's HI suffix
was 'preset'). The five fan-test occurrences and the five
production occurrences updated; HassApi.PRESET_MODE_ATTR now
serves both as the HA attribute name and the HI substate suffix
identifier. Existing imported HA fan entities with preset
substates need re-import — the integration_key suffix changed
from ~preset to ~preset_mode.

Simulator additions:
- HassThermostatFields.preset_modes default
  ['eco', 'away', 'home', 'sleep'].
- HassThermostatPresetState mirrors HassThermostatFanModeState
  with sim_state_id='preset_mode' so the dispatcher tuple
  matches.
- Thermostat SimEntityDefinition registers the new state.
- service_dispatchers._thermostat adds a set_preset_mode branch
  so HI's outbound preset change reaches the simulator.

Deferred items from Stage B added as a comment on meta-issue
#315: target_humidity, swing modes, light effect/effect_list,
brightness=1 rounding edge case, hs_color range verification.

* Complete #294 Stage C HA validation: leak sensor support and unit display fixes

- Add full leak-detector support (LEAK_SENSOR EntityType, MOISTURE
  EntityStateValues, SVG icon, alarm wiring, status decay)
- Fix wind speed not converting under imperial display: compound-unit
  keys in IMPERIAL_TO_METRIC_UNITS were Quantities, not Units, and
  silently failed dict lookup; route through ureg.parse_units
- Add console DISPLAY_UNIT_OVERRIDES so ELECTRIC_USAGE always displays
  in watts regardless of stored unit (avoids surprise W -> hp)
- Make color temperature slider honor EntityState value_range_dict
  (template was hardcoding 2000-6500 instead of reading device bounds)
- Expand HA simulator hass-zoo profile with non-Insteon switch/outlet/
  motion variants, combo motion sensor, presence/opening/power-meter/
  weather-station/occupancy-light/water-leak entities
- Add display_unit properties on sim numeric states so simulator
  sliders label their units
- Rename HI 'preset' substate suffix to 'preset_mode' for HA consistency
- Centralize remaining HA wire-format strings in HassApi

* Promote default-alarm wiring to HiModelHelper and document integration conventions

Phase 1 of #294 item #5 promotion analysis: extract HA-side patterns
that are home-automation-general up to the framework layer.

- Add 'add_default_alarm: bool = False' to HiModelHelper's
  create_connectivity_sensor, create_open_close_sensor,
  create_movement_sensor, create_smoke_sensor, create_moisture_sensor.
  When True, the factory also creates the canonical default alarm
  event definition using the sensor's integration_key.
- Collapse five HA converter blocks from sensor-call +
  conditional-alarm-call to a single sensor call.
- Add "Code Conventions" section to integration-guidelines.md
  covering file layout / <prefix>_ naming, wire-constant
  centralization (HassApi as exemplar), and the default-alarm flag
  with guidance on the shared-key vs. separate-key cases.

ZoneMinder's movement alarm is left untouched: it uses a separate
integration_key (MOVEMENT_EVENT_PREFIX) from the sensor, so the
explicit two-call pattern remains correct there.

* Add PRESENCE, carbon monoxide, and combustible-gas alarm support

Fills three alarm-coverage gaps surfaced by the audit in #294 item #5:

- PRESENCE alarm: parallel to MOVEMENT (SECURITY, CRITICAL/INFO).
  Adds create_presence_sensor + create_presence_event_definition to
  HiModelHelper; HA converter's PRESENCE branch now uses the factory
  with add_default_alarm.
- Carbon monoxide (binary_sensor + device_class=carbon_monoxide):
  full SMOKE-pattern replication. New EntityStateValue.CO_DETECTED/
  CO_CLEAR, EntityStateType.CO, HiModelHelper factories, status
  decay, SvgStatusStyle entries, CSS rules, HA converter mapping +
  value translation + creation branch + entity-type inference. CO
  is life-safety: both security levels map to CRITICAL.
- Combustible-gas (binary_sensor + device_class=gas): same shape as
  CO. New EntityType.GAS_DETECTOR with new SVG icon. Life-safety
  semantics.

Simulator side: SimEntityType.CARBON_MONOXIDE_DETECTOR and
GAS_DETECTOR (alphabetical position), sim entity classes, zoo
profile entries (Zoo CO Detector, Zoo Gas Detector).

Deferred to a separate effort (will track in #315): BATTERY_LEVEL
threshold alarm — requires extending EventDefinition to support
"value below threshold" triggers, which is a substantive change to
the event subsystem better done in isolation.
2026-05-11 21:04:30 -05:00
Tony C
900f85394c HA thermostat support + temperature unit handling (#299 / #308) (#309)
* Issue #299 Phases 1+2: HA climate substate decomposition and thermostat simulator entity

Climate is inherently multi-axis, so apply the substate-decomposition pattern from #295/#298. A climate entity that declares hvac_modes always decomposes into substates: current_temperature (sensor), hvac_mode (controllable from declared modes), hvac_action (sensor with HA's standard action choices), plus a setpoint substate set chosen from the supported modes — single target_temperature for any single-mode operation, low+high pair for heat_cool. Climate entities lacking hvac_modes fall through to the existing single-state TEMPERATURE mapping for backward compatibility.

Per-domain composer methods _climate_substate_specs and _climate_substate_value join the existing _light/_fan ones. _climate_to_sensor_value_map handles the multi-axis decomposition; the dispatcher routes climate to it. A small _numeric_attr_as_str helper centralizes the recurring float-coercion-with-graceful-fallback pattern used across temperature attributes.

Simulator side adds HassThermostatFields with operator-configurable hvac_modes list and temperature_unit, paired with six SimState classes (current_temperature, target_temperature, target_temp_low, target_temp_high, hvac_mode, hvac_action). The new HassApiComposer._thermostat collapses them into one HA climate.x entity, picking the setpoint shape (single temperature vs target_temp_low+high) based on the active hvac_mode — mirroring real HA's per-mode shape change. The HassServiceDispatcher._thermostat handler routes set_temperature (single or low/high) and set_hvac_mode to the matching SimStates. The hass-zoo profile gains two thermostats: Zoo Thermostat (full-feature heat/cool/heat_cool/off, °F) and Zoo Heater (heat-only, °C) — exercising both single-mode and multi-mode setpoint shapes plus both temperature units.

* Issue #299 Phases 3+4: outbound dispatch for climate substates plus fan_mode and current_humidity axes

Phase 3 — outbound dispatch

_substate_service_call extended for climate suffixes: target_temperature routes to set_temperature with single-value payload; the target_temp_low / target_temp_high pair routes to set_temperature with the dual-value payload, using a companion-substate cache lookup that mirrors the hue/saturation pattern (so adjusting one bound carries the other along, with a safe ordering fallback when the partner value isn't yet cached); hvac_mode routes to set_hvac_mode. New HassServiceComposer methods for_temperature_range and for_hvac_mode encapsulate the wire shapes. New controller_temperature.html slider widget reads min/max from the EntityState's value_range_dict (set per-temperature_unit at Phase 1 substate creation time) so the slider bounds adapt to °F vs °C; unit-suffix display is a placeholder pending #308's full unit-handling work.

Phase 4 — stretch axes

Climate substate spec composer extended for fan_mode (DISCRETE controllable, choices from the per-thermostat fan_modes list when reported) and current_humidity (HUMIDITY sensor-only when reported). _climate_substate_value reads fan_mode and current_humidity attributes. New HassServiceComposer.for_fan_mode and HassApi.SET_FAN_MODE_SERVICE constant. The simulator's HassThermostatFields gains a fan_modes field (default fan-aware; Zoo Heater overrides to []) and two new SimStates: HassThermostatFanModeState (DISCRETE per-instance choices) and HassThermostatCurrentHumidityState (CONTINUOUS sensor-driven). The thermostat composer emits fan_mode / fan_modes / current_humidity attributes when the underlying SimStates supply them; the service dispatcher routes set_fan_mode to the matching SimState.

* Issue #299 Phase 5: tests for HA climate converter and new HassServiceComposer methods

- New test_hass_converter_climate.py: substate spec composition (no hvac_modes -> no decomposition; full vs heat-only vs heat_cool-only mode sets; per-unit value_range; choices from hvac_modes / fan_modes; fan_mode omitted when fan_modes absent); inbound translation for full-feature and single-mode thermostats including missing-attribute graceful drop and non-numeric temperature graceful empty; end-to-end import for both flavors verifying right EntityState count and per-substate types; outbound dispatch for target_temperature, hvac_mode, fan_mode, and the target_temp_low/high pair (companion-substate cache lookup with cached and uncached partner).
- TestClimateAxisComposers in test_hass_service_composer: for_temperature_range / for_hvac_mode / for_fan_mode happy paths plus the ValueError-raising validation branches (low > high; empty mode strings).

* Fix UPDATE button hidden on config settings page

The action bar in edit_content_body.html gated UPDATE-button visibility
(and its dirty/status message stack) on attr_item_context.can_add_custom_attributes.
That flag did double duty for "page is editable" and "new attributes can
be added" — and attr_item_context isn't part of the top-level template
context in multi-edit (config settings) mode, so the gate silently
evaluated false and the UPDATE button disappeared, blocking edits to
user/system settings.

Split the conflated concept and move both flags up to the page-level
context so they work for single- and multi-edit surfaces alike:

- allow_edits (new): drives UPDATE button + dirty/status visibility
- can_add_custom_attributes: drives Add File / Add Info visibility only

Entity overrides allow_edits to track its existing externally-managed
flag (HomeBox-imported entities remain fully read-only). Subsystem
contexts override can_add_custom_attributes to False (config exposes
only system-defined attributes) while inheriting allow_edits=True.

Also tightened the per-attribute Restore-from-history gate to require
both page-level allow_edits AND the attribute's own is_editable, so
predefined non-editable attributes correctly hide Restore even on an
otherwise editable page.

* Issue #308: HA temperature unit handling end-to-end

Per-EntityState unit storage with conversion at all boundaries
(HA wire ↔ HI canonical, HI canonical ↔ user display unit, UI
input ↔ stored unit). HI's canonical temperature unit is declared
once in the climate substate spec; downstream code consults
EntityState.units / payload metadata so the canonical choice is
data-driven, not code-distributed.

Cross-cutting infrastructure:

- IntegrationMetadataCache: process-wide, lazy-warmed cache of
  EntityState metadata (sync/async parallel APIs) so polling-loop
  unit lookups don't multiply DB queries inside the HA monitor's
  tight loop. Bulk warmup on first read; lazy fill for entities
  added later. Keyed by IntegrationKey; entry shape is a small
  dict so future metadata can be added without API churn.

- IntegrationConverterHelper / ConsoleConverterHelper: symmetric
  to_/from_entity_state_value pairs for the integration and UI
  boundaries respectively. ConsoleConverterHelper returns a
  DisplayValue dataclass so magnitude-only consumers (slider
  numeric value) and combined-text consumers (status text, modal)
  share one helper output; templates use a new to_display filter
  that replaces the verbose as_quantity | format_magnitude chain.

- ControlViewMixin.to_entity_state_value delegates to the helper.

- StatusDisplayData.latest_display_value routes async UI poll
  refreshes through the same helper, fixing anomalies where
  canonical magnitudes leaked through unconverted (post-poll
  display reverting to raw °C when initial render correctly
  showed °F).

HA simulator additions for end-to-end verification:

- SimulatorRuntimeSettings: process-wide in-memory temperature
  unit override (Default / °F / °C) selectable from a header
  dropdown with antinode swap-in-place to preserve the active
  integration tab. Composer and dispatcher convert at the wire
  boundary; SimStates store in profile unit so the override is
  purely a wire-format toggle.

- UnitTranslationHelper (HA-specific): SimTemperatureUnit → HA
  wire format mapping plus F↔C conversion at composer/dispatcher
  boundaries.

- Thermostat seeds: friendly_name set on the emitted attributes
  so HI shows pretty entity names. Temperature SimState defaults
  and slider bounds are now unit-aware so °C-native profiles
  start at sensible Celsius values rather than 70°C-as-room-temp.
  Per-control unit suffix on the simulator's continuous slider;
  slider value displays use floatformat to avoid Pint float reprs
  leaking many decimals into the UI.

UI:

- continuous_slider_with_units.html (new): unit-aware slider
  routing through the helper. Slider step aligned with 1-decimal
  display precision so HTML range step coercion doesn't mismatch
  the template-rendered value on async polling refreshes.

- sensor_response_value_temperature.html (new): per-EntityStateType
  render for TEMPERATURE.

- controller_temperature.html updated to use the unit-aware slider.

Phase 1's hi_temperature_unit field is removed from
integration_payload — the cache provides EntityState.units to
outbound dispatch directly, eliminating the redundancy.

Trace-output column widths adjusted to better fit the longer
substate-suffixed integration_name strings.

Tests: 2788 passing.

* Skip controller poll updates for actively-dragged sliders

Polling-driven controller value updates could replace a slider's
value mid-drag, yanking the thumb out from under the operator's
fingers. Track active drag state via a WeakSet (mousedown /
touchstart add; mouseup / touchend / change / blur remove); the
polling-update path skips range inputs in that set. The slider's
``change`` event clears the flag on release and antinode's
onchange-async handler submits the new value, so values still
reconcile correctly — only the in-flight redraw is suppressed.

* Tests for unit translation infrastructure (#299 / #308)

High-value coverage for the unit-translation classes added with
the thermostat / temperature unit work:

- IntegrationMetadataCache: warmup loads units from Sensors,
  dedupes Sensor/Controller pairs sharing a key, lazy fill for
  entities created post-warmup, miss caching to avoid re-querying
  the DB, divergent Sensor/Controller pair resolution (Sensor
  wins), async variant returns same as sync, warm-cache async
  short-circuits the sync_to_async hop.

- IntegrationConverterHelper.to_/from_entity_state_value: F→C and
  C→F conversion via cache lookup, passthrough when units match
  or are absent, round-trip preservation, sync/async variant
  equivalence.

- ConsoleConverterHelper / DisplayValue: combined-text
  formatting via __str__, inbound conversion at the HTML/JS
  boundary, outbound rendering as DisplayValue, passthrough on
  no-units / non-numeric / None inputs, full round-trip via the
  to_/from pair.

- UnitTranslationHelper (HA simulator boundary): emitted unit
  selection respects the runtime override, F↔C conversion at
  notable points (freezing, boiling, room temp), defensive
  passthrough on missing units / non-numeric / None, round-trip
  precision.

Tests follow the codebase convention of disabling logging at
module load (assertions are behavior-based, not log-based).
Async paths use AsyncTaskTestCase so the event-loop lifecycle is
managed and DB-locking deadlocks are avoided.

* Document unit-conversion conventions and helper classes

Frontend guidelines gain a "Unit-Bearing Values: Server ↔ UI
Translation" section covering the canonical-storage convention,
the ConsoleConverterHelper to_/from pair, the DisplayValue
return shape, the to_display template filter, and the contract
that initial render and polling refresh share the same helper.

Integration guidelines gain a "Unit conversion at the integration
boundary" section covering IntegrationMetadataCache and
IntegrationConverterHelper.to_/from_entity_state_value, with a
pointer to the HA converter as a worked example. Cross-links the
parallel UI-boundary section in the frontend guidelines.

* Review feedback: hoist canonical, Singleton cache, JS drag-protect

Address review findings on the unit-translation infrastructure
before PR:

- Hoist ``CANONICAL_TEMPERATURE_UNIT`` out of ``HassConverter`` to
  ``hi/units.py`` so other integrations adopting the canonical-at-
  boundary pattern import the choice cross-cuttingly rather than
  duplicating or reaching into HA-specific code. The climate-
  specific slider bounds stay on ``HassConverter`` but are renamed
  ``_SETPOINT_MIN_CANONICAL`` / ``_SETPOINT_MAX_CANONICAL`` to make
  their narrower scope clear.

- Convert ``IntegrationMetadataCache`` to use the project's
  ``Singleton`` base class for consistency with
  ``SimulatorRuntimeSettings`` and the rest of the codebase. Cache
  state moves to instance attrs; callers updated to
  ``IntegrationMetadataCache().method(...)`` form.

- JS slider drag-protect: add Pointer Events (``pointerdown`` /
  ``pointerup`` / ``pointercancel``) for modern stylus / touch
  interactions, and move release-side handlers from ``body`` to
  ``document`` so off-element / off-window releases still clear
  the active-drag flag.

- Rename the ``to_display`` template filter to ``as_display_value``
  for clearer return-type signaling (filter outputs a
  ``DisplayValue``).
2026-05-10 12:49:12 -05:00
Tony C
a74b6a8b41 Issue #295: HA smart bulb support (brightness + color) (#303)
* Add HASS smart bulb sim devices and continuous-slider infrastructure

Phase 1 of #295. Three coupled additions to the HASS simulator that
together let HI's existing dimmer infrastructure exercise on a non-
Insteon shape and expose the gaps in HI's color handling.

Continuous-state UI infrastructure: new
``sim_control_continuous.html`` widget renders an HTML range slider
keyed off a SimState's ``min_value``/``max_value``/``value``.
Reusable for upcoming thermostat/fan/cover work.

Light variants:
  * ``HassSmartBulbState`` — brightness-only smart bulb. Single
    CONTINUOUS SimState in HA's 0-255 range; emits one HA
    ``light.x`` entity with ``brightness`` and
    ``color_mode='brightness'``.
  * ``HassColorSmartBulbBrightnessState`` plus three sibling
    SimStates for hue/saturation/color-temperature. Operator gets
    a slider per state in the simulator UI (HI-centric model);
    the four states collapse into ONE HA ``light.x`` entity at
    emit time via ``api_composers``.
  * ``HassInsteonDimmerLightLightState`` switched from ON_OFF to
    CONTINUOUS so it actually emits ``brightness``. Without this
    HI's ``_has_brightness_capability`` check failed and the
    Insteon dimmer imported as ON_OFF rather than LIGHT_DIMMER —
    making the existing ``controller_light_dimmer.html`` slider
    unreachable for HASS-imported dimmers.

API composition: new ``api_composers.py`` introduces
``HassApiComposer`` with a per-device-type registry keyed off
SimEntityFields class. Default composer keeps the existing one-
state-per-HA-entity behavior so the motion detector / switches /
sensors are unaffected. Color smart bulb's composer collapses
its four SimStates into one HA entity, combining hue+saturation
into the standard ``hs_color: [h, s]`` two-element list and
emitting ``color_mode='hs'``. ``AllStatesView`` now iterates
per-entity and dispatches through the composer.

Seed: ``hass-zoo`` profile adds one Smart Bulb and one Color
Smart Bulb so the two new shapes are immediately importable.

* Add controller-value polling path so widgets reflect external state

The existing polling mechanism set the bucketed status string as a
DOM attribute and let CSS drive icon styling — works for the
LocationView's visual representation, breaks for interactive
widgets in the entity-status modal because a slider's thumb
position, a checkbox's checked, and a select's selected option
aren't driven by attribute updates. Symptom most visibly: the
dimmer slider in the modal didn't follow external (sim-side)
brightness changes, and the on/off toggle relied on a
status-attribute-to-checked side-effect inlined in the polling JS.

Introduce a parallel ``cssControllerValueMap`` in the polling
response, keyed off the same EntityState class names as the
existing status map but carrying widget-shaped values rather than
bucketed status strings. ``StatusDisplayData.controller_data_value``
produces the value (default: raw latest sensor value; per-state-
type reshape is the extension point). On the client side a new
``Hi.controllers.applyValueMap`` dispatches to the appropriate
DOM property by element shape — slider ``.value``, checkbox
``.checked``, select ``.value`` — only updating when the value
differs to avoid mid-drag interference. The legacy checkbox /
select special-cases inside ``handleCssClassUpdates`` are removed
now that the controller-value path covers those widgets explicitly.

Three peripheral fixes the foundation revealed:

  * ``HassConverter._has_brightness_capability`` now recognizes a
    dimmer-capable light from its ``supported_color_modes``
    declaration in addition to a currently-present ``brightness``
    attribute. HA omits ``brightness`` when a light is off, which
    used to flip a known dimmer to the on/off conversion path
    each off→on cycle and surface ``"off"`` as the slider value
    (browser clamping to mid-range and the percent label
    displaying ``"off%"``).

  * Simulator's ``ServiceCallView`` previously hard-coded a
    ``(domain, service) → 'on'/'off'`` literal mapping that
    polluted CONTINUOUS state values (a brightness state would
    end up holding the literal string ``'on'``). Replaced with a
    per-device-type ``HassServiceDispatcher`` that translates HA
    payloads into the right SimState updates — turn_on with
    brightness/brightness_pct routes to the brightness state as
    a numeric, turn_on with hs_color routes to the hue and
    saturation states, etc. The default handler covers single-
    state devices; color smart bulb registers its own.

  * Brightness slider's percent label (``.brightness-value``)
    didn't track the slider value across drag, button click, or
    polling-driven updates. ``controllers.js`` now syncs the
    label from all three paths.

The new module ``controllers.js`` is added to the
``js_before_content`` pipeline so it loads on every page that
might render the dimmer (LocationView and the entity-status modal
both included).

* Issue #295: Color attribute support and HassController/HassConverter refactor

- One HA state -> N HI EntityStates (brightness + hue/saturation/color_temp)
  on inbound; many HI values -> one HA service call on outbound
- Move HI->HA conversion logic from HassController into HassConverter;
  controller becomes a thin orchestrator over hi_value_to_hass_service_call
- Add IntegrationConverterMixin for cross-integration sibling-state lookup;
  SensorResponseManager gains a batch-shaped get_latest_sensor_response_map
- Add hue, saturation, and color temperature controller templates with
  reference gradient tracks (rainbow / gray-to-saturated / warm-to-cool)
- Generalize continuous-slider display sync via shared DIVID/Hi
  data-attribute constants
- Align vocabulary: 'HA state' (bundle) vs 'HA substate' (each HI
  EntityState); hass_entity_id at the HA boundary; hi_control_value at
  the integration boundary

* Issue #295: Code organization cleanup — extract HassServiceComposer, generalize slider widget

- Split HassConverter outbound dispatch: pure HA-side composition
  moves to a new HassServiceComposer; HassConverter retains the
  HI<->HA bridge with explicit to_ha_* boundary helpers
  (to_ha_numeric_parameter_value, to_ha_on_off_intent)
- Generalize substate-related names from color_sub_state -> substate
  throughout, so the framework reads as domain-agnostic for future
  one-to-many integrations beyond color lights
- Unify continuous-slider templates behind one parameterized partial
  (continuous_slider.html); dimmer/hue/saturation/color_temperature
  collapse to single-line includes; brightness preset buttons
  generalized to opt-in min/max preset mechanism
- Hoist remaining JS/template-coordinated strings into DIVID/Hi
  constants (CONTROLLER_SLIDER_*, CONTROLLER_PRESET_BTN_CLASS,
  DATA_VALUE_ATTR); controllers.js has no brightness-specific code
- Re-sync loop now refreshes substate controller payloads and
  creates any newly-implied substate controllers on existing
  parents, symmetric with the existing shrink behavior
- Replace top docstring of HassConverter with concise terminology
  (domain, device_class, entity, state, substate, device) and the
  three jobs the class performs
- Vocabulary alignment: control_value -> hi_control_value at the
  IntegrationController boundary

* Issue #295: Tests and docs for substate composition

- Add test_hass_service_composer covering all 10 composer entry
  points (for_brightness, for_temperature, for_volume, for_position,
  for_numeric_parameter, for_numeric_best_effort, for_on_off_best_effort,
  for_payload_intent, for_color_temp, for_hs_color)
- Add test_hass_converter_substate covering inbound substate
  decomposition for color lights, outbound substate dispatch
  (color_temp, hue/saturation with cached and missing partners),
  and the to_ha_* boundary helpers
- Document one-to-many state composition in
  integration-guidelines.md (framework capability via
  IntegrationConverterMixin and the integration_key suffix
  convention)
- Document HA state vs HA substate terminology and the
  HassConverter vs HassServiceComposer split in the HA
  integration's developer doc

* Issue #295: Replace magic strings in HASS service-call composition

- Replace raw domain/service literals throughout HassServiceComposer
  with HassApi constants (LIGHT_DOMAIN / TURN_ON_SERVICE etc.) so
  service-name typos surface as import errors rather than silent
  routing bugs
- Introduce ControlIntent class for the four canonical operator
  intents (ON / OFF / OPEN / CLOSE) flowing across the HI->HA
  bridge; HassConverter.to_ha_on_off_intent now returns the named
  constants and HassServiceComposer dispatches on them
- Align to_ha_on_off_intent with hass_entity_id_to_state_value_str
  by using EntityStateValue.ON/OFF/OPEN/CLOSED for the recognized
  HI input forms (alongside the existing 'true'/'false'/'1'/'0'
  shorthand)
- Skip redundant DB write in re-sync substate-payload refresh when
  the payload is unchanged
- Drop the _is_numeric_value predicate; _best_effort_service_call
  now does try/except around to_ha_numeric_parameter_value, so the
  parse work isn't duplicated. Rename _is_numeric_control to
  _payload_supports_numeric_control to reflect that it only checks
  payload flags now
- Extract _substate_integration_key_for_suffix so the partner-key
  construction in _substate_service_call goes through the same
  IntegrationKey builder as the rest of the substate path

* Issue #295: Review-driven cleanup — accuracy, efficiency, idempotency tests

- Push payload-equality guard into update_integration_payload so it
  skips the DB save when nothing changed; both parent and substate
  re-sync paths benefit and the substate site no longer needs its
  own guard
- Rename IntegrationConverterMixin to IntegrationConverterHelper
  (namespace-style classmethod helper, not a base for inheritance);
  internally caches a SensorResponseMixin-using instance so manager
  access stays on the project's coordination path. Brief docstring
  notes the proper Singleton-converter fix is deferred
- Update _extract_substate_value docstring to accurately describe
  behavior: HI relays HA-reported color attribute values; we don't
  filter by color_mode (a value HA reports IS the bulb's last-known
  state regardless of which mode is currently authoritative)
- Switch continuous_slider.html from default to default_if_none so
  a numeric 0 sensor reading isn't silently rewritten to the default
- Add idempotency tests for substate controller creation: initial
  import produces three controllers; re-sync doesn't duplicate;
  re-sync creates newly-implied substate controllers when
  supported_color_modes grows on an already-imported parent
- Add direct tests for SensorResponseManager.get_latest_sensor_response_map
  using fakeredis: empty-list short-circuit, uncached → None,
  cached returns latest, mixed batch preserves order
- Update inlined mock impl in test_models to match the new
  update_integration_payload contract

* Issue #295: Add COLOR_MODE substate, fix awaiting-reading display

- New EntityStateType.COLOR_MODE (sensor-only, discrete) tracks
  HA's color_mode attribute on multi-mode lights. Adds 10
  COLOR_MODE_* EntityStateValues covering HA's mode set, with
  COLOR_MODE_UNKNOWN catching null and 'unknown' from HA
- Consolidate _HASS_SUBSTATE_SUFFIXES and _CONTROLLER_STATE_VALUE_RANGES
  into a single _SUBSTATE_SPECS registry keyed by EntityStateType,
  carrying suffix, is_controllable, and optional value_range
- _create_substate_controllers becomes _create_substate_models;
  branches on is_controllable to create a Sensor or Controller
  per spec. Re-sync passes both existing-controllers and
  existing-sensors maps for symmetric idempotency
- COLOR_MODE substate is created only for lights with more than
  one supported_color_mode — a single-mode bulb has a constant
  value and the substate adds no information
- _extract_substate_value translates HA's color_mode strings to
  the corresponding HI EntityStateValue; missing-key still skips
  (last-known retained), null/'unknown' both map to UNKNOWN
- Display layer: add has_sensor / has_controller flags to
  EntityStateStatusData (in-memory only, no polling-JSON impact);
  templates differentiate "awaiting sensor reading" (sensor
  defined, no cache yet) from "no sensors or controllers defined"
  (model-level setup error). Both messages now include the state
  name for context. Fixes a misleading alert that surfaced for
  any sensor-only EntityState pre-population
- New tests cover COLOR_MODE value translation across the HA
  mode set, missing-key skip, sensor-only creation, and
  re-sync idempotency for the COLOR_MODE Sensor

* Issue #295: Simulator color_mode SimState for color smart bulbs

- Add HassColorSmartBulbColorModeState — DISCRETE SimState with
  HA's color-mode value set as choices. Operator-controllable
  via the simulator UI dropdown so testers can reach edge values
  (unknown, rgbww, etc.) directly without having to drive every
  mode through HI's controllers
- Composer reads the SimState's value into the entity-level
  color_mode attribute, replacing the previous hard-coded 'hs'
- Service dispatcher emits a color_mode update alongside the
  axis updates: hs_color writes set color_mode='hs', and
  color_temp_kelvin writes set color_mode='color_temp'. Mirrors
  HA's "active mode follows the most-recently-written attribute"
  behavior on the standard inbound flow

* Issue #295: Promote brightness to peer substate for color bulbs

For multi-substate lights (anything with chromatic or color_temp
modes), brightness becomes a peer substate at the ~brightness
integration_key alongside hue/saturation/color_temp/color_mode,
rather than the bare-key "primary" with the others as siblings.
Removes the asymmetric "one is special" model and makes UI labels
("{name} Brightness", "{name} Hue", etc.) symmetric across all
controls. Single-state lights (brightness-only, on/off-only)
keep the bare-key model unchanged.

- LIGHT_DIMMER joins _SUBSTATE_SPECS with suffix='brightness'
- _substate_types_for_hass_state includes LIGHT_DIMMER when any
  color substate is present
- _create_hass_sensors_and_controllers skips parent EntityState
  creation for multi-substate lights; all states routed through
  _create_substate_models with suffixed keys
- _dimmer_light_to_sensor_value_map skips bare-key brightness
  emit when LIGHT_DIMMER is in the substate set
- _extract_substate_value handles LIGHT_DIMMER (delegates to
  _dimmer_brightness_value)
- _substate_service_call handles 'brightness' substate via
  HassServiceComposer.for_numeric_best_effort with the parent
  entity_id as the HA call target
- Re-sync existence check broadened: matches existing substates
  at suffixed keys, not just the bare key, so multi-substate
  lights don't fall through to "missing — adding" on every sync
- Tests updated for the new key shape; idempotency tests now
  expect 4 substate controllers (brightness + hue + saturation +
  color_temp)
2026-05-08 22:04:14 -05:00
Tony C
8314eced6e Issue #280: Per-integration documentation structure and content (#291)
Establishes a two-track per-integration doc structure (user-facing
under docs/integrations/, developer-facing under docs/dev/integrations/)
with templates that enforce a consistent set of sections for each
integration. Populates the new structure for the three existing
user-configured integrations (Home Assistant, ZoneMinder, HomeBox)
and updates the integration-addition guideline to require both docs
for any new integration.

User-facing changes:

- New per-integration pages with overview, prerequisites, credential
  acquisition, configuration values, setup walkthrough, troubleshooting,
  and known limitations. ZoneMinder's existing CORS and HTTPS/SSL
  troubleshooting content migrated out of the shared Integrations.md
  into its own page.
- Restructured docs/Integrations.md as a short landing page that
  centralizes the conditional UI flow for enabling an integration
  (different button label and location depending on whether any are
  configured yet) so per-integration pages can link to it instead of
  restating it.
- Conventions documented in the user-facing template: introduce
  Home Information (HI) on first mention; use 'item' for HI's
  representation, reserve 'entity' for upstream services that use
  that term; use 'Import' (first run) and 'Refresh' (subsequent),
  not 'sync', in user-facing copy.

Developer-facing changes:

- New per-integration dev docs for ZoneMinder and HomeBox; HA dev
  doc expanded from a single section to all six required sections.
- Dev template explicitly directs writers to keep content
  high-level and refer to the code for details; existing dev docs
  follow that principle (key modules listed with one-line roles,
  no method signatures or field lists duplicated).
- Weather integration doc gets a one-paragraph preface marking it
  as an internal-source exception that does not follow the
  per-integration template structure.

Simulator documentation:

- docs/dev/testing/test-simulator.md expanded to cover the
  simulator's purpose and architecture, the URL mappings for
  pointing each HI integration at the simulator, and the
  seed_sim_profiles command (with a profile summary table and a
  pointer to the command's docstring for the full operator
  workflow).
2026-05-05 23:02:07 -05:00
Tony C
8151961fec New background SVG editor (#276)
* Added  new background SVG editor
* Added palette drag-and-drop for new SVG editor.
* Added new SVG edit commands: delete, icon mirror, full path move
* Added SVG editor help modal and button.
* Added "revert" to new SVG editor.
* Added EXPORT feature for SVG background editor.
* Updated the snapshot generation tool for profile creation.
* Updated and added docs around background SVGs and profiles.
* Changed SVG editor save to not overwrite existing one in MEDIA_ROOT
* Added snap-to-grid and Undo features to SVG editor.
* Generated all new profiles with new editable background SVG images.
* Refactored entity editing using new, common, core javascript modules.
* Added snap feature for entity editing. Updated edit help modal.
* Updated copyrights.
* Added fixes and enhancements to antinode.js from other project.
* Improved HomeBox integration enable error handling/messaging.
* Added ability to dismiss first time user special content displays.
* Updated screenshots and markdown files for new SVG background editor.
* Removed docs/graphics that should not have been in this repo.
2026-04-20 10:05:04 -05:00
Anthony Cassandra
9374dc0418 Clarify that Redis is bundled in Docker deployments
Developer docs mentioned Redis as a requirement without distinguishing
between local development (where you install it yourself) and Docker
deployment (where it is bundled and managed by supervisord). This caused
confusion for a user who set up an external Redis server not realizing
the Docker image already includes one.

Closes #241
2026-03-29 13:15:54 -05:00
Thiago Trautwein Santos
10444fc165 Feature/234 homebox integration (#249)
* finished Items and Locations
* finish maintenance, labels and start notifier
* finish v1 of HomeBox Client API
* sync now creates and updates entities and entities attributes
* create sync for homebox item attachments
* finish homebox integration
* finish test implementation

* Improve Homebox API client quality and pattern conformance

Align the Homebox integration with patterns established by the existing
Home Assistant (HASS) integration, and fix several minor issues found
during code review of PR #249:

- Assert required API options in HbClient.__init__ instead of silently
  logging a warning, matching the HASS client's defensive pattern
- Add warning log when login response lacks a token to aid debugging
- Translate Portuguese log message to English for consistency
- Reference HbClient constants in factory instead of magic strings
- Initialize all cache lists in HomeBoxManager.__init_singleton__
- Update factory test to work with constant references on mock class

* Fix sync bugs and remove dead code in Homebox converter

- Fix attachment order_id overlap: fields and attachments each
  enumerated from 0 independently, causing interleaved display since
  EntityAttribute orders by order_id. Attachments now start after the
  last field index.
- Fix variable shadowing in _sync_helper_entity_attributes where the
  loop variable 'hb_attachment' was immediately reassigned by tuple
  unpacking on the next line.
- Remove unused _create_entity_attributes_from_hb_fields method, which
  was superseded by the sync code using hb_item_to_attribute_field_list.

* Fix entity edit dialog for externally managed entities

HomeBox entities have non-editable attributes with disabled HTML form
fields. Browsers omit disabled fields from POST data, which caused
AttributeForm.clean() to see empty values and raise spurious validation
errors when updating the EntityType.

- Skip validation for non-editable attributes in AttributeForm.clean(),
  since save() already short-circuits without writing for these
- Add suppress_add_new property to AttributeForm to control whether the
  "Add New" attribute card is shown, following the existing pattern of
  display-hint properties (suppress_history, show_as_editable, etc.)
- Pass can_add_custom_attributes through form_kwargs in the entity
  formset so the form can suppress the add-new card for externally
  managed entities even after a bound form re-render

* Add HomeBox integration to documentation

Add HomeBox alongside Home Assistant and ZoneMinder across all
user-facing and developer documentation: Integrations guide, Features,
FAQ, Installation, architecture overview, integration guidelines, and
service patterns.

Also remove stale reference to non-existent hi.services.weather module
from integration guidelines example list.

---------

Co-authored-by: Anthony Cassandra <github@cassandra.org>
2026-03-28 09:53:46 -05:00
Anthony Cassandra
e8e0d8efc8 Tweaked release process doc. 2025-09-25 18:31:38 -05:00
Tony C
47acdaee39 Documentation and agent/command config improvements. (#225)
* Documentation and agent/comment config improvements.
2025-09-23 15:55:17 +00:00
Tony C
06bddccc6a Updated docs, claude agents and commands (#193)
* Working on improving AI agent docs.

* Update dev/docs and claude agent configs.

* Updated claude commands.
2025-09-17 17:19:51 +00:00
Tony C
f5032b72e0 Docs updates for new first-time user flows (#191)
* New Getting Started page, moved and revised content into Editing page.
2025-09-17 00:09:29 +00:00
Tony C
436821a415 Added duration to video browse timline [#165] (#187)
* Added duration time to video browse timeline. 
* Fixed date/time display bug. Tweaks to video stream display points.
* Added correlation role/id for better video browse semantics.
* Fix weather module async/sync issues and enable parallel test execution
- Convert weather tests to AsyncTaskTestCase for proper async handling
- Add async version of weather alerts enabled check
- Fix cache interference in parallel tests with unique test identifiers
2025-09-15 17:36:29 +00:00
Tony C
cf8a02c9c8 Improvement to EntityType icons (#185)
* Added 18+ new entity type items, svg and visual display configs.
* Added EntityType visual browser and improved some SVGs.
* Added small appliance entity type. Location edit mode button tweak.
* Updated initial profiles. A few more UI tweaks.
2025-09-15 01:23:04 +00:00
Tony C
4ca4f6852b Initial startup profile support (#183)
* Added new home profile icon images for upcoming new start page.

* New start page design and structure. New profiles app created (stub).

* First time sidebars/modals. ProfileManager JSON docs and parsing.

* First working ProfileManager. WIP

* Added mechanisms for knowing when to show first-time help content.

* Refactor to clean up initial flows and intro help display.

* New DevTools views structure. Added stub for profile snapshot tool.

* Created initial profiel snapshot generator devtool.

* Rationalized profile generation and parsing with shared constants.

* Fixed location SVG path issues in predefined profiles.

* Improved unit tests for ProfileManager.

* Added SVG fragment install and generation for predefined profiles.

* Fixed unit tests depending on MEDIA_ROOT.

* Fixed remaining unit tests for MEDIA_ROOT isolation.

* Moved JSON initialization profiles to 'assets' dir.

* Refactored all the help content and added view mode ref dialog.
2025-09-14 16:03:15 +00:00
cassandra-ai-agent
6fc76e3da2 Add JavaScript unit tests for auto-view.js using QUnit (#160)
* Add JavaScript unit tests for auto-view.js using QUnit

- Set up QUnit testing framework with local vendoring (no CDN dependency)
- Create comprehensive test suite for auto-view.js core functions:
  * throttle() - timing behavior and edge cases
  * shouldAutoSwitch() - idle timeout decision logic
  * isPassiveEventSupported() - feature detection and caching
  * recordInteraction() and state management functions
- Add test runner HTML file that works offline
- Document testing approach and usage in README.md
- All tests focus on business logic, avoid testing framework internals
- Establishes reusable pattern for testing other JS modules

* Fix test assertion for shouldAutoSwitch 60s threshold behavior

The shouldAutoSwitch function uses >= comparison, so at exactly 60 seconds
it returns true (should auto-switch). Updated test to match actual behavior.

Also added Node.js test runner for command-line testing capability.

* Add master test runner for all JavaScript tests

- Created test-all.html that runs all JavaScript module tests in one page
- Updated README.md with improved workflow for single-URL testing
- Added instructions for adding future JavaScript modules to master runner
- Maintains individual test runners for focused debugging

This addresses the practical concern of needing to visit multiple URLs
as JavaScript test coverage grows.

* Organize JavaScript testing documentation

- Created high-level documentation at docs/dev/frontend/javascript-testing.md
- Covers testing philosophy, framework choice, and best practices
- Uses auto-view.js as reference implementation example
- Streamlined tests/README.md to focus on practical usage
- Follows project documentation organization patterns
- Maintains concise, maintainable approach with code references

* Remove redundant README.md, consolidate all JS testing docs

- Removed src/hi/static/tests/README.md (duplicate content)
- Made docs/dev/frontend/javascript-testing.md completely self-contained
- Added detailed code examples for adding new tests
- Single source of truth for JavaScript testing documentation

* Add comprehensive JavaScript unit tests for high-value modules

- svg-utils.js: 150+ test cases for SVG transform string parsing with regex edge cases
- main.js: Tests for generateUniqueId, cookie parsing/formatting with URL encoding
- video-timeline.js: Tests for VideoConnectionManager array logic and caching algorithms
- watchdog.js: Tests for timer management and restart logic simulation
- Updated test-all.html with proper module loading order (main.js first)
- Fixed svg-utils test assertions to match actual regex behavior
- Enhanced Node.js test runner with better mocking (though some modules too DOM-heavy)

All JavaScript tests now pass in browser - establishes comprehensive testing
coverage for business logic functions across multiple modules.

* Add JavaScript testing documentation links

- Added link to javascript-testing.md in frontend-guidelines.md
- Added link to javascript-testing.md in testing-guidelines.md
- Ensures JavaScript testing approach is discoverable from main guideline docs
2025-09-09 20:46:18 +00:00
Tony Cassandra
a2994ca20e Added document about Home Assistant Integration. 2025-09-09 11:41:36 -05:00
Tony Cassandra
faa2887d6d New HI_VERSION to reflect new process for versioning. 2025-09-08 15:32:38 -05:00
Tony C
9a2944d6ac [Bugfix, Ops] Audio fix, install scripts, dependency updates (#154)
* Fixed bug for audio signal not being cleared.

* Added better server-to-client config interactions.

* New install and update scripts. Supporting GitHub actions and docs.

* Refactor to rationalize server and client environment config.

* Added missing debug setting guard around JS debug.log() messages.

* Dealt with packages having deprecated pip install method.
2025-09-08 17:56:57 +00:00
Tony C
ff762ec185 Fixed: ngnix config issue, install link, Dockerfile improvements. (#148) 2025-09-07 16:30:12 +00:00
Anthony Cassandra
28cb0e6356 A few more documentation fixes. 2025-09-07 00:11:15 -05:00
Anthony Cassandra
dd339de6af Document fixes. 2025-09-06 22:46:55 -05:00
Tony C
535549e74c Documentation updates and pre-release tweaks (#146)
* Bug fix and some documentation improvements.

* Addd model diagrams.

* Documentation Updates and DB utility scripts added.

* Updated documentation.

* Documentation refinements.

* Added new screenshot images and updated docs.

* Added linkk to helper data export scripts.
2025-09-06 22:31:17 -05:00
Tony C
49331dbf8e [Feature] Integration Settings Redesign Implementation (#144)
* Checkpoint: basic integration attribute editing integrated.

* Changed the integrations manage page layout for new editing.

* Improved integrations first time use flows/views.

* Added modal attribute editing for enabling integration.

* Fixed extraneous config page section.

* Added attribute editing options for integartion enable modal.

* Tweaks to integrations editing pages.

* Added left column option for main attribute edit content pane.

* Better styling for vertical tabs: settings and integration pages.

* Removed code made obsolete with new attribute editing framework.

* More obsolete code removal.

* Added missing log suppression for many test files.

* Added first time edit message. CSS file re-org too.

* Improved the editing help modal styling/layout. Added more icons.
2025-09-06 11:48:42 -05:00
Tony C
aacb0b5bbf Configuration Settings Redesign with Attribute Framework Integration (#142)
* Implement Configuration Settings redesign with sticky header + tabs

- Add allow_reordering parameter to AttributeForm to control reorder controls
- Create ConfigEditFormHandler for unified multi-formset handling
- Create ConfigEditResponseRenderer for page-based response generation
- Add system_settings_redesigned.html template with sticky header + tabbed interface
- Update ConfigSettingsView to use helper classes following Entity/Location patterns
- Disable attribute reordering for system-defined config attributes
- Maintain existing URL structure and form submission behavior

* Fix AttributeEditContext integration with SubsystemAttributeEditData

- Create SubsystemAttributeEditContext for proper template context
- Add SubsystemAttributeEditData dataclass to cleanly pair formsets with contexts
- Update form handler and response renderer to use dataclass approach
- Simplify template logic by preparing data structure on server side
- Remove complex template logic in favor of clean dataclass properties

Fixes AttributeError: 'str' object has no attribute 'history_url_name'

* Add missing URL patterns for AttributeEditContext integration

- Add subsystem_attribute_history_inline and subsystem_attribute_restore_inline URL patterns
- Create SubsystemAttributeHistoryInlineView and SubsystemAttributeRestoreInlineView
- Follow Entity/Location pattern for inline history and restore functionality
- Resolves NoReverseMatch error for subsystem_attribute_history_inline URL

Fixes django.urls.exceptions.NoReverseMatch for inline attribute history URLs

* Tweaks for getting backend structure in place. WIP

* Config attrribute config mostly working, but still WIP.

* Good checkpoint on config attribute editing. Works, but WIP.

* Moved config settings to vertical tabs for subsystems.

* Added selected settings tab restoration url strategy.

* Added setting page error count badges to tabs.

* Tweaks to config page entry point and labeling.

* Added auto-dismiss settings update success message.

* Major refactor of attr-v2-modal.js to remove global namespacing.

* Fixed attr-v2 async initialization issue.

* CONverting magic stricts to constants.

* Refactor attribute javascript files to be better organized.

* Fixed ENTER key submit suppression for attributes. Dead code removal.

* Cleaned up all shared temp,ate id, class and selectors.

* Refactor: Attribute editing javascript message posting.

* Removed legacy config attribute history modal views.

* Refactor for better attributeb editing response code organization.

* Interim Refactor: Simple general attribute view working. WIP

* Refactor to introduce attribute page vs. item editing contexts.

* Have Entity and Location converted to refactored attribute editing.

* Checkpoint on converting Subsystem to new attribute editing. WIP

* Completed major attribute editing refactoring.

* Working on unit tests for new attribute refactor. WIP

* Fixed some unit tests. Tests are WIP though.

* Miscellaneous cleanup tasks.

* Fixed unit tests, linting and upload file deletion bug.
2025-09-05 15:26:24 +00:00
Tony C
4305280c14 Location Editing Modal Redesign (#137)
* Implement template generalization for Entity/Location attribute editing

Add AttributeEditContext pattern enabling ~95% code reuse between Entity
and Location editing modals while maintaining type-safe owner-specific access.

- Add AttributeEditContext base class and owner-specific implementations
- Create generic edit_content_body.html template with extensible blocks
- Add template filters/tags for dynamic method calls and URL generation
- Update Entity/Location handlers to provide generalized context
- Replace specific templates to extend generic version
- Fix file upload and history restore template context issues

All Location tests (139/139) and Entity editing tests (17/17) passing.

* Updated documentation. Generic Attribute Editing, Testing

* Added unit tests and fixed linting errors.

* Rationalized view vs. edit mode names and directory organization.

* Fixed JS issues with previous refactor.

* More refactoring to rationalize view vs. edit mode.

* Yet more refactoring to rationalize view vs. edit mode.

* Continued refactoring to Javascript bug fixes for refactoring.

* Fixed a couple unit tests and linting errors.
2025-08-31 23:48:52 -05:00
Tony C
193c86eb65 Entity View/Edit Modal Redesign (#136)
* Document design workflow process and update CLAUDE.md

- Add comprehensive design workflow documentation in docs/dev/workflow/design-workflow.md
- Update CLAUDE.md with design work documentation workflow section
- Establishes data/design/issue-{number}/ pattern for local work
- Documents GitHub issue attachment vs comment organization
- Provides reusable process for future design-focused issues
- Maintains repository cleanliness while enabling design iteration

* Snapshpt fo semi-working new entity and attribute edit modal.

* Refactored new forms to remove duplication. Fixed some textarea bugs.

* Fixed textarea attributes to prevent data corruption.

* Fixed new attribute validation error handling.

* Moved Entity attribute formset non-file filtering closer to def.

* Styling impovements.

* Added attribute value history browsing and restoring.

* Fixed styling on new entity edit modal header area.

* Refactored the messy and confusing new entity edit modal.

* Fixed icon in new attruibute form.

* Attribute Edit style changes.

* Fixed bad claude factoring. Fixed CSRF file upload issue.

* Added scroll-to in antinode.js whic helped fix file upload UX issue.

* Fixed styling of the add new attribute card for the 'mark as secret'.

* Added modified field forms styling for new entity edit modal.

* Fixed bug in secret attribute readonly editing logic.

* Added file attribute value editing to new entity edit modal.

* Removed legacy EntityEditView and related code.

* Refactor to remove EntityEditData.

* Refactor to rename: EntityDetailsData -> EntityEditModeData

* More refactoring for name change from "details" to "edit mode"

* Removed debug log messages. Doc typo fix.

* Refactored entity views to add helper classes.

* Coded cleanup and test fixes.

* Refactored to replace use of hardcoded DOM ids, classes, selectors.

* Refactorings: better naming removed debug messages.

* Renamed "property" to "attribute".

* Fixed unit test gaps.

* Replaced hardcoded form field prefixes with common access.

* Added EntityTransitionType to replace brittle "magic" strings.

* Tweaks on display names for entity edit modal.

* Added missing __init__.py to nested test dirs. (New failures found.)

* Working on fixing unit tests: checkpoint. WIP

* Fixed units tests

* Removed mocking from soem unit tests.

* Removed partial V2 implementation mistake from entity edit work.

* Added testing doc about lessons learned.

* Added a bunch of testing-related documentation.

* Fixed test expectation for LocationAttributeUploadView invalid upload

Updated test to expect 200 status with modal form response instead of 400 error, aligning with new form error handling behavior from entity attribute redesign.
2025-08-31 13:21:22 -05:00
Tony C
39c7266e77 Refactor developer documentation for role-based use (#126)
* Refactored dev documentation extensivekly.

* Style tweaks and link removals.
2025-08-26 23:17:26 +00:00
Tony C
3d1e7f0cd3 [Refactor] Complete icon standardization and UX improvements (#120)
* Implement inline SVG icon system with template tags

- Add dedicated icons.py template tag with validation and accessibility
- Create 10 SVG icon templates for common UI elements
- Add comprehensive CSS classes integrated with existing variables
- Replace HTML entities in alert banner and attribute form
- Support semantic colors, multiple sizes, and accessibility attributes
- Fully self-contained with no external dependencies

* Fix linting issues in icons.py template tag

* Complete icon standardization across all templates and JS files

- Replace all HTML entities (&#43;, &#9650;, &#9660;, &#128247;, &#9654;, &#9201;) with standardized SVG icons
- Replace all emojis in templates (🦊, 🎯, 🧭, 🔒, 🛡️, 💡, 📹) with proper icons or text
- Replace emoji debug prefixes in JS with consistent [AUDIO] text prefixes
- Add {% load icons %} to all modified templates following Django conventions
- Maintain accessibility with proper aria-label attributes throughout
- All templates now use consistent icon system with semantic sizing and colors

* Fix missing icons template tag in event history modal

Resolves template rendering errors in event history views by adding
missing {% load icons %} directive to event_history.html modal template.

This completes the icon standardization refactor (Issue #119) ensuring
all templates properly load the icons template tag before using {% icon %}.

* Add icon usage guidelines to templates documentation

Documents when and how to use the standardized icon system,
including requirements and basic example usage.

* Improve pagination template styling and icons

Replace HTML entities in pagination with standardized SVG icons and
apply app primary color theming. Creates consistent button heights
and responsive icon/text layout.

Changes:
- Add chevron-left, chevron-right, chevron-double-left, chevron-double-right icons
- Update pagination template to use {% icon %} tags instead of HTML entities
- Add pagination CSS overrides using app primary color variables
- Fix responsive layout to show single icon per button with conditional text
- Ensure consistent 24x24 viewBox and template structure for all chevron icons

* Add UX-focused icon usage guidelines to templates documentation

Establishes clear principles for when to use icons based on action types
rather than object specificity. Emphasizes consistency and accessibility
while providing practical examples for common use cases.

* Add essential icons and implement high-impact UX improvements

Phase 3: Add missing essential icons (edit, delete, save, cancel, close)
Phase 4: Implement critical UX improvements:
- Modal close buttons now use semantic close icon (icon-only)
- Primary action buttons (ADD NEW RULE, EDIT) include appropriate icons
- Table delete buttons use semantic delete icon (icon-only for space)
- Modal cancel buttons include cancel icon
- Modal submit buttons include action-specific icons

Follows established UX principle: action type determines icon,
with icon-only for space-constrained areas (modals, tables).

* Complete UX-focused icon improvements with comprehensive documentation

Phase 5: Finalize implementation and documentation
- Complete modal form icon improvements (edit forms now use save icon)
- Update Templates.md with comprehensive examples showing icon usage patterns
- Document icon-only vs icon+text usage patterns with clear examples
- Reference authoritative source for available icons list

All 1,617 tests passing. UX-focused icon rationalization complete.

* Add icons to remaining action buttons following UX guidelines

- Add upload icon for file upload actions
- Add icons to all ADD, CREATE, UPDATE buttons
- Add icons to DELETE actions for consistency
- Skip icons for acknowledgment buttons (OK, DONE) per guidelines
- All 1617 tests passing

* Remove temporary test database files

* Added icon browing UI testing page.

* Fixing linting newline issues.
2025-08-26 04:28:40 +00:00
cassandra-ai-agent
f88ecf57ee Implement auto-view switching feature for AlertManager integration (#98)
* Implement Phase 1 & 3 of auto-view switching feature

- Add TransientViewSuggestion dataclass to console transient_models
- Create TransientViewManager singleton for managing view suggestions
- Add auto-view console settings (enabled, idle timeout, duration)
- Add console helper methods for auto-view settings
- Modify API status endpoint to include transient view suggestions
- Create auto-view JavaScript module with user interaction tracking
- Integrate auto-view with status polling system
- Add CSS styling for auto-view indicator
- Use antinode.js pattern for asynchronous content loading

* Implement Phase 2: AlertManager integration with auto-view switching

- Integrate AlertManager with TransientViewManager for EVENT alarms
- Add auto-view suggestion logic for motion/movement detection alarms
- Implement camera URL resolution using sensor_id from alarm source details
- Create extensible structure for future WEATHER/CONSOLE alarm support
- Replace TODO comment with actual implementation using recent alarms
- Use alarm priority and type for suggestion priority and trigger reason

* Fix flake8 linting issues

- Fix console settings parameter ordering to include value_range_str
- Fix transient_view_manager.py indentation and line continuation issues
- Add newline at end of file

* Add high-value unit tests and fix missing configurations

- Create focused unit tests for TransientViewManager singleton behavior
- Test priority-based suggestion replacement business logic
- Add integration tests for AlertManager with TransientViewManager
- Test motion detection alarms trigger auto-view suggestions
- Test alarm priority propagation to suggestion priority
- Add auto-view.js to Django pipeline configuration in js_hi_grid_content
- Fix SecurityLevel enum values (OFF instead of DISARMED)
- Ensure proper test isolation for singleton pattern tests

* Fix test isolation issues in integration tests

- Reset singleton instances properly between test iterations
- Ensure clean state for each alarm priority test

* Simplify integration tests and fix timezone issues

- Use timezone-aware datetimes for Django compatibility
- Simplify complex integration tests to focus on actual business logic
- Test priority handling and suggestion replacement directly
- Remove overly complex mocking that didn't add value
- All tests now pass reliably

* Refactor antinode.js to add public loadAsyncContent API for programmatic content loading

- Add AN.loadAsyncContent() method for JavaScript-initiated DOM replacement
- Update auto-view.js to use the new public API instead of internal functions
- Remove unnecessary fallback logic and references to internal handleNewContentAdded
- Rewrite antinode.js documentation to be comprehensive and well-structured
- Document all features including the new programmatic API
- Preserve detailed version support explanation in dedicated section

* Updated copyright dates.

* Refactor auto-view switching with improved encapsulation

Move responsibility boundaries to better encapsulate auto-view logic:

**Alert Model:**
- Add get_view_url() method to encapsulate view URL generation
- Move URL extraction logic from AlertManager to Alert model
- Add comprehensive documentation about HiGridView constraint
- Handle EVENT alarm source with sensor_id mapping to camera URLs

**TransientViewManager:**
- Add consider_alert_for_auto_view() method with all business logic
- Move auto-view decision making from AlertManager
- Centralize settings access and alert type filtering
- Handle motion detection filtering and URL validation

**AlertManager:**
- Simplify to coordinator role, just delegates to TransientViewManager
- Remove auto-view decision logic and URL generation methods
- Clean delegation: if new_alert exists, consider it for auto-view

**Test Coverage:**
- Add high-value business logic tests for Alert.get_view_url()
- Add TransientViewManager decision logic tests with realistic scenarios
- Add AlertManager delegation tests (some test isolation issues remain)
- Remove obsolete integration tests for old implementation

**Technical Improvements:**
- Better separation of concerns with each class owning its domain logic
- Clear constraint documentation where URLs are generated
- Extensible design for future alarm source types (WEATHER, etc.)
- Mock only at system boundaries following testing best practices

Note: Some unit tests need refinement for test isolation - will be addressed separately.

* Fix auto-view switching unit tests and improve test reliability

- Replace excessive mocking with real behavior testing in AlertManager delegation tests
- Fix state pollution between tests by adding proper cleanup in setUp/tearDown methods
- Clear singleton state for both AlertManager and TransientViewManager between tests
- Add proper settings mocking for tests running in full suite context
- Fix test data to ensure separate alerts are created for proper testing
- Remove unused imports and variables to pass flake8 quality checks
- Improve test isolation following testing best practices from docs/dev/Testing.md

All auto-view related tests now pass consistently both in isolation and when run together.

* Refine auto-view switching implementation and fix test issues

- Fix singleton test isolation issues in alert and notification manager tests
- Remove unused AUTO_VIEW_IDLE_TIMEOUT backend setting (frontend handles idle detection)
- Add event throttling and passive listeners to auto-view.js for better performance
- Clean up test setup code and improve reliability
- All 1521 tests now pass, flake8 clean

This refinement improves performance and removes unnecessary backend configuration
while maintaining full functionality of the auto-view switching feature.

* Update CLAUDE.md to use Makefile targets for tests and linting

Replace direct Django/flake8 commands with make test and make lint
to eliminate directory path issues and improve reliability

* Fix new alert detection by tracking queue insertion time

The alert system was missing new alerts due to timing differences between
when events occur and when they get added to the alert queue. The client's
"since" timestamp was newer than the event's original timestamp, causing
alerts to be filtered out even though they were newly added to the queue.

Changes:
- Add queue_insertion_datetime field to Alert class
- Set queue_insertion_datetime when alerts are added to AlertQueue
- Update get_most_important_unacknowledged_alert to use queue_insertion_datetime
  instead of start_datetime for "new alert" detection
- Preserve existing behavior for get_most_recent_alarm (uses alarm timestamps)

This ensures new alerts are properly detected regardless of processing delays
between event occurrence and queue insertion.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Simplify auto-view alert filtering logic

Remove brittle string-based filtering in TransientViewManager. The event
subsystem already filters alerts based on user-defined rules, so any alert
that reaches the TransientViewManager should be considered a valid candidate
for auto-view switching.

Changes:
- Remove alarm type and source filtering from _should_alert_trigger_auto_view
- Trust the event subsystem's upstream filtering based on user preferences
- Only verify that alerts have valid data (first_alarm exists)
- Let configuration settings and view URL availability be the only filters

This respects separation of concerns and removes unnecessary duplication of
filtering logic between the event and view management subsystems.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update test description to reflect actual behavior

The test was checking that non-motion alerts don't trigger auto-view, but
after simplifying the filtering logic, the test still passes because these
alerts don't have valid camera view URLs. Updated the test description to
accurately reflect what's being tested - alerts without view URLs are handled
correctly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Refactor view URL generation to fix ZoneMinder auto-view issues

- Move view URL logic from Alert to Alarm class for better encapsulation
- Remove AlarmSource.EVENT restriction allowing all alarm types to have view URLs
- Add sensor_id field to AlarmSourceDetails for clean data propagation
- Create ViewUrlUtils utility class with proper entity/sensor relationship traversal
- Update Event.to_alarm to propagate sensor_id from SensorResponse
- Add comprehensive tests for ViewUrlUtils with proper database setup

Fixes issue where ZoneMinder motion detection events returned None for get_view_url
due to alarm source filtering and missing sensor_id propagation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix ViewUrlUtils bugs and remove obsolete tests

- Fixed entity relationship name from entity_states to states
- Fixed filtering to use entity_state_type_str field instead of property
- Simplified tests to use actual URL generation instead of excessive mocking
- Removed obsolete alert view URL tests that used fake sensor IDs
- All 7 comprehensive ViewUrlUtils tests now pass

The ViewUrlUtils now correctly detects video stream capabilities and generates
proper URLs for ZoneMinder motion detection events.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* A few debug logging improvements while debugging.

* Add development data injection system for frontend testing

Creates a safe, external mechanism to inject test data into API responses
for frontend testing without modifying backend state or triggering Django
auto-reload. Uses dual safety checks (DEBUG + DEBUG_FORCE flags) to prevent
production use.

Components:
- DevInjectionManager utility class for cache/file-based data injection
- DEBUG_FORCE_TRANSIENT_VIEW_OVERRIDE setting with base/development config
- Single-line integration in StatusView for transient view overrides
- Management command for external data injection and control

Usage: python manage.py dev_inject transient_view '{"url":"/test"}'
Options: --persistent, --cache, --list, --clear

* Phase 1: Create hi.testing Django app and migrate dev injection system

- Create proper hi.testing Django app structure with apps.py
- Copy all files from hi/tests to hi/testing (parallel structure)
- Add hi.testing to INSTALLED_APPS in development.py (alongside hi.tests)
- Move dev_inject management command from config to testing app
- Update dev injection imports in api/views.py to use hi.testing
- Verify system works: command available, imports successful

Both hi.tests and hi.testing coexist safely during transition.
Dev injection system now in semantically correct location.

* Complete migration from hi.tests to hi.testing Django app

Phase 2 & 3: Mass import migration and cleanup
- Update 105+ files to import from hi.testing instead of hi.tests
- Remove hi.tests from INSTALLED_APPS in development.py
- Delete old hi/tests directory and all contents
- Fix internal imports within hi.testing module
- Clean up migration artifacts

Results:
- All imports now use proper hi.testing Django app
- Dev injection system fully functional in new location
- Clean codebase with proper semantic organization
- Development-only safety pattern maintained

* Document development data injection system in Testing.md

Add high-level overview of the dev injection system as a general-purpose
runtime behavior modification tool. Uses status response injection as
concrete example while positioning it as extensible mechanism for any
injection point. References detailed docs in DevInjectionManager.

* Fix flake8 linting violations in testing module

* Fix sensor ID type consistency and video stream URL logic

- Change AlarmSourceDetails.sensor_id from str to int for type consistency
- Fix ViewUrlUtils to use video stream sensor ID instead of source sensor ID
- Update tests to use proper sensor ID types and expected video stream URLs
- Fix unit test data setup with proper entity/sensor relationships

* Fix URL bar not reverting when auto-view switches back to original view

- Track original URL when entering transient view
- Use history.back() to properly pop transient URL from browser history stack
- Add fallback using replaceState() if history navigation fails
- Ensure URL bar matches displayed content after auto-view revert

* Fix URL management for multiple consecutive transient views

- Track all transient URLs in array as they are pushed by antinode.js
- Pop all tracked URLs with comprehensive sanity checking when reverting
- Handle race conditions by validating current URL matches expected before each pop
- Enforce invariant that original URL is restored using replaceState fallback
- Prevent history corruption when antinode.js fails or timing issues occur

* Refactor auto-view.js for better maintainability

- Add resetTransientState() helper to consolidate state cleanup
- Extract popTransientUrlIfMatches() to reduce code duplication
- Simplify revertToOriginalView() and makeTransientViewPermanent()
- Add section organization comments for better readability
- Reduce complexity in restoreOriginalUrl() method

* Improve auto-view transient indicator to be layout-friendly

Replace top banner with constrained visual indicator:
- Border effect around main content area during transient views
- Corner badge with reason and pulse animation
- Indicator constrained to main panel, no content occlusion
- Responsive design for mobile devices

* Fix corner badge visibility and use warning colors

- Use portal approach: append badge to body with calculated positioning
- Safer implementation that doesn't modify main content area styling
- Change colors from primary to warning for alert-like appearance
- Add dynamic repositioning on window resize
- Preserve SVG layout responsiveness

* Improve auto-view indicator design per feedback

- Remove pulse animation for cleaner, less distracting indicator
- Change from rounded to rectangular badge design
- Move badge to bottom-right corner, flush to main content boundary
- Dynamic width sizing: auto-width up to max-width of main content
- Short text = minimal occlusion, long text = bottom banner equivalent

* Refatored recently added new "testing" module.

* Updated testing doc for new testing URL location.

---------

Co-authored-by: Tony Cassandra <github@cassandra.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 04:52:49 +00:00
cassandra-ai-agent
a44c973412 [Ops] Add Development Setup Helper Script (Issue #7) (#72)
* Add development setup helper script (Issue #7)

- Create comprehensive setup script deploy/dev-setup.sh
- Automates git configuration, environment setup, virtual environment creation
- Handles package installation and database initialization
- Interactive prompts for user input with validation
- Idempotent design allows safe repeated execution
- Update docs/dev/Setup.md to reference new automated setup option
- Maintain existing manual setup instructions as alternative

* Update CLAUDE.md with GitHub issue template guidance

- Add section on creating GitHub issues
- Document available issue templates
- Include examples of using gh CLI with templates
- Note that blank issues are disabled
2025-08-20 23:33:59 +00:00