4a82595f26ffca8cb93c0352b6694619c71cdba7
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0be3f833df |
feat(android): on-device OS notification capture (NotificationListenerService)
Add an Android backend to os_notification_listener.py so notifications on the experimental Android-TV build drive the existing NotificationColorStripSource LED effects (flash/pulse/sweep, per-app colors + sounds) at app-name parity with the Windows/Linux backends. A Kotlin NotificationListenerService forwards the posting app's display label across the Chaquopy JNI boundary into a new push-based _AndroidBackend + module-level push_notification() receiver; the existing color-strip pipeline, per-app colors/filters, and history endpoint are reused unchanged. - Python: _AndroidBackend (probed first), push_notification() receiver, _LinuxBackend.probe() hardened with is_linux() to exclude Android (which also reports platform.system() == "Linux"). - Android: LedGrabNotificationListener NLS — serial single-thread executor, full crash isolation around Python.getInstance(), label-only forwarding (never notification title/body), ongoing/group-summary/self-package noise filtering. Manifest service exported + gated by BIND_NOTIFICATION_LISTENER_SERVICE (no new uses-permission). - UX: prompt-once notification-access + manual "Grant notification access" button wired into the D-pad focus chain (computed from visible controls); en/ru/zh strings. - Tests: 11 isolated unit tests — module-global + tmp_path history isolation, push routing contract, callback-exception swallowing, None app-name, and a desktop-regression lock on backend selection order. - Docs: README OS-support Android column (notification + audio cells), ANDROID-REVIEW status flipped to Implemented. Zero new Python deps; no build.gradle.kts / Chaquopy pip changes. |
||
|
|
669ae20824 |
feat(value-sources): optional normalization for magnitude sources
Add an opt-out `normalize` flag to the four magnitude value sources (ha_entity, http, system_metrics, game_event). get_value() stays in [0,1] for every source (the normalized scalar-bus invariant), so all existing consumers — brightness sinks, gradient_map, template `name` bindings, and color-strip bindable floats — are unaffected. - normalize=False is a clamp-passthrough: skip the min/max rescale and clamp the raw reading into [0,1] (for sources already reporting a 0..1 fraction). The un-normalized magnitude stays available via get_raw_value() / template raw[name] / automations. - Add get_raw_value() + a raw channel to GameEventValueStream (it had none). game_event's flag is model/stream-level only (no CRUD schema). - Finite-safe clamp01() util; harden the composite-layer brightness multiply (latent negative-wrap / >=1 skip) with it. - Preview WebSocket: tolerate non-numeric raw, generalize raw-range. - Frontend: settings-toggle slider per HA/system_metrics/http editor with min/max grey-out; toggle hidden for fixed-mapping percent metrics. en/ru/zh locale keys. - Additive optional field (default True) — JSON round-trip, no migration. Tests: store create/update round-trip, clamp-passthrough, live normalize flip, game_event raw channel + build_stream forwarding, and finite-safe clamp01. |
||
|
|
6de61b965e |
feat(value-sources): add sandboxed-Jinja template combinator
A new `template` value source evaluates a hardened, sandboxed Jinja expression over the live values of other value sources — the system's first float combinator. Backend: - Shared engine (utils/template_expr.py): ImmutableSandboxedEnvironment with filters/tests and auto-injected globals stripped; only min/max/abs/round/ clamp exposed; rejects **, string/collection-literal repetition, attribute access and non-global calls; NaN/inf-safe result coercion. - TemplateValueSource model + TemplateValueStream runtime: compile-once, primitives-only eval context, raw[name] exposure, eval_interval throttle, ref-counted input acquire/release, rename-safe hot-update. - Validation: unbound-variable + reserved-name rejection, reference cycle/depth guards (depth-only at create, full cycle at update), runtime acquire() depth backstop, and delete referential-integrity. - API: Create/Update/Response schemas + discriminated unions, _RESPONSE_MAP, and an advisory POST /value-sources/validate-template endpoint. - Demo seed: a static source plus a template combinator example. Frontend: - Editor modal section: repeatable inputs list (EntitySelect rows), a zero-dependency Jinja syntax highlighter, a hints/reference panel, and a debounced live validator that gates Save (stale-response-safe). - Graph editor: read-only template node with one edge per input. - i18n (en/ru/zh), icon, and card rendering. Tests: engine, stream, factory/cycle, validate endpoint, and demo seed. |
||
|
|
888f8fd16e |
refactor(types): PEP-604 union sweep + UP007/UP045 enforcement
ruff --select UP007,UP045 --fix converted ~1760 sites across the backend: `Optional[T]` → `T | None`, `Union[X, Y]` → `X | Y`. The remaining module-level alias targets that ruff conservatively skips (BindableFloatInput, ColorList, DeviceConfig) were converted by hand earlier in the pass. black -formatted the result so the wider unions fit cleanly under the 100-char line budget. pyproject.toml now sets [tool.ruff.lint] extend-select = ["UP007", "UP045"] so future legacy imports fire CI on every push. The pre-commit ruff hook was bumped from v0.8.0 -> v0.15.12 to recognise UP045 (split off from UP007 in v0.13). |
||
|
|
9f3f346543 |
refactor(value-source): MetricSpec registry for SystemMetricsValueStream
SystemMetricsValueStream used to dispatch on its ``self._metric`` string across three independent if/elif chains (audit finding M5): * priming in ``start()`` (cpu_percent seed, initial network counter); * raw reading in ``_read_metric_psutil`` plus ``_read_metric_fallback``; * normalisation in ``_normalize`` (percent / min-max range / max-rate). Adding a new metric meant editing all three chains plus the Android fallback — and forgetting one branch made the metric silently return 0. Lift each per-metric concern into a free function and register them as a ``MetricSpec(name, read_psutil, read_fallback, normalize, prime)`` in a new ``core.processing.metric_readers`` module. Shared normalisers (``_norm_percent`` / ``_norm_range`` / ``_norm_rate`` / ``_zero``) live once. The stream's ``start()`` / ``_read_metric()`` / ``_normalize()`` collapse to a single registry lookup + delegation. The stream still owns its mutable state (``_disk_path``, ``_sensor_label``, ``_gpu_unavailable``, ``_prev_net_bytes``, ``_prev_net_time``, etc.) — readers operate on the stream by parameter, not by inheritance, so the kitchen-sink class shrinks by ~140 lines without losing the per-stream cadence bookkeeping. Each spec function's docstring documents which fields it reads or mutates. Tests: 16 new tests cover the 10-metric coverage set, callable shape of every spec field, the three normaliser primitives' clamping + divide-by-zero behaviour, prime-hook presence (only the three metrics that need a baseline: cpu_load + network_rx + network_tx), and fallback-path expectations (desktop-only sensors -> _zero, cpu/ram -> real MetricsProvider). 754 existing core / storage / api tests stay green; ruff clean. |
||
|
|
97dae2cd62 |
refactor(processing): replace inline effect dispatch with @_effect_renderer registry
EffectColorStripStream._animate_loop used to rebuild a 12-entry dict
``renderers = {"fire": self._render_fire, ...}`` on every frame, then
look up ``renderers.get(self._effect_type, self._render_fire)``. Two
audit smells (H1) at once: per-frame dict-rebuild churn and a silent
fallback to fire whenever ``self._effect_type`` was a typo or any
``_render_*`` method got renamed without updating the dict.
Fix:
* ``@_effect_renderer("fire")`` stamps an attribute on the unbound
method.
* ``@_collect_effect_renderers`` (applied to the class) walks
members at class-creation, gathers the marked ones into
``cls._RENDERERS``, and raises ``RuntimeError`` on duplicate
registration.
The loop now reads ``type(self)._RENDERERS`` once and calls the
unbound method with explicit ``self``. An unknown ``_effect_type``
logs a warning and skips the frame (sleep one frame_time) instead of
silently rendering fire — louder failure mode without crashing the
animation thread.
Tests: 5 new tests cover the 12-effect coverage set, callable shape,
class-level (not per-instance) dict identity, duplicate-name
rejection, and the marker stamp contract.
343 existing processing / storage / API tests stay green; ruff clean.
|
||
|
|
29bdacf69a |
refactor(processing): dedupe HA/Z2M _swap_color_source via shared helper
HALightTargetProcessor and Z2MLightTargetProcessor used to carry
character-for-character identical _swap_color_source method bodies
(audit finding C5) — only the log prefix differed. Extract the body
into a free function ``swap_color_source(processor, new_kind,
new_color_vs_id, *, log_label)`` in a new ``light_target_helpers``
module. Each processor's _swap_color_source now delegates to the helper
and then clears its per-entity history (``_previous_colors`` /
``_previous_on``) — that bit stays on the processor because it's per-
target state, not colour-source state.
Scope deliberately narrower than the full BaseLightTargetProcessor ABC
the audit gestured at: the 76 read sites for the per-processor colour
state across the two files made a full state-composition refactor too
risky for the live LED control loop. The free-function helper is the
minimum-blast-radius way to delete the duplication while leaving WLED
(which has no value-stream-vs-CSS dispatch) untouched.
The helper standardises both warning messages on HA's original wording
("failed to acquire color VS stream" / "failed to re-acquire CSS
stream") so existing log alerts/grep patterns keep working.
A LightTargetSwapState Protocol under TYPE_CHECKING documents the
expected processor surface; no runtime enforcement (acceptable trade-
off vs a 76-site touchpoint).
Tests: 7 new tests cover the release+acquire ordering, the not-running
no-op path, the manager-error-swallowing behaviour, the empty-id
short-circuit, and the missing-manager (TargetContext(None, None))
fallback. 354 existing storage + API + e2e + processing tests stay
green; ruff clean.
|
||
|
|
563cbac88c |
refactor(storage,processing): kind registries + versioned data migrations
Two CRITICAL data-safety bugs from the architecture audit and the two
worst parallel-change problems are fixed in one coherent pass.
Audit findings addressed:
- C2 silent CSS response fallback. The previous _RESPONSE_MAP fell
through to a fabricated PictureCSSResponse whenever a source
class lacked an entry; in particular game_event sources were
silently mis-shaped. Now: GameEventCSSResponse/Create/Update
schemas exist, _RESPONSE_MAP is re-keyed by source_type string,
an import-time _assert_response_map_coverage() requires symmetric
agreement with storage._SOURCE_TYPE_MAP, and the runtime path
raises instead of fabricating a response.
- C11 string-replace JSON migration. ColorStripStore used
blob.replace('"source_type": "static"', '"source_type":
"single_color"') which can corrupt unrelated substrings (e.g.
an animation type named "static_wave") and provides no audit,
no transaction, no idempotency. Replaced with
storage.data_migrations.MigrationRunner backed by a
data_migrations audit table. Each migration runs inside one
db.transaction() that covers the applied-check, the apply(),
and the audit-INSERT — partial failures roll back atomically.
StaticToSingleColorMigration parses each row with json.loads
and mutates only the source_type field. Frozen-write databases
skip with a warning.
- C3+C4 color-strip stream dispatch. The 7-branch elif in
ColorStripStreamManager.acquire() and the duplicate one in
ws_stream._create_stream() now share a single STREAM_BUILDERS
registry in core.processing.color_strip_kinds, keyed by
source.source_type. Both call sites populate a StreamDeps bag
and delegate to build_stream(). _assert_stream_kind_coverage()
asserts at import that STREAM_BUILDERS plus SHARABLE_KINDS
partitions storage._SOURCE_TYPE_MAP. ws_stream's preview path
wraps each FastAPI-DI getter in _safe() so non-audio previews
no longer crash when audio/CSPT stores are not wired.
- C6+C7 value stream dispatch. The 14-branch isinstance ladder in
ValueStreamManager._create_stream and its silent
StaticValueStream(value=1.0) fallback are replaced by
core.processing.value_kinds.STREAM_BUILDERS, keyed by
source_type string (so AdaptiveValueSource's adaptive_time and
adaptive_scene route to different builders correctly). The
manager retains only the SyncClockRuntime pre-acquisition step
for animated_color (kinds needing this are listed explicitly
in NEEDS_CLOCK_RUNTIME). Symmetric coverage assertion plus a
separate assertion that NEEDS_CLOCK_RUNTIME is a subset of the
registry.
Bundled in: the static->single_color rename plus the HTTPValueStream
/ http_endpoint introduction that were already in flight on this
branch share these files; the registry refactor naturally absorbs
both via the new "single_color" / "static" alias entries and the
_build_http builder.
Tests: 26 new tests cover response-map coverage drift, migration
runner audit-table mechanics + transactional rollback +
frozen-write skip, and the two stream-builder registries. 343
existing storage / API / e2e tests stay green. Ruff clean.
|