perf: server-side aggregate sensors and client-side optimizations#152
Conversation
| "type": "bind" | ||
| } | ||
| ], | ||
| // "mounts": [ |
There was a problem hiding this comment.
Not sure what this folder is for?
There was a problem hiding this comment.
Good catch. That's a mount for sapiens, a project I use to help my AI agents develop, unrelated to Linus Dashboard.
| # Composants essentiels (remplace default_config sans go2rtc) | ||
| automation: !include automations.yaml | ||
| script: !include scripts.yaml | ||
| # automation: !include automations.yaml |
There was a problem hiding this comment.
Errors booting HA up if these are enabled, since the files don't exist
There was a problem hiding this comment.
Are you launching the test instance through the devcontainer setup? Trying to figure out if this is a gap in the devcontainer bootstrap (should probably generate/include empty automations.yaml/scripts.yaml) or something else on your end.
| # Start Home Assistant | ||
| dev: | ||
| @echo "📄 Loading environment..." | ||
| @if [ ! -e config/custom_components ]; then \ |
There was a problem hiding this comment.
This allows LD to be present as an integration in the new HA setup
|
Hey @flatline-84 , sorry for the late review, and thanks for this ! 💚 You're our first outside contributor so it's an honor to review your MR, and it tackles a real perf problem which makes it even more relevant. I'm more reactive on Discord if you want to discuss features or anything related, feel free to ping me there. You put your finger on a real problem. I've always tried to minimize the complexity to keep things light, but you're right that with a lot of entities it gets slow to load... Going through dedicated sensors like you did is a really good approach. I tested the branch on our HA test instance and ran into something that looks like a bug: on a cold restart, all the aggregate sensors came up empty (0 entities), but reloading the integration afterward found everything correctly. I still need to investigate more on my end to confirm it's not just something specific to my test setup. If it does turn out to be a race condition (entity discovery running before other integrations have posted their state). Feel free to take a look either way. One question: device_class_filter is always None in _build_aggregate_sensors, so device_class-scoped chips never get a server sensor and fall back to the old path. Intentional for this first pass? I just merged the Magic Areas PR so this branch is behind now, probably conflicts — can you rebase when you get a chance? Thanks again, looking forward to getting this in. ✨ |
…e state The aggregate sensors only tracked entities whose live state already existed in the state machine at platform-setup time. On a cold boot the sensor platform is forwarded before other integrations have posted their states, so entities were skipped and the tracked set (frozen at construction) never recovered until a reload. Depending on timing this produced fully empty aggregates or partially-populated ones that never updated when the untracked entities changed (most visibly the floor light chips). Resolve device_class from the entity registry, which is fully populated and persistent early in startup, and drop the hass.states gate. Live states are still read in _update_state at compute time, so counts remain correct as states arrive.
The build was hardcoding device_class_filter=None, so the only sensors created were domain-level. But the frontend (AggregateChip.getAggregateSensorId) still appends device_class to the id it looks up, and compute_color for binary_sensor keys its color map on device_class. As a result every device_class-scoped chip failed the lookup and silently fell back to the slower client-side template path, losing the perf win for exactly the high-entity-count domains (motion/door/etc.). Emit both tiers: an entity always joins its domain-level bucket and, when it has a device_class, an additional (domain, device_class) bucket. This produces sensor.linus_dashboard_<domain>_active for plain chips and sensor.linus_dashboard_<domain>_<device_class>[_<floor>]_active for device_class chips, matching the ids the frontend constructs.
|
Hey @s4piens, Out of all the dashboards I've tried, this is the one that got closest to what I wanted for how little setup I wanted to do 😆 I've updated and fixed (I think) the problems you've highlighted. I also had an issue (same problem?) with the count of entities being off sometimes and I think it's fixed. Unfortunately, pretty hard to test so I usually end up deploying to the HA that runs my house and I receive feedback (complaints) from my wife.
Yeah this was because on cold-boot not all of the entities are ready, so it freezes the set before it has a chance to get everything. This should be fixed
Should be fixed as well. This is a repo (and PR) that's very heavily developed with AI but going forward I'm going to try to understand the codebase better and put more directed fixes in (or features). Just wanted to get a small(-ish) PR up to understand how it's all setup and make sure I can dev properly. PR #159 looks handy!
Done. Let me know if anything is incorrect - I don't use Discord often but maybe we can chat and work on features / improvements in the future! |
Resolves conflicts in .devcontainer/devcontainer.json and README.md against the fake-house dev-env automation merged to main — took main's side for both (fake-house-watch autostart, corrected dev-setup docs), nothing from performance-improvements' own changes to those two files was lost since it only touched the surrounding dev-setup text, not the performance work itself.
|
Hey @flatline-84, Really glad this is your first contribution — and love that your wife is doing QA on the live deployment 😄 That's about as real-world testing as it gets. The server-side aggregate sensor approach was exactly the right call — cutting home view template evaluations from ~60 to ~3 is a big deal, and it's basically what kicked off the whole performance push the project has had since. I'll probably revisit parts of it to make it a bit more general-purpose down the line, but that's building on what you opened up here, not replacing it. Glad the cold-boot race and the device_class_filter thing are sorted — merged! 🎉 Feel free to drop by Discord if you want to bounce around feature ideas, always happy to have you keep contributing. |
Brings in PR #152 (server-side aggregate sensors + Magic Areas reintegration) and PR #159 (fake-house dev-env automation). Real conflicts in sensor.py, AggregateChip.ts and entityResolver.ts — resolved by keeping PR #152's full DOMAIN_ACTIVE_STATES + device_class bucket coverage in _build_aggregate_sensors (needed for AggregateChip's floor/global fast path across every domain) rather than restricting it to climate/media_player as originally planned; that restriction assumed every other domain's hidden counting sensor was fully superseded by this branch's own dedicated group entities, but AggregateChip.ts doesn't know about those yet, so dropping the generic sensors would have silently regressed PR #152's just-merged perf work for floor/global-scope chips. Left a note in the module docstring — de-duplicating the two systems (AggregateChip preferring the dedicated group entity when one exists) is a follow-up, not part of this change. entityResolver.ts: kept this branch's removal of the Linus Brain branch for presence/all_lights (superseded by native Dashboard entities, no backward compat per plan) while preserving main's Magic Areas fallback branches intact.
… entities AggregateChip.ts's floor/global fast path (PR #152) and this session's dedicated group entities (light.py/switch.py/fan.py/cover.py/siren.py/ binary_sensor.py) were computing the same total/active_entity_ids/icon/ color twice for light, switch, fan, cover and most binary_sensor device_classes — the chip never knew the dedicated entity existed, so it kept reading the generic hidden counting sensor even when a richer, controllable equivalent was sitting right next to it. - AggregateChip.ts: getAggregateSensorId() now tries the dedicated group entity first (light.linus_dashboard_all_lights_*, etc., or binary_sensor.linus_dashboard_{device_class}_* for classes that have one), only falling back to the generic sensor for climate/ media_player (no dedicated group — no simple on/off control semantics) and binary_sensor classes that don't get their own group (motion/presence/occupancy, folded into the presence composite instead). The two sources report their count differently (the generic sensor's own state IS the count; a group entity's state is on/off, count is in active_entity_ids), so the content template branches on which one was found. - sensor.py: _build_aggregate_sensors now only generates the domain-level (no device_class) bucket for GENERIC_DOMAIN_LEVEL_DOMAINS (climate, media_player, binary_sensor — binary_sensor has no "every class regardless of type" dedicated group). Device_class buckets stay unrestricted for every domain, since they're still the only source for combinations without a dedicated group (cover-by-type, binary_ sensor's presence-related classes). Also renamed the sensor's `entity_ids` attribute to `entity_id` (ATTR_ENTITY_ID) for consistency with every other group-shaped entity in this integration. - __init__.py: added async_cleanup_orphaned_aggregate_sensors, called before platform setup — anyone already running the old (still-) restricted set would otherwise keep 11 permanently-unavailable sensors (light/switch/fan/cover's domain-level bucket) in the registry forever, same footgun as the Linus Brain cleanup. Verified live against the ha-test fake house: hidden sensor count went from 33 to 22 (exactly the 11 light/switch/fan/cover domain-level entries removed, climate/media_player/binary_sensor untouched), the cleanup routine logged and removed all 11 orphans on restart with no errors, and all 39 dedicated group entities are unaffected.
… computation Goes further than the previous group.util-based refactor: light.py, switch.py, fan.py, cover.py, and binary_sensor.py's per-device_class groups now directly inherit HA core's own homeassistant.components.group.<domain> class (e.g. group.light.LightGroup) alongside NestedGroupMixin, delegating async_update_group_state() entirely to HA instead of maintaining our own copy of that computation. Same class Magic Areas builds its own groups on. NestedGroupMixin gained _sync_ha_group_state(domain, device_class=None) to drive this: keeps self._entity_ids (HA's own classes read this name, not our _member_entity_ids) in sync, calls async_update_supported_features per member for the platforms that track features incrementally (fan/cover), calls the inherited async_update_group_state(), then layers our own entity_id/total/active_entity_ids/icon/color on top via compute_group_attributes. async_added_to_hass no longer calls super() — GroupEntity's own version (further up these classes' MRO) sets up an *undebounced* subscription, which would defeat NestedGroupMixin's own debounce (added for PR #152's "flatline" performance issue). Two HA-init quirks discovered and documented in light.py's module docstring (same pattern repeated in every file below): - Entity._name_internal checks `hasattr(self, "_attr_name")`, not truthiness. HA's own __init__ always sets it, permanently blocking our translation_key-based naming unless explicitly `del`eted (not nulled) right after calling it. - HALightGroup specifically also sets a class-level _attr_icon default ("mdi:lightbulb-group") — Entity.icon has the same hasattr pattern, and when non-None it overwrites our own dynamic on/off icon (which lives inside extra_state_attributes, same "icon" key, different mechanism) in HA's flattened state attributes. Explicitly nulled at the instance level for light.py only — no other converted platform sets a default icon. Deliberately kept as our own overrides, not HA's equivalents, where we have a considered behavior difference: - light.py/fan.py: async_turn_on's smart on-members-only adjustment filtering (HA's own forwards blindly to every member regardless of current on/off state). - light.py: supported_features narrowed to the intersection across members and min/max_color_temp_kelvin computed from real members (HA's own unions features and hardcodes a fixed 2000-6500K range regardless of members). - fan.py: preset_mode support entirely, plus async_set_percentage/ async_set_preset_mode's on-members-only filtering — HA's own FanGroup doesn't track preset_mode at all (missing from its SUPPORTED_FLAGS). - cover.py: async_set_cover_position's on-members-only filtering. Inherited as-is (no override) where HA's own behavior is equal or better: - switch.py: turn_on/turn_off (identical blind-forward logic either way). - fan.py: gains oscillate/direction support for free (never had it before). - cover.py: open/close/stop_cover and all _tilt variants — HA's own already forward only to members supporting the relevant feature, a safety property our own hand-rolled versions of these (not set_cover_position, which still has real on-members-only logic worth keeping) didn't have. climate.py (no HA core group.climate exists) and media_player.py (HA's own group.media_player doesn't inherit GroupEntity at all and uses a different internal attribute name — a worse fit for this pattern) are unchanged, already at the group.util level from the previous refactor. binary_sensor.py's PresenceGroup (a composite OR-gate across multiple domains/device_classes, not a single homogeneous list) has no HA equivalent either, also unchanged. Verified via live restarts of the ha-test fixture after each file: no tracebacks, and correct friendly_name/icon/device_class/active_entity_ids for light/switch/fan/cover/binary_sensor groups, including a live light turning on and the group correctly reflecting active_entity_ids/icon/color.
…#161) * feat: area/floor/global group entities for presence, lights, and more Rapatrie de Linus Brain vers Linus Dashboard les entités groupe "mécaniques" (pas d'IA) — présence par zone et groupe de lumières — pour qu'elles soient disponibles à tout utilisateur de Dashboard, pas seulement ceux ayant Brain installé. Généralise ensuite le même pattern à switch/fan/cover/siren et à tous les device_class de binary_sensor, avec une hiérarchie imbriquée à trois échelles (global > étage > zone) plutôt qu'un système plat. - group_manager.py, entity_group.py : logique de scan/imbrication partagée, portée du pattern anti race-condition de Linus Brain (EVENT_HOMEASSISTANT_STARTED + délai) qui a résolu le bug de cold-restart de _build_aggregate_sensors. - binary_sensor.py : groupe de présence composite (motion/presence/occupancy/ media_player) + un groupe par device_class binary_sensor restant. - light.py, switch.py, fan.py, cover.py, siren.py : groupes de contrôle par zone/étage/global, mêmes device en HA (get_area_device_info/ get_floor_device_info), même garde anti-boucle infinie (entity.platform == DOMAIN) documentée dans chaque fichier qui scanne par zone. - sensor.py : ajout de sensors numériques (température/humidité/illuminance/ batterie, somme/moyenne/minimum selon state_class et device_class) et d'un sensor de santé (entités unavailable/unknown) par zone/étage/global ; le système de comptage caché existant (PR #152) ne garde un rôle que pour climate/media_player, tous les autres domaines étant désormais couverts par une entité groupe dédiée et visible. - entityResolver.ts : resolvePresenceSensor/resolveAllLights pointent directement vers les nouvelles entités natives (plus de fallback Brain) ; ActivityDetectionChip.ts corrigé pour ne plus dériver la détection Brain de la résolution de présence (devenue toujours "native"). - config_flow.py/__init__.py : option pour masquer ces nouvelles entités des assistants vocaux (best-effort, API HA versionnée). Basé sur pr-152 (pas main) car aggregate.py/sensor.py — dont ce travail réutilise compute_active_count/compute_icon/compute_color — n'existent que là. Conflit de merge attendu sur entityResolver.ts une fois pr-152 fusionnée, main ayant déjà réintégré le support Magic Areas (#142) après la création de pr-152 ; à réconcilier à ce moment-là. Validé en live sur l'env de test HA (ha-test, HA 2026.4.0, fake house 6 zones / 2 étages) : 88 entités créées sans erreur au démarrage, hiérarchie imbriquée confirmée via l'API (le groupe étage liste bien les entity_id des groupes de zone, le global liste les groupes d'étage, pas les entités brutes). Un bug réel corrigé pendant cette validation : `GroupEntity` n'est pas dans `homeassistant.helpers.group` sur HA 2026.4.0 mais dans `homeassistant.components.group.entity`, avec des attentes internes (`_entity_ids`, hooks de preview) non satisfaites par mon code — retiré de l'héritage de PresenceGroup/BinarySensorDeviceClassGroup, NestedGroupMixin gère déjà seul ce qui compte réellement (l'attribut entity_id lu par la modale more-info). Connu comme non couvert par cette PR : aucun test pytest (le dépôt n'a actuellement aucune infra de test Python, seulement ruff en CI — bootstrap d'un harnais pytest-homeassistant-custom-component est un chantier séparé) ; retrait des entités équivalentes côté Linus Brain (repo différent, PR séparée) ; agrégation position/vitesse pour cover/fan (v1 = on/off uniquement) ; câblage des nouveaux groupes switch/fan/cover dans AggregateChip.ts (les entités existent, ce chip ne les résout pas encore) ; contrôle réel des entités (turn_on/turn_off) pas encore testé en live, juste leur création et leurs attributs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat: full feature aggregation for light/fan/cover/siren groups Previously every group entity (except switch, inherently binary) only forwarded bare on/off — light.py's own docstring claimed brightness/color support but _attr_supported_color_modes was hardcoded to ONOFF, and fan/cover explicitly deferred percentage/position as v1 scope. Ports Linus Brain's AreaLightGroup feature-aggregation logic (dynamic supported_color_modes/supported_features detection via intersection, brightness/color averaging across on members, smart filtering so an adjustment like "nudge brightness" only touches already-on members without waking dark ones) and extends the same pattern to fan (percentage/preset_mode) and cover (position/tilt), plus lighter supported_features/available_tones detection for siren (which has no persistent tone/volume state to average, unlike the others). Two real bugs found while validating live against the ha-test fake house: - entity_group.py's scan_domain_members only excluded this integration's own entities (entity.platform == DOMAIN), so a foreign integration's own group entity living in the same area (Linus Brain's light.linus_brain_all_lights_salon, Magic Areas' fan group) got scanned in as if it were a raw member — double-counting it and skewing averaged attributes. Fixed by also excluding any entity whose own state already carries an `entity_id` attribute (the same ATTR_ENTITY_ID convention this integration, Brain, and HA's native `group:` all use to mark "I am a group"), a general fix that doesn't need to know about specific other integrations. - cover.py read ATTR_POSITION/ATTR_TILT_POSITION from member state attributes, but those are the set_cover_position/ set_cover_tilt_position *service call* parameter names — the actual state attributes are ATTR_CURRENT_POSITION/ATTR_CURRENT_TILT_POSITION, a different pair of constants. Fixed; verified live (opening cover.volet_salon now correctly surfaces current_position on the area group). Verified live: fan percentage read/write round-trip (turn_on with percentage, set_percentage, turn_off — all correctly forwarded to and reflected from the real member), cover open/close + position reporting, and the foreign-group exclusion (member counts dropped from 4→3 and 2→1 once Brain's/Magic Areas' own groups stopped being counted). Brightness/color specifically not live-tested — the fake house's lights are all plain on/off templates with no dimmable fixture — but the aggregation math is structurally identical to the verified percentage/position averaging. * fix: drop redundant location suffix from group entity names, add dimmable test light Every group entity (light/switch/fan/cover/siren/binary_sensor groups, numeric sensors, health sensor) already gets its own per-scope device (get_area_device_info/get_floor_device_info/get_global_device_info), which HA's has_entity_name convention combines with the entity's own name wherever it's displayed. But the entity's own translation string also embedded the area/floor name (or "(Whole House)"), producing doubled names like "Linus Dashboard - Chambre Toutes les lumières - Chambre" in notifications/modals. Stripped the redundant suffix from both en.json and fr.json across all 12 affected translation key pairs — the device name is now the only thing carrying location. Also added a real dimmable light (salon_principal, via level/set_level in fake_house.yaml) to the test fixture: every other fake light was plain on/off, so light.py's brightness/color aggregation had nothing to actually exercise in this environment. Caught two real HA template quirks while wiring it up and verified live: the modern `template:` light schema uses `level`, not `level_template` (that's the legacy `light: platform: template` key); and HA calls the `set_level` action *instead of* `turn_on` when brightness is passed to turn_on (not both), so set_level has to also flip the underlying boolean on itself. Verified live end-to-end: group turn_on(brightness=X) with nothing on turns every member on (brightness applies to the one that supports it); an adjustment with a mix of on/off members only touches the already-on one, leaving onoff-only siblings alone — the exact Linus Brain smart- filtering behavior this was meant to port. * fix: point at Linus Dashboard's own presence/light-group entities, not Brain's Linus Brain's AreaLightGroup and PresenceDetectionBinarySensor were just removed (superseded by this integration's own native area/floor/ global equivalents — see the Linus Brain PR). Two popups still built entity_ids for the old linus_brain_presence_detection_*/ linus_brain_all_lights_* pattern directly (bypassing EntityResolver), which would now silently resolve to nothing: - LinusBrainAreaPopup.ts: its presence/all-lights status and control cards now read Dashboard's own binary_sensor.linus_dashboard_ presence_detection_area_*/light.linus_dashboard_all_lights_area_*. Also dropped presence from the hasLinusBrain gating check — a Dashboard-native entity existing no longer signals Brain is installed, only the activity sensor still does that. - ActivityDetectionPopup.ts: the presence-group member lookup (used to catch sensors that don't match the standard device_class filters) now reads Dashboard's presence group instead of Brain's. Also adds the hide_groups_from_voice_assistants translation that was missing from the config flow (en.json/fr.json data + data_description). * fix: de-duplicate aggregate chip data source — prefer dedicated group entities AggregateChip.ts's floor/global fast path (PR #152) and this session's dedicated group entities (light.py/switch.py/fan.py/cover.py/siren.py/ binary_sensor.py) were computing the same total/active_entity_ids/icon/ color twice for light, switch, fan, cover and most binary_sensor device_classes — the chip never knew the dedicated entity existed, so it kept reading the generic hidden counting sensor even when a richer, controllable equivalent was sitting right next to it. - AggregateChip.ts: getAggregateSensorId() now tries the dedicated group entity first (light.linus_dashboard_all_lights_*, etc., or binary_sensor.linus_dashboard_{device_class}_* for classes that have one), only falling back to the generic sensor for climate/ media_player (no dedicated group — no simple on/off control semantics) and binary_sensor classes that don't get their own group (motion/presence/occupancy, folded into the presence composite instead). The two sources report their count differently (the generic sensor's own state IS the count; a group entity's state is on/off, count is in active_entity_ids), so the content template branches on which one was found. - sensor.py: _build_aggregate_sensors now only generates the domain-level (no device_class) bucket for GENERIC_DOMAIN_LEVEL_DOMAINS (climate, media_player, binary_sensor — binary_sensor has no "every class regardless of type" dedicated group). Device_class buckets stay unrestricted for every domain, since they're still the only source for combinations without a dedicated group (cover-by-type, binary_ sensor's presence-related classes). Also renamed the sensor's `entity_ids` attribute to `entity_id` (ATTR_ENTITY_ID) for consistency with every other group-shaped entity in this integration. - __init__.py: added async_cleanup_orphaned_aggregate_sensors, called before platform setup — anyone already running the old (still-) restricted set would otherwise keep 11 permanently-unavailable sensors (light/switch/fan/cover's domain-level bucket) in the registry forever, same footgun as the Linus Brain cleanup. Verified live against the ha-test fake house: hidden sensor count went from 33 to 22 (exactly the 11 light/switch/fan/cover domain-level entries removed, climate/media_player/binary_sensor untouched), the cleanup routine logged and removed all 11 orphans on restart with no errors, and all 39 dedicated group entities are unaffected. * feat: group-control tile for switch/fan/cover/siren, voice-hide on rebuild, docs+version Area-scope popup group-control tile (renamed getLinusBrainEntity -> getGroupEntity / buildLinusBrainSection -> buildGroupControlSection, since it resolves Dashboard-native and Magic Areas entities too, not just Brain's): - entityResolver.ts: added resolveGroupEntity(), one generic method for switch/fan/cover/siren rather than four near-identical ones — none of them have a Magic Areas equivalent, unlike light/climate. - AggregateChip.ts: getGroupEntity() now resolves switch/fan/cover/siren via the new resolver method, using the existing DEDICATED_GROUP_DOMAINS slug map. - AggregatePopup.ts: buildGroupControlSection() picks a domain-appropriate tile feature (light-brightness / fan-speed / cover-open-close + cover-position), instead of hardcoding light-brightness. - PopupFactory.ts: CoverPopup and the generic AggregatePopup path (which also handles switch/fan/siren) were hardcoding groupEntity: null, discarding it before it ever reached the popup — now forwarded. Voice-assistant hiding now also runs after dynamic rebuilds, not just once at config entry setup: previously an area/entity added after startup (new area, entity moved in, ...) got a fresh group entity that was never hidden, silently exposing it to voice assistants by default even with the option enabled. async_hide_group_entities_from_voice_ assistants moved its option-check inside itself and is now called from every platform's _rebuild() after adding new entities, not just once from async_setup_entry. Housekeeping: bumped to 1.7.0-beta.1 (manifest.json, package.json, package-lock.json) for this feature; added "siren" to manifest.json's after_dependencies (missed when siren.py was added); documented the new native group entities in the README FAQ (previously undocumented anywhere). * fix: voice-assistant hiding was silently broken since it was first added Found while live-verifying the previous commit's "hide on rebuild" fix: async_listed_assistants (imported from homeassistant.components. homeassistant.exposed_entities) doesn't exist on this HA version — the whole function has always been raising ImportError and bailing out immediately, silently logging a warning and never hiding a single entity, at setup or on rebuild. Confirmed by checking core.entity_ registry directly before/after: 0 Linus Dashboard entities had should_expose set on any assistant despite the option being on. HA's exposed_entities module replaced that function with a plain KNOWN_ASSISTANTS constant at some point; async_expose_entity itself was never affected. Imported it separately from async_expose_entity so a future rename of one can't silently take the other down with it again, same failure mode that caused this in the first place. Verified live: after the fix, all 90 Linus Dashboard entities in core.entity_registry now correctly show should_expose: false for conversation/cloud.alexa/cloud.google_assistant. * fix: add missing binary_sensor/siren entries to Helper's icon fallback map Helper.ts's #DOMAIN_ACTIVE_STATES (the client-side fallback AggregateChip uses for area scope, or when a server-side entity is momentarily unavailable) never got binary_sensor/siren added, even though aggregate.py's DOMAIN_ACTIVE_STATES/DOMAIN_ICONS — the source of truth this mirrors — already had both. Without an entry, getIcon() silently fell through to "Path A" (HA's own per-entity icon state map), which the surrounding comment already documents as unreliable for a group of entities in mixed states rather than a single entity — exactly the case that matters now that binary_sensor-by-device_class and siren groups exist as real AggregateChip targets. colorMapping in variables.ts already had full coverage for both domains, so only getIcon's map needed this. * feat: use dedicated group entities for area-scope chip rendering too Re-checked the "AreaView/FloorView recompute client-side" concern from the original plan by actually reading createAreaScopeChips/ createFloorScopeChips (utils.ts) — they don't recompute anything themselves, they just existence-check then delegate straight to AggregateChip, which was already the single shared renderer. So that specific worry didn't apply. But it did surface a real, adjacent one: getAggregateSensorId() unconditionally returned null for area scope, forcing every area chip through the slower client-side per-entity Jinja template path (Helper.getIcon/getIconColor/getContent iterating every raw entity) even though light.py/switch.py/fan.py/cover.py/ siren.py/binary_sensor.py's dedicated group entities have the exact same icon/color/active_entity_ids attributes at area scope as they do at floor/global — that null was only ever correct because dedicated group entities didn't exist yet when this method was first written. Now tries the dedicated group entity at area scope first, same as floor/global, and only returns null if none exists (climate/ media_player, or a binary_sensor device_class without its own group — same domains that already fell back to client-side at floor/global). The generic hidden counting sensor has no area tier at all (sensor.py only builds floor/global), so area scope skips that fallback tier entirely rather than trying entity_ids that could never exist. * refactor: de-duplicate per-domain conditional chip building, fix stale comments Reviewed every remaining AggregateChip caller (HomeAreaCard, ControllerCard, StandardDomainView, HomeView, AggregateView, SectionBuilder, deviceClassHelper) for cleanup opportunities. Two had real duplication: - HomeAreaCard.ts::getChipsCard: the health/window/door/cover/climate/fan badge chips all repeated the identical "wrap an AggregateChip in a ConditionalChip watching these entities for state on" shape 6 times. Extracted to buildConditionalDomainChip(). - HomeView.ts::createSectionCards: the per-floor light/climate/fan/cover chips repeated "check entities exist on this floor, then build the chip" 5 times, including an inline IIFE for the no-device_class cover case. Extracted to #buildFloorScopeChipIfPresent(). Also fixed stale "(Linus Brain only)" comments in HomeAreaCard.ts: resolveAllLights has been Dashboard-native-first (not Brain at all) since this session's earlier work; resolveAreaState/ resolveLightControlSwitch are still Brain-first but the comment ignored their Magic Areas fallback. The other 5 files were left alone on purpose: - ControllerCard.ts / StandardDomainView.ts / AggregateView.ts: already clean, no meaningful duplication. - SectionBuilder.ts: doesn't call AggregateChip at all — a separate, more generic badge builder using Helper.getIcon/getIconColor directly (already benefits from this session's earlier binary_sensor/siren fix to that shared function). Not redundant with AggregateChip, serves a different (caller-provided entity list vs domain+scope auto-discovery) purpose. - deviceClassHelper.ts: uses require() instead of ES imports (likely dodging a circular-import with Helper.ts) and does its own dynamic device_class discovery instead of the fixed DEVICE_CLASSES list other views use. Both pre-existing and working; changing either without a real build to verify against (no Node in this environment) is a risk not worth taking for a cosmetic win. * feat: Turn All On/Off buttons target the dedicated group entity when one exists AggregatePopup's control buttons called light.turn_on/off (etc.) with every raw member entity_id listed individually. For domains with a dedicated group entity (light/switch/fan/cover/siren), that's strictly worse than just targeting the group entity itself: more data in the service call, and it bypasses the group's own control method entirely even though that's exactly what already exists to handle this. Reuses the resolution AggregateChip already did for its own rendering (getAggregateSensorId's isDedicatedGroup) instead of re-deriving it — threaded through PopupFactory as dedicatedGroupEntity alongside the existing (area-only) groupEntity field. buildControlButtons targets [dedicatedGroupEntity] instead of entity_ids when present, falling back to the raw list unchanged for climate/media_player (no dedicated group ever exists for either). No behavior change for the bare on/off case itself: turn_on() with no kwargs isn't treated as an "adjustment" by any group's smart-filtering (that only kicks in for brightness/percentage/position/preset_mode adjustments), so it still targets every member either way — this is a pure simplification of the request being sent, not a functional change. * fix: stray */ inside a JSDoc comment broke the Rspack build A comment mentioning `_floor_*/_global` closed its own /** block early at the literal */ — everything after that point (including the dedicatedGroupEntity field declaration) parsed as garbage, which is why the actual error surfaced many lines later as a confusing 'Expected {, got interface' on the *next* interface. Build & Smoke Tests caught it (Rspack actually compiles the bundle); TypeScript & ESLint didn't flag it because that job's steps are continue-on-error: true — worth remembering it isn't a reliable green signal on its own, only Build & Smoke Tests actually proves the file parses. * fix: floor/area drill-down separators lost dedicated-group Turn All targeting buildFloorSeparator/buildAreaSeparator built the heading's tap_action via a separate PopupFactory.createPopup(...) call that hardcoded groupEntity and dedicatedGroupEntity to null, instead of reusing the AggregateChip it builds right next to it for the badge — which already resolves both correctly for that floor/area. Net effect: opening a floor/area popup by drilling down from its parent (global or floor) popup silently fell back to targeting every raw member entity on Turn All On/Off, instead of the single dedicated group entity, even though opening that same floor/area popup directly (from its own top-level chip) used the group entity correctly. Fix: use the already-built chip's own tap_action as the heading's tap_action — same popup, correctly resolved, one fewer redundant aggregate-source lookup. * feat: group control tile at every scope, history graph for sensor popups Group control tile (brightness/fan-speed/cover-position slider) previously only rendered at area scope, since it was gated on groupEntity — which getGroupEntity() only ever populates for area scope by design (floor/global route through dedicatedGroupEntity instead). Gate on dedicatedGroupEntity first (covers light/switch/fan/cover/siren at area/floor/global alike), falling back to groupEntity for domains with no dedicated group (climate, via Magic Areas) — same tile, same behavior, just no longer area-only. Also add a 24h history-graph card to sensor-domain popups (temperature, humidity, etc.), one line per entity in scope — the statistics card above it only shows the current sum/average, not the trend, which is usually the actual reason to open one of these. * fix: hide Turn All On/Off buttons when a group control tile is shown The group control tile's own toggle already turns every member on/off (plus brightness/fan-speed/cover-position via its features) — showing the Turn All On/Off buttons above it was a redundant, less capable duplicate of the same control whenever a dedicated (or Magic Areas) group entity exists for this scope/domain. Buttons now only render when there's no tile to show instead. * fix: binary_sensor group icons/translation, hidden climate/media_player aggregate visibility, popup separator consistency - compute_icon() ignored device_class entirely for binary_sensor (always the generic checkbox icon), unlike compute_color() which already had a BINARY_SENSOR_COLORS override — added the equivalent BINARY_SENSOR_ICONS map and threaded device_class through both call sites (entity_group.py's compute_group_attributes, sensor.py's generic aggregate sensor), so door/motion/window/etc. groups get an icon that actually matches instead of a generic checkbox. - binary_sensor.py's per-device_class groups (door, window, smoke, ...) used a hand-rolled translation string "{device_class}" that just echoed the raw English slug back verbatim in every language ("door" even in a French install) instead of translating anything. Dropped the custom translation_key/placeholders for this class entirely — with _attr_device_class already set and no translation_key, HA falls back to its own core per-device_class entity name (already localized) combined with the device's own name for the area/floor distinction, same mechanism every other integration relies on for this exact situation. - LinusDashboardAggregateSensor (the old generic hidden-counting sensor, which now only exists for climate/media_player/binary_sensor domain-level buckets — every other domain has its own dedicated group entity) was always diagnostic/hidden by class default. climate and media_player have no dedicated group of their own, so this hidden sensor was the *only* aggregate they had — as a hidden diagnostic entity, a user had no reason to ever notice it existed. Made domain-level buckets (device_class_filter is None) visible and non-diagnostic; device_class-specific buckets are unchanged. - AggregatePopup's "Individual Controls" section heading only appeared when an area had more than one entity, making a 1-light area's popup look structurally different from a 3-light area's for no functional reason. Now shows whenever there's at least one entity. * fix: un-hide climate/media_player/binary_sensor domain-level aggregate sensors Bumping LinusDashboardAggregateSensor's visible_default in the previous commit only affects entities created from now on — HA applies entity_registry_visible_default once, at first creation, and never retroactively on an existing registry entry. Anyone who already ran an earlier version has these stuck with hidden_by="integration" forever, so the class-attribute fix alone would look like it did nothing. Explicit one-time migration, same exact-unique_id-set technique as the existing orphaned-sensor cleanup, clears hidden_by only when it's still "integration" (never touches a user's own manual hide). * feat: dedicated group entities for climate and media_player (full domain parity) Adds climate.py/media_player.py, following the exact same nested area/floor/ global pattern as light/switch/fan/cover/siren (entity_group.py's build_nested_domain_groups, unchanged). v1 "dumb" aggregation, matching the documented incompleteness already accepted for fan/cover: - ClimateGroup: hvac_mode/supported_features are an intersection across members (a mode/feature the group offers must actually work everywhere); current/target temperature are simple averages; min/max_temp is the widest range any member supports. turn_on/turn_off go through the universal set_hvac_mode primitive rather than the optional TURN_ON/ TURN_OFF feature, so the group's own turn_on/off always works regardless of member support — TURN_ON/TURN_OFF are unconditionally added to supported_features for the same reason. - MediaPlayerGroup: play/pause/stop/turn_on/turn_off forwarded blindly to every member (a member lacking support just logs a warning, doesn't fail the group); volume_level is a simple average. source/source_list deliberately not aggregated — no sensible single value across a mixed fleet of players. Consequences of no longer deferring these two domains: - sensor.py's GENERIC_DOMAIN_LEVEL_DOMAINS drops to just ("binary_sensor",) — climate/media_player's old hidden counting sensor is now dead weight, added to __init__.py's _DEAD_GENERIC_SENSOR_DOMAINS cleanup instead of needing to stay un-hidden (_UNHIDE_DOMAIN_LEVEL_SENSOR_DOMAINS trimmed to just binary_sensor accordingly). - AggregateChip.ts's DEDICATED_GROUP_DOMAINS and AggregatePopup.ts's GROUP_TILE_FEATURES gain climate/media_player entries so the frontend picks up the new dedicated groups the same way it already does for light/ switch/fan/cover/siren (Turn All button targeting, quick-control tile). Also fixes a real, previously-invisible bug found while building this: get_area_device_info()'s own docstring said area-scoped Linus Dashboard devices must be explicitly placed in their real HA area via device_registry (since suggested_area risks creating a duplicate area) — but nothing ever actually made that call, for any domain, since the very first area-scoped group entity shipped. Every "Linus Dashboard - <Area>" device has been sitting with no area assigned this whole time; none of the "open Settings > Areas > Salon and see its Linus Dashboard device" UX the plan described was actually happening. Added entity_group.py's ensure_area_device_placed() (called from build_nested_domain_groups and binary_sensor.py's two custom area loops) to actually do it. Placing these devices in their real area makes the self-inclusion risk entity_group.py's CRITICAL PATTERN comment warns about newly real on the frontend too (it was already guarded against on the backend) — a group entity's own device now genuinely sits in the area it's a member of, so without a matching exclusion, e.g. light.linus_dashboard_all_lights_area_salon would show up as an extra "individual" tile in the Salon light popup, indistinguishable from a real light. Helper.ts already excluded Magic Areas/ Linus Brain's own entities from area/domain entity lists for the exact same reason — added Linus Dashboard's own platform to that same exclusion. * debug: log ensure_area_device_placed to diagnose why area_id isn't sticking * chore: drop debug logging now that ensure_area_device_placed is verified working * fix: MediaPlayerPopup never received dedicatedGroupEntity at all PopupFactory's createMediaPlayerPopup hardcoded groupEntity: null (correct — no Magic Areas media_player group exists) but never forwarded dedicatedGroupEntity, unlike every other popup builder. Combined with MediaPlayerPopup's own buildControlButtons override (custom "Play All"/ "Pause All" conditional cards, written before dedicatedGroupEntity existed) always targeting the raw entity_ids directly, media_player never picked up its new group entity at all: no quick-control tile, and Play All/Pause All never targeted the group even when it existed and was available. Threading dedicatedGroupEntity through now makes AggregatePopup's own tileEntity gating do the right thing automatically — the group tile shows when the entity is available (with its own play/pause/stop/volume, added to GROUP_TILE_FEATURES), and MediaPlayerPopup's override is skipped entirely in that case (same "don't show a redundant duplicate control" gate every other domain already gets), falling back to Play All/Pause All against entity_ids only when there's no group tile to show instead. * clean: dedup media_player active states, remove dead buildFloorAreaSeparators media_player.py's own _ACTIVE_STATES tuple was a hand-copied duplicate of aggregate.py's DOMAIN_ACTIVE_STATES["media_player"] — two lists that would only stay in sync by remembering to update both. Import it instead. buildFloorAreaSeparators had zero callers: buildIndividualCardsHierarchical (the only place that builds floor/area separators) already covers both cases it tried to handle — global scope gets buildFloorSeparator per floor plus buildAreaSeparator per area inside it, floor scope gets just buildAreaSeparator — via two separate, actually-wired methods. This third one duplicated the same two scenarios in a single parameterized function that never replaced them, just sat unused alongside. * refactor: reuse HA core's group.util helpers instead of hand-rolled reduction Architecture request: stay as close to HA's own patterns as practical for easier long-term maintenance. homeassistant.components.group.util ships with every HA install (it's what HA's own light.group/switch.group/etc. platforms are built on) and provides exactly the "reduce this attribute across a list of states" primitives light.py/climate.py/media_player.py were each hand-rolling with their own for-loops: - light.py: brightness averaging -> reduce_attribute (its default reducer, mean_int, is the same sum/len/int() math we did by hand). Color attributes (color_temp/rgb/hs/rgbw/rgbww/effect) -> a small _agree_or_none built on attribute_equal + find_state_attributes, preserving the existing "show nothing rather than an averaged color no member is actually displaying" behavior rather than switching to HA's own group.light (which averages colors via mean_tuple/mean_circle) — that's a real behavior difference, not just an implementation detail, so kept as-is rather than silently changed. min/max_color_temp_kelvin -> reduce_attribute(reduce=min/max). - climate.py: current/target temperature -> reduce_attribute with a new shared mean_float reducer (group.util's default, mean_int, truncates — wrong for temperature). min/max_temp -> reduce_attribute(reduce=min/max). - media_player.py: volume_level -> reduce_attribute(reduce=mean_float). supported_features/supported_color_modes/hvac_modes intersection logic is deliberately NOT switched to HA's own group platforms' approach (which union features rather than intersect) — that's a considered difference covered in each file's own docstring, not something "closer to HA" should paper over. hvac_mode/media_player state aggregation also stays manual: group.util's helpers reduce *attributes*, and those are the state itself. mean_float added once in entity_group.py (shared by climate.py and media_player.py) rather than duplicated in both. * feat: Magic Areas fallback for media_player group, matching climate media_player.py's native group entity is still tried first at every scope (getAggregateSensorId, unchanged) — this only kicks in as a secondary fallback if that entity happens to be unavailable, same role resolveClimateGroup already plays for climate. Magic Areas does have a real media_player_group entity (wraps HA core's own group.media_player. MediaPlayerGroup) to fall back to, unlike media_player_control (a boolean switch, not a media_player entity). Filling this in for consistency with climate rather than leaving an asymmetry where one dedicated-group domain gets a fallback and the other doesn't, for no principled reason. * refactor: multiply-inherit HA core's group.<domain> classes for state computation Goes further than the previous group.util-based refactor: light.py, switch.py, fan.py, cover.py, and binary_sensor.py's per-device_class groups now directly inherit HA core's own homeassistant.components.group.<domain> class (e.g. group.light.LightGroup) alongside NestedGroupMixin, delegating async_update_group_state() entirely to HA instead of maintaining our own copy of that computation. Same class Magic Areas builds its own groups on. NestedGroupMixin gained _sync_ha_group_state(domain, device_class=None) to drive this: keeps self._entity_ids (HA's own classes read this name, not our _member_entity_ids) in sync, calls async_update_supported_features per member for the platforms that track features incrementally (fan/cover), calls the inherited async_update_group_state(), then layers our own entity_id/total/active_entity_ids/icon/color on top via compute_group_attributes. async_added_to_hass no longer calls super() — GroupEntity's own version (further up these classes' MRO) sets up an *undebounced* subscription, which would defeat NestedGroupMixin's own debounce (added for PR #152's "flatline" performance issue). Two HA-init quirks discovered and documented in light.py's module docstring (same pattern repeated in every file below): - Entity._name_internal checks `hasattr(self, "_attr_name")`, not truthiness. HA's own __init__ always sets it, permanently blocking our translation_key-based naming unless explicitly `del`eted (not nulled) right after calling it. - HALightGroup specifically also sets a class-level _attr_icon default ("mdi:lightbulb-group") — Entity.icon has the same hasattr pattern, and when non-None it overwrites our own dynamic on/off icon (which lives inside extra_state_attributes, same "icon" key, different mechanism) in HA's flattened state attributes. Explicitly nulled at the instance level for light.py only — no other converted platform sets a default icon. Deliberately kept as our own overrides, not HA's equivalents, where we have a considered behavior difference: - light.py/fan.py: async_turn_on's smart on-members-only adjustment filtering (HA's own forwards blindly to every member regardless of current on/off state). - light.py: supported_features narrowed to the intersection across members and min/max_color_temp_kelvin computed from real members (HA's own unions features and hardcodes a fixed 2000-6500K range regardless of members). - fan.py: preset_mode support entirely, plus async_set_percentage/ async_set_preset_mode's on-members-only filtering — HA's own FanGroup doesn't track preset_mode at all (missing from its SUPPORTED_FLAGS). - cover.py: async_set_cover_position's on-members-only filtering. Inherited as-is (no override) where HA's own behavior is equal or better: - switch.py: turn_on/turn_off (identical blind-forward logic either way). - fan.py: gains oscillate/direction support for free (never had it before). - cover.py: open/close/stop_cover and all _tilt variants — HA's own already forward only to members supporting the relevant feature, a safety property our own hand-rolled versions of these (not set_cover_position, which still has real on-members-only logic worth keeping) didn't have. climate.py (no HA core group.climate exists) and media_player.py (HA's own group.media_player doesn't inherit GroupEntity at all and uses a different internal attribute name — a worse fit for this pattern) are unchanged, already at the group.util level from the previous refactor. binary_sensor.py's PresenceGroup (a composite OR-gate across multiple domains/device_classes, not a single homogeneous list) has no HA equivalent either, also unchanged. Verified via live restarts of the ha-test fixture after each file: no tracebacks, and correct friendly_name/icon/device_class/active_entity_ids for light/switch/fan/cover/binary_sensor groups, including a live light turning on and the group correctly reflecting active_entity_ids/icon/color. * fix: use native presence entity in ActivityDetectionPopup without Brain The Presence status chip and the presence line in the history graph were both gated on isLinusBrain, even though presenceSensorEntity (binary_sensor.linus_dashboard_presence_detection_area_X) has been native since presence moved out of Brain — it works with or without Brain installed. Only the genuinely Brain-only pieces (Activity chip/duration/ details/stats, all reading sensor.linus_brain_activity_*'s own attributes) still require it. * fix: don't show an empty brightness slider on non-dimmable light groups light-brightness tile feature renders empty when the entity's supported_color_modes is just ["onoff"] — nothing to bind the slider to. HomeAreaCard's light tile and AggregatePopup's group control tile both assumed every light group supports it. Added Helper.lightSupportsBrightness and made both conditional on it; when unsupported, features falls back to [] rather than a broken slider — the tile's own tap-to-toggle still turns the group on/off either way, no dedicated "on/off buttons" tile feature exists for light in HA to use instead. * feat: aggregate entity for every device_class, not a fixed short list binary_sensor.py: motion/presence/occupancy were excluded from _discover_generic_device_classes, so only the composite presence_detection group existed for them — no way to see "how many motion sensors are triggered right now" specifically, only "is anyone home". Removed the exclusion; a sensor can feed both the composite and its own device_class group, they answer different questions. sensor.py: NUMERIC_DEVICE_CLASSES was a fixed 4-item list (temperature/ humidity/illuminance/battery), deliberately short to avoid entity sprawl. Replaced with _discover_numeric_device_classes (same dynamic-discovery approach binary_sensor.py already used), gated on the state actually being numeric (skips enum/timestamp/date device classes outright, and anything else whose live value doesn't parse as a float) rather than a hand-picked allowlist. Fixed a real bug found while making this dynamic: the aggregation mode was hardcoded to "measurement" for every device_class ("battery/illuminance/ humidity/temperature are all measurement in practice" — true for exactly those four, not for whatever else gets discovered now). A sum-type device_class like energy/water/gas would have been silently averaged instead of summed. Now reads each device_class's real state_class from a sampled member and passes that through instead. Also dropped the hand-written translation_key/placeholders for numeric sensors (same reasoning as binary_sensor.py's per-device_class groups, already fixed for the same reason) — no way to pre-write a translation for a device_class discovered at runtime, so _attr_device_class + no translation_key falls back to HA's own core per-device_class sensor name instead. Removed the now-dead numeric_* translation strings (including battery's "(min)" annotation, which doesn't have a generic equivalent — minor label simplification, not worth blocking dynamic discovery over). * feat: activity chip falls back to native presence entity, not a static icon Without Linus Brain, ActivityDetectionChip showed a permanently static grey "mdi:home-search" icon — now falls back to binary_sensor.linus_dashboard_presence_detection_area_X (an "entity" type chip, same as the Brain branch) when this area has one, which HA renders with its own dynamic device_class=occupancy icon (changes with on/off) automatically — no template/state_attr wiring needed for that part. Also gave PresenceGroup its own icon/color in extra_state_attributes (compute_icon/compute_color with device_class="occupancy"), matching every other group entity in this integration — it was the one exception. Reuses the exact member scan/state dict _recompute already builds; deliberately does not reference the per-device_class motion/presence/occupancy groups binary_sensor.py also builds from the same raw sensors, so there's no dependency between this composite and them — verified live (state history shows the normal ~30-60s fixture motion cycle, not rapid flapping). * refactor: consolidate LinusBrainAreaPopup into ActivityDetectionPopup AreaView showed two separate chips whenever Brain was installed: Activity Detection (always shown, localized, with history graphs and the sensor list) and a second "Linus Brain" chip opening LinusBrainAreaPopup — a near-duplicate covering the same activity/presence/duration/stats ground in a different flat-cards layout with hardcoded English strings (no Helper.localize at all). Two popups answering the same question two different ways depending on installed integrations, rather than one model consistently augmented when Brain is available. Everything LinusBrainAreaPopup showed beyond what ActivityDetectionPopup already covers was: automatic lighting switch and a device config link — both genuinely Brain-only, no other home in the UI. Folded those into a new Controls section (Brain-gated) in ActivityDetectionPopup; dropped the redundant all-lights control (the area's own light card already provides it) and the "Time Occupied" stat (already shown in the existing Statistics section, same entity). Removed the LinusBrainAreaPopup chip/import from AreaView and deleted the now-dead file. * fix: health sensor never recomputed after initial boot-time snapshot LinusDashboardHealthSensor computed its unavailable-entity count/list once in __init__ and had no state-change subscription at all, unlike every other aggregate/group entity in this integration. In practice this froze whatever a handful of entities happened to report during the first second of HA startup (many platforms briefly report "unknown" before their first real state lands), and never updated again — so it could permanently list entities as unavailable long after they came back online, which is what the user was seeing on the test environment. Gives it the same debounced async_track_state_change_event pattern already used by LinusDashboardAggregateSensor in this file. Area-level sensors track their raw scanned entities directly; floor/global-level sensors track their child sensors' entity_ids and read each child's own already-computed list (same nested-scope pattern as the rest of the integration) instead of re-scanning raw entities. Updated _build_health_sensors to wire up tracked_entity_ids/nested instead of precomputing unavailable lists at build time, and switched the global tier's build gate from area_group_ids to floor_group_ids for consistency with every other platform's nested-hierarchy gate. Verified live on the ha-test fixture: after restart, all unavailable_area_*/ floor_*/global sensors settled to entity_id=[]/total=0 within the 0.1s debounce window, correctly reflecting that no fixture entity is actually unavailable right now — previously they stayed stuck reporting the transient boot-time "unknown" entities as permanently unavailable. * fix: show a confirmation card when the Unavailable view has nothing to show UnavailableView built its sections entirely from per-area unavailable-entity scans; when nothing was unavailable anywhere, createSectionCards() returned an empty array and the view rendered completely blank — reported live on the test environment right after the health-sensor fix made "zero unavailable" an actually-reachable state. Adds the same empty-state message TagsView already uses for "no labels": a mushroom-template-card with a green check icon, under new entity.text.unavailable_view translation keys (en/fr). Verified via tsc --noEmit, eslint, and rspack build:prod (node:24 container, no errors) — no browser access in this environment to visually confirm rendering, so this is compile-verified, not screenshot-verified. * test: add backend unit tests for aggregate/entity_group/health sensor, fix numeric aggregation mode bug found by them No Python test infrastructure existed in this repo at all — every backend change this session (multi-inheritance group refactor, dynamic device_class discovery, health sensor live-update fix) had only been verified by hand against the ha-test fixture. Adds a pytest suite covering the parts that don't need a full pytest-homeassistant-custom-component harness: the pure aggregate.py helpers, entity_group.py's exclusion parsing and group-attribute builder, and LinusDashboardHealthSensor's recompute logic (including a direct regression test for the boot-time-snapshot bug fixed earlier this session). Uses lightweight MagicMock(spec=HomeAssistant) fixtures, same convention as the sibling Linus Brain project's tests/conftest.py, rather than pulling in the full plugin. Writing test_resolve_numeric_aggregation_mode_total_increasing_sums caught a real, live bug in resolve_numeric_aggregation_mode(): it compared its state_class parameter against SENSOR_STATE_CLASS_TOTAL(_INCREASING), sets that actually hold device_class names (ported from the frontend's device_class-keyed heuristic in variables.ts/Helper.ts, a different check entirely) — a real state_class value like "total_increasing" can never match a device_class name, so the sum branch was dead code and every energy/water/gas/monetary numeric aggregate was silently averaged instead of summed. Fixed by comparing state_class directly against HA's own SensorStateClass values ("total"/"total_increasing") and removing the misleading sets. 32/32 tests pass. Verified live on ha-test: temperature aggregates (measurement -> average) unaffected; no fixture entity with a total/ total_increasing state_class exists to also confirm the sum path live, so that path is confirmed by the new unit test only. * docs: regenerate CHANGELOG.md and draft RELEASE_NOTES.md for the beta CHANGELOG.md was stuck at 1.4.0-beta.8 (2026-01-05) despite several already-tagged releases since (1.5.x, 1.6.0-beta.1) — regenerated via scripts/generate-changelog.sh to catch it up to every existing tag. It still won't show an entry for the current unreleased work (1.7.0-beta.1 in package.json) since the generator only reads from git tags and no tag exists for it yet — that entry appears once an actual version tag is cut, which normally happens after this branch merges to main, not from here. RELEASE_NOTES.md drafted via scripts/generate-release-notes.sh (70 commits since the 1.6.0-beta.1 tag) then hand-written into real English/French descriptions and a beta-tester checklist (the generator only produces raw commit-message dumps and TODO placeholders), then run through scripts/format-release-notes.sh for the collapsible GitHub layout. Not done: version bump, git tag, or push — those are separate, more consequential steps (tagging decides the actual release number and triggers the GitHub Actions release/Discord workflow once pushed) left for an explicit go-ahead rather than bundled into this docs update. * test: cover UnavailableView's empty-state fallback and its regression guard No frontend test existed for this view at all. Adds three cases: the new confirmation-card path when nothing is unavailable anywhere (including the edge case of zero floors), and a guard that the real per-floor/per-area cards still render — not the confirmation card — when something actually is unavailable, so the empty-state addition can't silently swallow real content. Also gives static confirmation that the English translation renders correctly (all_good_title/all_good_subtitle in en.json) — the user had only visually checked the French rendering; no browser access here to screenshot either language, so this is compile+unit-test verified, not visually verified. --------- Co-authored-by: root <root@srv544245.hstgr.cloud> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Linus Dashboard is great but I find that my tablet struggles with it. Part of it is all my devices, part of it is the tablet is not very good. Hoping to make LD a little quicker. Also did some dev changes since it was hard to get this repo initialized and ready to work in.
sensorplatform creates server-side aggregate sensors per domain/floor, reducing home view template evaluations from ~60 to ~3Changes
Server-side aggregate sensors
aggregate.py: constants and pure computation functions (active states, icons, colors)sensor.py: diagnostic sensors, event-driven with 100ms debounce, hidden from default UIFrontend
AggregateChipuses server-side sensor when available (state_attr(...)instead of inlining 100 entity IDs), falls back gracefully if sensor doesn't existnow()fromgetLastChangedTemplate— was forcing allrelative_time()cards to re-evaluate every minutePromise.all()indexOf()in comparator with pre-builtMapexposedDomainIds.some(...)inner-loop calls with a pre-computedSetObject.keys(DEVICE_CLASSES).includes()with a module-levelSetentity_idarrays to AggregateChip and UnavailableChip for faster Mushroom subscriptionsPerformance
Test plan
Uh there was no real test plan. I put it in my personal HA with ~2000 entities / ~135 devices and it seemed a little faster? Probably a bit of placebo but I expect this to be a first pass for performance improves