feat(cec): rewrite HDMI-CEC on the kernel uABI, fix passthrough, add a display schedule - #3277
feat(cec): rewrite HDMI-CEC on the kernel uABI, fix passthrough, add a display schedule#3277vpetersson-bot wants to merge 14 commits into
Conversation
Replaces libcec with the kernel CEC API (/dev/cec*), fixes device passthrough, and adds a scheduled display on/off. Mechanism - New lib/cec.py talks to /dev/cec* via ioctl from pure stdlib: no subprocess, no native library, no core-dump workaround. - libcec cannot select an adapter — it enumerates the kernel adapter as com port 'Linux' and treats /dev/cecN as a Pulse-Eight serial port, so `cec-client ... /dev/cec1` fails to open at all (11.07s of retries, measured). The kernel API addresses each node directly. - Every operation fans out across all adapters with a live link, so a device with two monitors attached does not leave the second one lit. - Drops cec==0.2.8, libcec7, cec-utils and libcec-dev. Passthrough - Which /dev/cec* exist is a host property, not a board property. The board-cased sed in upgrade_containers.sh left pi2/pi3/pi3-64/pi4-64 on a useless /dev/vchiq; enumerate the host instead and emit a generated docker-compose.cec.override.yml. - Removes the now-dead vchiq mounts and their per-board strip logic. Schedule - New lib/display_power.py plus a per-minute, edge-triggered beat task. - Falls back to the viewer's blank/unblank when no CEC display answers, so it works on plain monitors too. Notable fixes found on hardware - A failed logical-address claim surfaced as 'no peer', reintroducing the exact conflation of GH Screenly#3267; failures are now a distinct ERROR state. - The kernel returns EBUSY when configuring an already-configured adapter, and libcec leaves every adapter configured after it exits, so the claim now always clears first. Without this, CEC would have failed on every device upgrading from the libcec build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- enable_ssl.sh / disable_ssl.sh recreate anthias-server with their own -f list, which omitted the CEC override and so silently brought the container back with no /dev/cec* passthrough. - Scheduled power now sends CEC *and* the local blank. A device can have a CEC TV on one output and a plain monitor on another; CEC alone reported success and left the monitor lit. A monitor whose EDID advertises no CEC is not even counted in `attempted`, so no count could have detected it. - Disabling the schedule while the display was off left the screen black with no way back: the manual controls are CEC-only and hidden entirely on a device with no adapter. The tick now restores the display and clears the stored state. - The per-minute task no longer lets arbitrary failures escape; it logs a warning and retries next tick, so a persistent fault cannot file a Sentry event every 60s. - Cover the day-checkbox int-membership render, which would silently render everything unchecked if the context handed over strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claim the right logical address. CEC_LOG_ADDR_TYPE_PLAYBACK is 3 and CEC_OP_ALL_DEVTYPE_PLAYBACK is 0x10; the audio-system values (4 and 0x08) were used, so the adapter claimed LA 5 (Audio System) and advertised an audio system. A TV that sees one appear commonly enables System Audio Control/ARC and mutes its own speakers, which would silence a signage player every time the 5-minutely probe ran. Verified on the Pi 5: now claims LA 4, mask 0x0010, matching cec-ctl --playback. - Serialise CEC access with a redis lock. The claim must clear the adapter first, and a clear on one fd unconfigures an operation in flight on another; the two beat tasks coincide every 5 minutes and the server drives the same nodes from a request thread. A contended bus now raises CecBusyError so the scheduler retries instead of latching. - Expire the scheduler's state key. The viewer's blanked flag is in-process and lost on restart while redis is persisted, so a viewer that restarted mid-off-period would stay lit until morning. - Map PowerStatus.TRANSITIONING instead of falling through to 'CEC error' — a TV that is merely warming up is not a fault. - Parse the schedule's day list with parse_days in the template context; the inline version had the opposite empty-input behaviour, showing a schedule as running on no days while it ran daily. - Accept HH:MM:SS (any <input type="time"> with a sub-minute step posts it) and surface an error instead of silently discarding the edit while reporting success. - Skip the lock entirely when no adapter has a live link, which is most of the fleet (0.3-1.4s of redis round-trip measured per transition). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- TimeoutError is an OSError subclass (PEP 3151), so naming both in an except tuple is redundant. Replaced the five sites with a documented CEC_ERRORS tuple so the subtlety is stated once. - Extract the duplicated 'CEC error' literal. - Use [[ ]] for the new conditionals in upgrade_containers.sh. - Annotate the BaseException catch in _run_bounded: it is stashed and re-raised on the calling thread, which Sonar cannot see across the thread boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The parenthesised rule-key form is not valid for Sonar's Python analyzer, which flagged it as a malformed suppression (S7632) and left the original finding open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3277 +/- ##
=========================================
Coverage ? 90.49%
=========================================
Files ? 79
Lines ? 8932
Branches ? 950
=========================================
Hits ? 8083
Misses ? 626
Partials ? 223 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Balena support is an acceptance criterion, and device passthrough cannot deliver it: the balena compose file is baked into the release from a workstation, nothing on-device can enumerate the host's /dev/cec*, and a statically listed node that turns out to be absent stops the container from starting. The viewer is `privileged: true` in all three compose templates, so it already sees every CEC node on every board and every deployment. Move the hardware access there and have anthias-server/anthias-celery ask it over the existing Redis request-reply bus — the same mechanism the v1 current_asset_id endpoint uses. - New lib/cec_client.py: power_status() / set_power() over the bus, plus available() read from a Redis fact the viewer publishes at startup so gating a settings render stays a single GET rather than a round trip. - Viewer gains display_power_status / display_on / display_off handlers. - diagnostics and the schedule now go through the client. - The compose override generator, and the SSL scripts' handling of it, are deleted. No `devices:` entries are needed for server/celery on any board, so the OTA upgrade risk this PR carried is gone entirely. - The CEC bus lock drops from a Redis lock to an in-process mutex, since exactly one process now drives the hardware. Verified end to end on the Pi 4, where anthias-server has no /dev/cec* at all: cec_available() True, get_display_power() 'No CEC display detected' in 79ms. And on the Pi 5 with a live link: real transmits at 430ms, matching direct device access, so the bus hop costs nothing. The Pi 2 — which could never have worked through the server — shows a live 1.0.0.0 physical address from the viewer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
API — the HTML form and DeviceSettingsViewV2 are documented mirrors of
each other, but the schedule was only settable from the form.
- GET returns the four schedule fields; PATCH accepts them.
- Serializer validators normalise 'HH:MM' (accepting 'HH:MM:SS', which
an <input type="time"> with a sub-minute step posts) and the weekday
list, so the beat can never read a value it cannot parse.
- Unlike the form, a malformed value is a 400 rather than a silent
keep-previous: an API client that explicitly sent a field should be
told it was wrong, not have it reinterpreted.
- _isolated_settings_conf moves to the root conftest so the API tests
can use it instead of duplicating the fixture.
UI — the section was built from ad-hoc utility classes that appeared
nowhere else on the page, which is exactly how a bolt-on looks.
- Reuse .weekday-picker / .weekday, the pill chips the asset modal and
bulk-edit modal already use for choosing days, so both places in the
product that pick weekdays look and behave identically.
- Add x-cloak so the fields do not flash open before Alpine initialises,
matching the modal's collapsible sections.
- Copy: lead with the observable outcome ("A TV that supports HDMI-CEC
powers down; any other display goes black") rather than naming the
mechanism, and drop the toggle label's stutter against the section
heading. The overnight-schedule note now gives a concrete example.
- Document why type="time" is right here: flatpickr is initialised in
home.ts, which the settings page does not load.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client could give up while the viewer was still working, reporting a fault that was not one — the same "we could not ask" vs "the display did not answer" conflation this feature exists to remove. viewer power_status worst case 8s lock + 2 x 5s guard = 18s client QUERY_TIMEOUT_MS 12s viewer set_power worst case 2 x 5s guard = 10s client COMMAND_TIMEOUT_MS 8s Both budgets were below the thing they were waiting for. Now the viewer exports MAX_OPERATION_S and the client derives its budget from it, so tuning either guard keeps the two in step. The bus lock also drops to 2s — the viewer dispatches commands sequentially from a single subscriber loop, so it is never contended in practice and only exists as a backstop if that invariant changes. Tests pin both ends of the chain: viewer worst case < client budget < celery soft limit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.weekday input { display: none }` dropped the checkbox out of the tab
order and the accessibility tree, so the day chips could only be operated
with a pointer. Clip the input instead of hiding it, and draw the focus
ring on the chip since the input itself is invisible.
Pre-existing, and fixed at the shared rule rather than in the new
markup, so the asset modal and bulk-edit modal get it too — all three
places that pick weekdays. Appearance is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The feature was shipped with no user-facing write-up, which is the one checklist box the PR left unticked. Covers what actually reaches the screen (CEC *and* local blanking, so a plain monitor is not left lit), the field reference with defaults, the overnight-window semantics — an on-period belongs to the day it starts, which is the part an operator will otherwise get wrong — the v2 API path, and the ~10-minute re-assertion that overrides a manual toggle. Flags the weekday-numbering mismatch against asset scheduling (Mon=0 here, Mon=1 there): the UI hides it behind identical day chips, but an API client hits it directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cec_available() stopped probing /dev/cec* when CEC moved into the viewer — it now reads a single Redis key the viewer publishes at startup. The page_context comment still described the device probe and quoted its per-adapter timing, which is the wrong cost model for anyone deciding whether this is safe to call on every render. Also document, on the website page, that deselecting every weekday means *every* day rather than *no* day. That is the one behaviour in the schedule an operator can trip over without any feedback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR rewrites Anthias’ HDMI-CEC integration to use the Linux kernel CEC uABI (/dev/cec* via ioctl) and moves hardware access into the viewer service, enabling CEC on deployments where the server/celery containers cannot access device nodes (notably balena). It also introduces a scheduled display on/off feature (with local blanking fallback) plus UI, API, docs, and tests.
Changes:
- Replace libcec-based subprocess probing with a pure-stdlib kernel CEC implementation and a server→viewer Redis request/reply client.
- Add a daily display power schedule (weekday selection + overnight semantics), exposed in Settings UI and the v2 device-settings API, backed by a per-minute Celery task.
- Remove libcec dependencies and device passthrough wiring from compose templates and upgrade/deploy scripts.
Reviewed changes
Copilot reviewed 32 out of 36 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| website/content/docs/display-schedule.md | Adds user documentation for the new display schedule and its semantics. |
| website/content/docs/asset-scheduling.md | Cross-links asset scheduling docs to the new display schedule docs. |
| uv.lock | Removes the cec (libcec Python wrapper) dependency from lockfile groups. |
| tools/image_builder/utils.py | Drops libcec build/runtime dependency references in image builder context. |
| tools/image_builder/main.py | Removes cec-utils/libcec7 from base apt deps and documents the new kernel-uABI approach. |
| tests/test_template_views.py | Extends settings-page tests for display schedule rendering and form round-trips; uses shared isolated-conf fixture. |
| tests/test_display_power.py | Adds unit tests for schedule parsing, on/off decision logic, and application behavior (CEC + blanking). |
| tests/test_diagnostics.py | Updates diagnostics tests for the new CEC status mapping and viewer-mediated behavior. |
| tests/test_celery_tasks.py | Adds Celery task tests for the per-minute display schedule tick and its Redis state/TTL behavior. |
| tests/test_cec.py | Adds tests for kernel CEC uABI constants, adapter behavior, aggregation, and bus locking. |
| tests/test_cec_client.py | Adds tests for the server→viewer CEC request/reply client and availability fact key. |
| tests/test_app.py | Updates integration tests/fixtures to skip CEC UI checks without real hardware (no more fake /dev/vchiq). |
| src/anthias_viewer/init.py | Implements viewer-side CEC command handlers and publishes CEC availability to Redis at startup. |
| src/anthias_server/settings.py | Adds persisted settings defaults for display schedule fields. |
| src/anthias_server/lib/display_power.py | Implements schedule parsing/decision logic and applies power via local blanking + viewer-driven CEC. |
| src/anthias_server/lib/diagnostics.py | Replaces libcec subprocess logic with a mapping layer over the new kernel/uABI + viewer client. |
| src/anthias_server/lib/cec.py | Introduces the pure-stdlib kernel CEC ioctl implementation with adapter fan-out and bus serialization. |
| src/anthias_server/lib/cec_client.py | Adds the server-side Redis request/reply client to invoke viewer-owned CEC operations and read availability. |
| src/anthias_server/celery_tasks.py | Adds a per-minute Celery beat task to enforce the display schedule and updates display-power polling behavior. |
| src/anthias_server/app/views.py | Persists display schedule fields from the settings form with normalization and validation. |
| src/anthias_server/app/templates/settings.html | Adds the “Display schedule” section UI (toggle, times, weekday picker). |
| src/anthias_server/app/static/sass/_styles.scss | Makes weekday chips keyboard/screen-reader accessible (visually-hidden inputs + focus ring). |
| src/anthias_server/app/page_context.py | Provides schedule fields and weekday options to the settings template; parses stored day list consistently. |
| src/anthias_server/api/views/v2.py | Exposes schedule fields via GET/PATCH on the v2 device-settings endpoint. |
| src/anthias_server/api/views/mixins.py | Updates display-power endpoint fast-fail comment to match new “no adapter” semantics. |
| src/anthias_server/api/tests/test_v2_endpoints.py | Adds v2 API tests for schedule fields and validation behavior. |
| src/anthias_server/api/serializers/v2.py | Adds schedule fields and validators (time normalization and weekday list validation). |
| pyproject.toml | Removes cec dependency and mypy ignore entry for that module. |
| docker-compose.yml.tmpl | Removes /dev/vchiq passthrough; documents viewer-owned CEC model. |
| docker-compose.balena.yml.tmpl | Removes /dev/vchiq passthrough; documents viewer-owned CEC model for balena. |
| docker-compose.balena.dev.yml.tmpl | Removes /dev/vchiq passthrough; documents viewer-owned CEC model for balena dev. |
| conftest.py | Moves _isolated_settings_conf fixture to shared conftest for cross-file reuse and isolation. |
| bin/upgrade_containers.sh | Removes old device-rewrite logic and deletes stale CEC override file during upgrade. |
| bin/deploy_to_balena.sh | Removes board-specific /dev/vchiq stripping since templates no longer bind-mount it. |
| bin/balena_ota_deploy.sh | Removes board-specific /dev/vchiq stripping since templates no longer bind-mount it. |
| .github/workflows/build-balena-disk-image.yaml | Updates workflow comment to reflect removal of the vchiq strip step. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The comment still described the pre-viewer design: it claimed the availability check globs device nodes in this container and that the boards taking the branch would start reporting real state "once the device passthrough hands them their /dev/cec* nodes". There is no device passthrough any more — cec_available() is a redis GET of the fact the viewer publishes, and the query it guards is a bus round trip rather than a subprocess, so the cost the comment used to justify is gone too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
parse_hhmm accepted any three-part value and read only the first two, so '07:30:xx', '07:30:' and '07:30:60' all became a valid 07:30. The v2 serializer builds its 400-on-malformed contract on this helper, so the API was silently reinterpreting exactly the input it promises to reject — and the HTML form recorded the edit as saved instead of toasting. Seconds are still dropped: the schedule has minute resolution and an <input type="time"> with a sub-minute step legitimately posts them. They are now parsed and range-checked first. Found by Copilot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/anthias_server/lib/display_power.py:64
- Same as above: the warning-level log on parse failure can be hit by API validation paths (400s) and by the per-minute scheduler if a bad value is ever persisted, which risks flooding logs. Downgrading to debug (or not logging here) would keep warnings for actionable faults.
second = int(parts[2]) if len(parts) == 3 else 0
parsed = time(hour, minute, second)
except (TypeError, ValueError):
logger.warning('Ignoring malformed display-power time %r', value)
return None
website/content/docs/display-schedule.md:139
- The docs imply the System Info card only reports
No CEC display detected,No CEC adapter, orCEC error, but the code can also reportMixedwhen multiple attached displays disagree about power state (e.g., one on and one standby). MentioningMixedhere would prevent operators treating it as an error or being surprised by an undocumented status string.
src/anthias_server/lib/display_power.py:52 parse_hhmm()logs a warning for malformed values, but it is called from request validation and from a once-per-minute Celery task. A persistent bad value (or a client sending bad input) could spam warning logs; consider downgrading this to debug (or leaving logging to the caller) so invalid user/config input doesn't become operational noise.
This issue also appears on line 60 of the same file.
parts = str(value).strip().split(':')
if len(parts) not in (2, 3):
logger.warning('Ignoring malformed display-power time %r', value)
return None
src/anthias_server/lib/display_power.py:157
- The comment says “Blank first” but the code sends
unblankfirst whenon=True. Updating the wording to reflect “send local blank/unblank first” will avoid confusion when reading the control flow.
# Blank first, and unconditionally. It is the layer that works
# everywhere, and doing it before CEC means a contended bus (below)
# still leaves the screen in the right visual state.
Two of Copilot's four suppressed comments were right. The System Info card can report 'Mixed' — PowerStatus.UNKNOWN, which lib/cec returns when two attached displays disagree or one is mid-transition. The docs listed every other reading and omitted that one, so an operator meeting it would have no way to tell whether it was a fault. Replaced the prose with the full table of readings. apply_power's comment said "Blank first" while the call sends unblank on the way up. Reworded to name both directions. The other two (parse_hhmm's warning-level logs are potential noise) are left as they are: the schedule silently does nothing when it cannot parse a time, so that warning is the only signal an operator gets, and WARNING does not file a Sentry event the way the runaway ERROR pattern in Screenly#3017/Screenly#3063 did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 36 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/anthias_server/lib/cec_client.py:150
set_power()ignores the viewer's'error'field. When the viewer hits a real CEC/IO error it replies with{'acknowledged': 0, 'attempted': 0, 'error': ...}; returning(0, 0)here makes callers mis-report the failure as "no CEC link" (or similar) and discards the actual reason. Treat'error'as a hard failure and raiseViewerUnavailableErrorso UI/toasts and the scheduler can surface the real problem and retry appropriately.
command = 'display_on' if on else 'display_off'
reply = _ask(command, COMMAND_TIMEOUT_MS)
if reply.get('busy'):
# The viewer had another CEC operation in flight and skipped
# this one. Nothing was transmitted.
raise ViewerUnavailableError('viewer CEC bus was busy')
return int(reply.get('acknowledged', 0)), int(reply.get('attempted', 0))



Issues Fixed
Fixes #3267 (display power permanently
CEC erroron mainline-KMS Pis) and closes #1178 (scheduled display power). Supersedes #3273, which fixed a subset of the passthrough problem.This cannot be verified end to end: no testbed has a CEC-capable display. See "What I could not verify". It no longer changes device passthrough at all — see below.
Description
Rewrites HDMI-CEC on the kernel CEC uABI (
/dev/cec*), moves the hardware access into the viewer so balena is supported, and adds a scheduled display on/off.Why not libcec. Measured on the Pi 5 testbed, libcec 7.0.0 enumerates the kernel adapter as com port
Linuxand treats a positional/dev/cecNas a Pulse-Eight serial port.cec-client -s -d 1 -p 1 /dev/cec1fails withCouldn't lock the serial portafter 11.07 s, having transmitted nothing. Autodetect works (3.2 s) but picks whichever adapter it finds first — a coin toss on a board with two HDMI outputs. It also collapses "no adapter", "no HDMI link" and "display present but not CEC-capable" into one error, which is what made #3267 unactionable.The kernel API answers both questions directly, with no dependency at all.
Fleet probe (
CEC_ADAP_G_CAPS/G_PHYS_ADDR/G_CONNECTOR_INFO, all 0.0–0.1 ms):Two things this settles: the "Pi 1-4 uses vchiq closed firmware" premise is dead (the Pi 2 runs
vc4_hdmiwith a live CEC link while its containers get only/dev/vchiq), and adapter→HDMI-port mapping is discoverable, so nothing needs to hardcode/dev/cec0.What changed
New
lib/cec.py— pure stdlib ioctl; no subprocess, no native library, no core-dump workaround. Every operation fans out across all adapters with a live link, because a device can have two monitors attached and powering down only the first would leave the other lit.diagnostics.py— the CEC half is now a thin translation layer; thestr | boolvalues the v2/infoAPI exposes are deliberately unchanged.libcec removed —
cec==0.2.8,libcec7,cec-utils,libcec-dev.The viewer owns the hardware, so balena works.
/dev/cec*is reachable from the viewer container on every board and every deployment, because it isprivileged: truein all three compose templates. anthias-server and anthias-celery ask it over the existing Redis request-reply bus (lib/cec_client.py) — the same mechanism the v1current_asset_idendpoint uses.Device passthrough cannot support balena: the compose file is baked into the release from a workstation, nothing on-device can enumerate the host's adapters, and a statically listed node that turns out to be absent stops the container from starting. Routing through the viewer means no
devices:entries are needed for server/celery on any board, which also deletes the OTA-upgrade risk an earlier revision of this PR carried, along with the$DEVICE_TYPEcase that left pi2/pi3/pi3-64/pi4-64 on a useless vchiq.New
lib/display_power.py+ a per-minute beat task — daily on/off schedule with day-of-week selection, sending CEC and the viewer's local blank/unblank so it works on plain monitors too.Settings UI + v2 API. A "Display schedule" section reusing the
.weekday-pickerpill chips the asset and bulk-edit modals already use for choosing days, so both places in the product that pick weekdays look identical.DeviceSettingsViewV2GET/PATCH expose the same four fields, since it and the HTML form are documented mirrors — with validators that normaliseHH:MMand the weekday list so the beat can never read something unparsable. The API returns 400 on a bad value rather than the form's keep-previous-and-toast: a client that explicitly sent a field should be told it was wrong.Bugs found while building this. On hardware: a failed logical-address claim surfaced as "no peer" (reintroducing #3267's conflation); and
CEC_ADAP_S_LOG_ADDRSreturnsEBUSYon an already-configured adapter, which libcec leaves behind after every probe — without the clear-first fix, CEC would have failed on every device upgrading from the libcec build while passing every test.In review: the adapter claimed LA 5 (Audio System) instead of LA 4, because
CEC_LOG_ADDR_TYPE_PLAYBACKis3(not4) andCEC_OP_ALL_DEVTYPE_PLAYBACKis0x10(not0x08) — three constants from different enumerations inlinux/cec.h. A TV seeing an audio system appear commonly enables ARC and mutes its own speakers. Also: concurrent CEC users trampled each other (now serialised, withCecBusyErrorso the scheduler retries instead of latching — an in-process mutex suffices since only the viewer drives the hardware), and the scheduler's state key outlived the viewer's in-process blank flag (now expires, so a viewer restart mid-off-period can't leave the screen lit until morning).Device testing
lib/cec.pyoverlaid alone (md5-matched to the commit) into the real server container:live=[cec0],no-peerin 606 ms; claims LA 4, mask 0x0010 (matchescec-ctl --playback)no-link, 0 msno-adapter, 0 msno-adapter, 0 msEnd to end through the Redis bus, overlaying the branch onto both containers and restarting the viewer:
/dev/cec*at all;cec_available()True,get_display_power()→'No CEC display detected'in 79 ms. Viewer loggedCEC: 2 adapter(s) present: /dev/cec0 (f.f.f.f), /dev/cec1 (f.f.f.f). This is the balena situation exactly.not acknowledgedmessage from the non-CEC monitor.1.0.0.0physical address and transmits in 465 ms. That board could never have worked through the server.Overlay provenance: the pinned testbed images are
fbe83e9, older than this branch's base, so the overlay ofsrc/anthias_viewer/__init__.pyanddiagnostics.pyalso carried the unrelated changes from #3224, #3240, #3271 and #3272 to those files. Both containers were restored to the pinned image afterwards and verified clean.What I could not verify
privileged: truegranting/dev/cec*, which is verified on the docker-compose path and is the same container flag balena honours.pulse8-cecand appear as a normal/dev/cecN, but no dongle was available — this is the one place dropping libcec carries real risk.Documentation
website/content/docs/display-schedule.md— a new user-facing page covering what actually reaches the screen (CEC and local blanking, so a plain monitor is not left lit), the field reference with defaults, the overnight-window semantics (an on-period belongs to the day it starts), the v2 API path, and the ~10-minute re-assertion that overrides a manual toggle. It also flags the weekday-numbering mismatch against asset scheduling — Mon=0 here, Mon=1 inplay_days— which the UI hides behind identical day chips but an API client hits directly. Cross-linked from the asset-scheduling page.Checklist
The two device boxes are deliberately unticked, despite the integrated run above. The full software path was exercised on real Pi hardware — real anthias-server process → real Redis bus → real viewer service → real CEC adapter — but the thing the feature exists to do, a display actually powering on and off, cannot be observed without a CEC-capable TV, and no OTA upgrade or balena device was available.
🤖 Generated with Claude Code