189 Commits
Author SHA1 Message Date
Dermot Duffy fe7ac89982 test: Avoid test flakiness by waiting for request count (#2740) 2026-08-30 16:39:39 -07:00
Dermot Duffy bb6794ac49 fix: Give media that is still loading a grace period before rebuilding (#2739)
- Related: #2718
2026-08-30 15:40:24 -07:00
Matthijsanddermotduffy e6bb0eb0e0 fix: claim pointer focus without a visible focus ring (#2733)
Since f240646 (#2639, first released in v8.0.0) the card claims focus on
any `pointerdown` inside it, so that `key` triggers receive their
keyboard events (`keyboard-state-manager.ts`). The claim is a script
call, `element.focus({ preventScroll: true })`, and script-initiated
focus comes with the browser's focus indicator: after a pointer press
while focus was outside the card, the card matches `:focus-visible` and
Chromium draws its default ring around the entire card (measured:
`outline: auto 1px rgb(238, 238, 238)`, a bright line on a dark
dashboard). The ring then persists until focus leaves the card, which
users experience as a white border that appears intermittently when they
click or tap the card.

Ordinary dashboard cards are unaffected because they rely on the
browser's native pointer focus, which shows no indicator. Isolated in
the same browser, a plain `tabindex` element gains focus from a click
without matching `:focus-visible`, while `focus()` from script does
match it. v7 did not claim focus at all, so it never showed this.

**Change:** pass the intent along with the claim: `element.focus({
preventScroll: true, focusVisible: false })`. This code path only runs
for pointer interaction, where no indicator is wanted. Keyboard focus
does not pass through it: tabbing to the card keeps its ring, and the
`key` trigger support from #2639 is unchanged. Browsers without
`FocusOptions.focusVisible` ignore the option and simply keep today's
behaviour. (`focusVisible` is not yet in the bundled TypeScript DOM
types, hence the small global augmentation.)

**Verification:**

- Unit test asserts the focus claim carries `focusVisible: false`.
- Measured in Chromium 152 on a live dashboard: before, a pointer press
on the card leaves it `:focus-visible` with the UA default ring; after,
the same press focuses the card without one, and reaching the card with
Tab still shows the ring. In the same browser, `focus({ focusVisible:
false })` verifiably suppresses `:focus-visible` where a plain `focus()`
sets it.
- `yarn run test`, `yarn run test:browser` (chromium and firefox), `yarn
run lint` and `yarn run typecheck` pass. The webkit browser run fails
one focus test in this local environment, identically on unmodified
`main`, so it is unrelated to this change.

---------

Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
2026-08-30 14:04:40 -07:00
Dermot Duffy 4b101545cc fix: Prevent premature reinitialization when stale HA reconnects (#2717)
- Closes: #2714
2026-08-29 13:47:38 -07:00
Dermot Duffy 8ee69be58b fix: Download recordings with a video file extension (#2712)
- Closes: #2711
2026-08-27 19:17:15 -07:00
Filip Pytlounanddermotduffy 3ee6b6059e feat: Add microphone audio processing constraints (#2708)
## Summary

- add optional microphone constraints for echo cancellation, noise
suppression, automatic gain control, and channel count
- request configured values as non-mandatory `ideal` constraints
- expose privacy-safe microphone capabilities, requested constraints,
and applied settings in card diagnostics
- document the new configuration and add schema, microphone manager, and
diagnostics tests

## Motivation

The card currently calls `getUserMedia()` with `audio: true`. This
leaves echo cancellation, noise suppression, automatic gain control, and
channel count implicit.

Browser and device behavior differs. Explicit processing defaults can
regress microphone gain or amplify noise on some devices. This change
therefore keeps all processing constraints optional and configurable.

## Configuration

```yaml
live:
  microphone:
    constraints:
      echo_cancellation: true
      noise_suppression: true
      auto_gain_control: false
      channel_count: 1
```

Configured values use `ideal` constraints. A browser can ignore
unsupported values. Card diagnostics show the browser capabilities, the
requested constraints, and the reported applied settings.

## Backward compatibility

- existing configurations still use `audio: true`
- no audio-processing defaults are added
- explicit `false` values are preserved
- diagnostic output excludes device and group identifiers

## Validation

- focused microphone, schema, and diagnostics tests: 46 passed
- full test suite: 7,177 passed
- lint passed
- format check passed
- typecheck passed
- unused-code check passed
- production build passed

The optional constraints were also tested successfully with an iOS Home
Assistant Companion client and a go2rtc-based full-duplex intercom. This
is a client microphone-processing change only. It does not add backend
audio denoise.

---------

Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
2026-08-24 07:38:33 -07:00
Dermot Duffy 03d9df8282 chore: Standardize code, comments and docs on US-English spelling (#2707) 2026-08-23 08:57:41 -07:00
Dermot Duffy 965f2b4c9d fix: Keep the notification popup open while its text is selected (#2706) 2026-08-22 21:29:04 -07:00
Dermot Duffy 11cc543406 fix: Hold the 2-way audio backchannel open for the shortest possible time (#2697)
The card claimed a camera's ONVIF audio backchannel in two places that
had
nothing to do with a call: the capability probe at camera init
(`&microphone`),
and a pre-armed `sendonly` audio transceiver on every live WebRTC offer.
Merely
looking at a dashboard occupied the camera's speaker line. Outbound
audio now
travels on its own audio-only WebRTC connection, opened when a call is
answered
and closed when it ends.

- The backchannel is claimed only for the duration of a call. Idle
viewing
  claims nothing.
- Two-way audio now works in `mse`, `mp4` and `mjpeg` modes (note: the
outbound
  audio still traverses WebRTC).
- No renegotiation and no video blink at call start or end.
- A call that cannot carry audio now reports it and ends, instead of
showing a
  live microphone that goes nowhere.
- `live.microphone.always_connected` is now purely about the browser
microphone
  permission prompt.
- Call setup measured at 66ms (LAN) and ~260ms (cellular) for ICE and
DTLS, plus
  ~300ms for `go2rtc` to open an RTSP backchannel.

Verified against a live Frigate + `go2rtc` instance, and by unit tests
at 100%
coverage.

 - Closes #2691
 - Closes #2039
 - Closes #2178

Ref #2299 -- the probe no longer opens a backchannel, but it still runs
per
camera on every load and reconnect, and still dials the camera on the
direct-`go2rtc` path. Caching remains to be done.

Ref AlexxIT/go2rtc#1860 -- once a call has opened a backchannel,
`go2rtc` keeps
that media set up on the camera's RTSP session for the life of the
producer.

Diagnoses #2678
2026-08-21 20:13:58 -07:00
Dermot Duffy 84b90bcc7e fix: Remove the unnecessary item limit from folder media (#2688)
- Fixed: #2573
2026-08-17 21:49:11 -07:00
Dermot Duffy 858c5d0610 fix: Move upgrade button to top of editor (#2687) 2026-08-14 21:13:10 -07:00
Dermot Duffy 85d6811761 fix: Release the microphone to the browser when a call ends (#2685)
The microphone is connected when a call needs it and released the moment
that
call ends, so the browser stops reporting it as in use at hangup rather
than
`disconnect_seconds` later.

Existing configurations are migrated automatically by the visual editor.

 - Closes: #2681 

BREAKING CHANGE: `live.microphone.disconnect_seconds` is removed. The
microphone is released when a call ends, so there is no idle countdown
to
configure. Use `live.microphone.always_connected` to hold it open
instead.

BREAKING CHANGE: The `microphone_connect` and `microphone_disconnect`
actions
are removed. The card owns the microphone lifecycle; `microphone_mute`
and
`microphone_unmute` remain.

BREAKING CHANGE: `call` is removed from `live.microphone.auto_mute`,
whose
default is now `[]`. The microphone is muted when a call ends regardless
of
this option.

BREAKING CHANGE: `microphone_unmute` has no effect outside a call.
Nothing
carries the audio at any other time, so the request is ignored rather
than
opening the microphone.
2026-08-14 16:17:31 -07:00
Dermot Duffy 70ca0a9af4 fix: Stop a released PTZ key ending another held key's movement (#2675)
- Closes: #2668
2026-08-10 20:22:39 -07:00
Dermot Duffy 3808a1b030 fix: Stop PTZ movement and browser key handling from fighting (#2674)
- Closes: #2623
2026-08-10 14:49:50 -07:00
Dermot Duffy a8aa9ec7c2 fix: Should recover from an interrupted initialization (#2673) 2026-08-10 14:27:40 -07:00
Dermot Duffy 5e199b5612 fix: Report media failures once per failure (#2666) 2026-08-09 10:48:17 -07:00
Dermot Duffy 8d8d486afa test: Split browser test-utils (#2662)
- Closes: #2660
2026-08-07 16:27:28 -07:00
Dermot Duffy 673d8fe133 test: Add browser tests for the media viewing gallery and viewer (#2661) 2026-08-07 16:08:56 -07:00
Dermot Duffy c137f4c00c fix: Report and retry media failures for every camera in a grid (#2658)
- Closes: #2637
 - Related: #2099
2026-08-05 21:35:47 -07:00
Dermot Duffy 80ec92e3b2 refactor: Replace Rollup with Vite (#2656)
This is a high risk change: every byte the card ships is emitted by a
different bundler, minified by a different minifier, and every
stylesheet is compiled by a different sass. The intent is that the
card's behaviour is unchanged.

**Significant development win**: Build time drops from 24s to 1.1s 🎉 

Also adds a test suite ('dist') that runs against the built bundle.
2026-08-04 15:00:13 -07:00
Dermot Duffy 2d124fe3cd test: Add a test harness for the built card (#2653)
Mounts the card from `dist/` the way Home Assistant does, by URL with a
HACS tag, and asserts the entry stays a facade, that no chunk imports
it, that the browser is served the file unmodified, and that the card
starts up and fetches a language chunk lazily.

Runs in Chromium, Firefox and WebKit, so a change of bundler or minifier
is checked against every engine.
2026-08-03 21:50:21 -07:00
Dermot Duffy 18ea725564 test: Also run firefox & webkit automated tests (#2651) 2026-08-03 16:48:30 -07:00
Dermot Duffy 5d247d4074 test: Add browser based tests for keyboard focus handling (#2647) 2026-08-02 16:38:17 -07:00
Dermot Duffy 17146c8ca6 fix: Clear the initialized condition state when the card is not usable (#2646)
The `initialized` state (used in conditions/triggers) was written once
and never cleared, so it meant "has this card ever been initialized"
while everything reading it took it as "is this card usable now". Home
Assistant takes a card off the page and puts it back whenever its
dashboard tab is left and returned to, so the card initialized again
while the state claimed it was initialized throughout: `trigger:
initialized` fired once per card rather than once per startup, and
automations were dropped in between.

The card lifecycle is now an explicit state machine (`SessionManager`),
the only writer of `initialized`, which separates a card that is
starting up from one initializing part of itself again while it runs. A
new `ever` parameter (conditions/triggers) selects the old latched
behaviour.

`remote_control` uses that parameter to keep its two camera priorities
correct under repeated starts. With `camera_priority: entity` the card
now re-reads the entity every time it starts, so a camera selected while
the card was away is picked up on return. With `camera_priority: card`
the card writes the entity on its first start only, unchanged, since
repeating that write would overwrite a camera the user had selected.

Closes: #2642


BREAKING CHANGE: `condition: initialized` is now `false` whenever the
card is not usable, and `trigger: initialized` fires each time the card
starts up rather than only the first time. Set `ever: true` on either to
keep the previous behaviour.
2026-08-02 12:52:58 -07:00
Dermot Duffy e590f72783 test: Add a browser based test harness (#2641) 2026-07-31 10:16:58 -07:00
Dermot Duffy f24064689a fix: Restore keyboard focus for key triggers (#2634) (#2639)
- Closes #2634
2026-07-30 20:52:54 -07:00
Dermot Duffy a736f3dfaf fix: Ensure a bare ptz_digital action resets the zoom (#2635)
- Closes: #2632
2026-07-29 23:18:06 -07:00
Dermot Duffy 15b08db3e8 fix: Clear stale media unavailable errors when a camera recovers (#2627)
- Closes: #2576
2026-07-28 21:21:50 -07:00
Dermot Duffy ad89aa9538 test: Split test utilities to improve test times (#2622) 2026-07-26 16:29:53 -07:00
Dermot Duffy 8d44fcecf1 fix: Ignore startup triggers from cameras without the trigger capability (#2621)
- Closes: #2103
2026-07-26 15:37:59 -07:00
Dermot Duffy 0c3d46ad3d chore: Upgrade to Vitest 4 and regroup tests for performance (#2618) 2026-07-26 11:28:58 -07:00
Dermot Duffy a0fd464d23 test: Remove unnecessary jsdom environment declarations (#2616) 2026-07-25 22:47:59 -07:00
Dermot Duffy 8f953547b1 fix: Connect the microphone when an inbound call is *answered* (#2609) (#2612)
- Closes: #2609
2026-07-25 20:56:07 -07:00
Dermot Duffy 2a84f199de fix: Size the expanded dialog and grid columns to their content (#2611)
- Closes #2607
2026-07-25 13:46:31 -07:00
Dermot Duffy f419798e8e fix: Exponentially back off initialization-error retries (#2579) (#2605)
- Closes #2579
2026-07-24 10:54:31 -07:00
Dermot Duffy 54f75beb9d fix: Improve media_query retry behavior (#2603) 2026-07-23 22:19:17 -07:00
Dermot Duffy 448fa2d9f1 feat: Surface the provider's error cause in the media_unavailable notification (#2599)
- Closes: #2592
2026-07-22 20:45:49 -07:00
Dermot Duffy 5a549c0326 fix: Prevent microphone auto-disconnect during calls and restore the removed microphone connected condition (#2597)
- Closes: #2590
2026-07-22 16:12:31 -07:00
Dermot Duffy 87ecbc7e45 feat: Trigger automations on call answered, rejected and hung up (#2591) 2026-07-22 11:11:57 -07:00
Dermot Duffy b6e1de5999 feat: Modernize the visual card editor. (#2586) 2026-07-20 20:44:16 -07:00
Dermot Duffy c02c692f68 feat: Add experimental rewrite of go2rtc live provider (MSE/WebRTC/MP4/MJPEG) (#2580)
- Closes #2556
- Closes #2450

**Key intended features:**
 - go2rtc compatible
- 100% test coverage to significantly improve ability to test, maintain
and work around browser weirdnesses (e.g. Safari).
 - Written from the ground up in the style of the rest of the project.

**To use:**
 - Change `live_provider` from `go2rtc` to `go2rtc-experimental`.
2026-07-14 14:22:41 -07:00
Dermot Duffy 5662e48c22 test: Add regression test for media info stability (#2571)
- Relates to: #2563
2026-07-08 11:24:18 -07:00
Dermot Duffy fb1bbc739e feat: Automatically recover from frozen live streams (#2569)
- Closes: #2099
2026-07-07 21:37:24 -07:00
Dermot Duffy 5c97e55a56 fix: Honor configured home preset / merge presets. (#2553)
- Closes:
https://github.com/dermotduffy/advanced-camera-card/issues/2525
2026-06-30 17:45:13 -07:00
Dermot Duffy d833edb65b perf: lazy-load js-yaml (~107KB) as needed (#2551)
- Closes: #2532
2026-06-30 17:45:13 -07:00
Dermot Duffy 3fe5e4004a fix: prevent infinite re-render loop from issue retry callbacks (#2550) 2026-06-30 17:45:13 -07:00
Dermot Duffy c3d3dd3934 chore: Update ha-nunjucks to 1.6.2 (#2549)
- Replaces:
https://github.com/dermotduffy/advanced-camera-card/pull/2515
2026-06-30 17:45:13 -07:00
Dermot Duffy ea251ca988 perf(bundle): lazy-load the nunjucks template engine (#2535)
Closes #2531.

## Summary

The full `nunjucks` templating engine (~226KB) plus `ha-nunjucks`
(~46KB) — roughly **272KB, ~13% of the eager entry chunk** — was
statically imported and downloaded by every card on initial load, even
though templates only apply when a config value contains a `{{ … }}` /
`{% … %}` delimiter. Most cards use no templates and never need the
engine.

This defers the engine behind a dynamic `import('ha-nunjucks/dist')` so
it ships in a separate, on-demand chunk instead of the eager
`card-*.js`.

## Approach

The render path (`TemplateRenderer.renderRecursively`) is **kept
synchronous** — it is called from many synchronous hot paths
(condition/trigger evaluators, picture-elements rendering, actions,
folder matchers), and making it async would be a large, high-risk
refactor of the evaluation core.

Instead:

- **New `src/card-controller/templates/engine.ts`** — a module-level
singleton lazy loader (`loadTemplateEngine()` / `getTemplateEngine()`)
shared across all `TemplateRenderer` instances, plus a
`containsTemplate()` delimiter helper.
- **Delimiter gating** — strings without a delimiter never touch the
engine (the overwhelming majority of renders).
- **Pre-warm at config time** — because every template string originates
in the config, a new mandatory `TEMPLATE_ENGINE` initialization aspect
loads the engine before first render whenever the config contains a
delimiter. This **guarantees no raw `{{ … }}` flash**: content/condition
rendering is blocked until the engine is present for template-using
cards. Cards without templates never load it.

## Result

- nunjucks + ha-nunjucks move out of the eager chunk into a separate
chunk fetched only when a card actually uses templates.
- No change to the synchronous public render API; condition/trigger
evaluation core untouched.

## Tests

- New `engine.ts` loader coverage (concurrent load, cached reuse,
not-loaded fallback).
- The 11 existing test files that render real (delimiter-bearing)
templates declare their dependency explicitly via
`beforeAll(loadTemplateEngine)` — no global/implicit setup hook.
- Full suite green (4764 tests), lint and ts-prune clean, per-file 100%
coverage maintained for the affected directories.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016ykWemdkZrgywvC71pHc6c

---
_Generated by [Claude
Code](https://claude.ai/code/session_016ykWemdkZrgywvC71pHc6c)_
2026-06-30 17:45:13 -07:00
Paul Botteinanddermotduffy b33c034810 feat: Add card picker suggestion for camera entities (#2517)
Selecting a camera entity in the dashboard card picker now suggests
Advanced Camera Card under the Community section, so you can add it in
one click without searching for it manually.

Requires Home Assistant 2026.6 or later.

Docs:
https://developers.home-assistant.io/docs/frontend/custom-ui/custom-card#suggesting-your-card-for-an-entity

---------

Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
2026-06-30 17:45:13 -07:00
Dermot Duffy 7dc29865b8 chore: Enforce consistent type imports via @typescript-eslint/consistent-type-imports (#2545) 2026-06-30 17:45:13 -07:00