Skip to content

Comments

Add exclude from All Areas toggle and floor-based aggregate sensors#377

Merged
Hankanman merged 2 commits intomainfrom
feature/exclude-from-all-areas-and-floor-aggregation
Feb 22, 2026
Merged

Add exclude from All Areas toggle and floor-based aggregate sensors#377
Hankanman merged 2 commits intomainfrom
feature/exclude-from-all-areas-and-floor-aggregation

Conversation

@Hankanman
Copy link
Owner

@Hankanman Hankanman commented Feb 22, 2026

Summary

Implements #335 — two features for multi-area management:

  • Per-area "Exclude from All Areas" toggle — prevents outdoor/utility areas (garages, driveways, backyards) from contributing to the All Areas aggregate sensors. Configurable in the behavior step of the config flow.
  • Floor-based aggregate sensors — auto-generated from HA's floor registry, providing per-floor occupancy probability, decay, prior, presence confidence, environmental confidence, and binary occupancy sensors.

Changes

File Change
const.py Added CONF_EXCLUDE_FROM_ALL_AREAS, DEFAULT_EXCLUDE_FROM_ALL_AREAS, bumped CONF_VERSION to 18
data/config.py Loads exclude_from_all_areas in AreaConfig._load_config()
area/all_areas.py Added _included_areas() filter to AllAreas, new FloorAreas class, shared _avg() helper
area/__init__.py Exported FloorAreas
coordinator.py Floor aggregator discovery (_build_floor_aggregators), lifecycle integration
sensor.py Widened type hints for FloorAreas, floor entity creation, updated routing logic
binary_sensor.py Same pattern as sensor.py
config_flow.py Added exclude toggle to behavior step
strings.json / translations/en.json UI text for exclude setting
tests/test_area_all_areas.py Fixed mock areas for exclude_from_all_areas attribute

Notes

  • No migration needed — missing exclude_from_all_areas key defaults to False
  • Floor assignments changing in HA requires an integration reload (v1 limitation)
  • All 1504 tests pass, lint clean, 86.96% coverage

Test plan

  • Configure an area with "Exclude from All Areas" enabled, verify it doesn't appear in All Areas aggregate values
  • Assign areas to floors in HA, verify floor aggregate sensors are created
  • Verify floor sensors show correct aggregated values for their floor only
  • Verify excluded areas still appear in their floor's aggregate sensors
  • Reload integration after changing floor assignments, verify sensors update

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Floor-level occupancy aggregation sensors to monitor occupancy by floor.
    • New area setting to exclude specific areas from "All Areas" aggregate sensors.
  • Chores

    • Migration support for the new setting and coordinator floor aggregators.
    • Tests and utilities updated to support the new area configuration.
  • Documentation

    • UI labels and translations added for the exclude-from-all-areas option.

…335)

Implements two features for multi-area management:

1. Per-area "Exclude from All Areas" toggle that prevents outdoor/utility
   areas (garages, driveways) from contributing to All Areas aggregation.

2. Floor-based aggregate sensors auto-generated from HA's floor registry,
   providing per-floor occupancy probability, decay, prior, and binary
   occupancy sensors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

Walkthrough

Adds floor-scoped aggregation via a new FloorAreas class and floor aggregators in the coordinator; introduces an exclude-from-all-areas flag (config, UI, migration, and AreaConfig support) and extends sensors/binary sensors to support floor-level aggregates and exclude behavior.

Changes

Cohort / File(s) Summary
Floor Aggregation Core
custom_components/area_occupancy/area/__init__.py, custom_components/area_occupancy/area/all_areas.py
Exports new FloorAreas; implements FloorAreas class; adds _avg() helper; AllAreas now filters excluded areas via _included_areas() and exposes areas().
Coordinator Integration
custom_components/area_occupancy/coordinator.py
Adds _floor_aggregators field, _build_floor_aggregators() initialization, lifecycle handling, and get_floor_aggregators() public method to manage per-floor FloorAreas.
Sensors & Binary Sensors
custom_components/area_occupancy/sensor.py, custom_components/area_occupancy/binary_sensor.py
Accepts FloorAreas in all_areas unions; derives floor-specific area names (floor_<id>), creates floor-aggregated sensors at setup, and adjusts entity initialization/aggregation logic to handle floor aggregates.
Configuration, Defaults & Migration
custom_components/area_occupancy/const.py, custom_components/area_occupancy/config_flow.py, custom_components/area_occupancy/migrations.py, custom_components/area_occupancy/data/config.py
Adds CONF_EXCLUDE_FROM_ALL_AREAS and DEFAULT_EXCLUDE_FROM_ALL_AREAS; bumps CONF_VERSION 17→18; exposes flag in config flow schema; AreaConfig loads exclude_from_all_areas; adds no-op migration to v18.
Localization & Tests
custom_components/area_occupancy/strings.json, custom_components/area_occupancy/translations/en.json, tests/test_area_all_areas.py
Adds translation keys and descriptions for exclude_from_all_areas; introduces _mock_area() test helper and updates tests to use it.
Public API Export
custom_components/area_occupancy/area/__init__.py
Adds FloorAreas to module exports (__all__).

Sequence Diagram

sequenceDiagram
    participant Setup as Setup/Config Flow
    participant Coord as AreaOccupancyCoordinator
    participant FloorAgg as FloorAreas
    participant Sensors as Sensors/Binary Sensors

    Setup->>Coord: _build_floor_aggregators()
    Coord->>Coord: discover floors from area configs
    Coord->>FloorAgg: instantiate FloorAreas(floor_id, floor_name)
    Coord->>Coord: store in _floor_aggregators

    Setup->>Sensors: async_setup_entry()
    Sensors->>Coord: get_floor_aggregators()
    Coord-->>Sensors: dict[str, FloorAreas]
    Sensors->>Sensors: create per-floor aggregated sensors using FloorAreas

    Note over FloorAgg: Aggregates included areas for its floor
    Note over Sensors: Use FloorAreas for aggregated metrics (probability, decay, priors, etc.)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main features added: an exclude toggle for all-areas aggregation and floor-based aggregate sensors.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/exclude-from-all-areas-and-floor-aggregation

Comment @coderabbitai help to get the list of available commands and usage tips.

@Hankanman Hankanman self-assigned this Feb 22, 2026
@coderabbitai coderabbitai bot added config enhancement New feature or request labels Feb 22, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
custom_components/area_occupancy/sensor.py (1)

102-165: ⚠️ Potential issue | 🟡 Minor

Scope aggregated attributes to the aggregator’s area set.

For FloorAreas, extra_state_attributes currently aggregates over all coordinator areas, and for AllAreas it ignores exclude_from_all_areas. This makes floor/all-areas attributes misleading compared to the aggregated values.

🐛 Suggested fix (sensor.py)
-                area_names = self._all_areas.coordinator.get_area_names()
-                area_attrs = {}
-                for area_name in area_names:
-                    area = self._all_areas.coordinator.get_area(area_name)
-                    if area is None:
-                        continue
-                    area_attrs[area_name] = {
+                area_attrs = {}
+                for area in self._all_areas.areas():
+                    area_attrs[area.area_name] = {
                         "global_prior": area.prior.global_prior,
                         "combined_prior": area.area_prior(),
                         "time_prior": area.prior.time_prior,
                         "day_of_week": area.prior.day_of_week,
                         "time_slot": area.prior.time_slot,
                     }
@@
-                area_names = self._all_areas.coordinator.get_area_names()
                 all_decaying = []
-                for area_name in area_names:
-                    area = self._all_areas.coordinator.get_area(area_name)
-                    if area is None:
-                        continue
+                for area in self._all_areas.areas():
                     all_decaying.extend(
                         [
                             {
-                                "area": area_name,
+                                "area": area.area_name,
                                 "id": entity.entity_id,
                                 "decay": format_percentage(entity.decay.decay_factor),
                                 "half_life": entity.decay.half_life,
                             }
                             for entity in area.entities.decaying_entities
                         ]
                     )
# all_areas.py (add public accessor on aggregators)
class AllAreas:
    def areas(self) -> list[Area]:
        """Return areas included in aggregation."""
        return self._included_areas()

class FloorAreas:
    def areas(self) -> list[Area]:
        """Return areas on this floor."""
        return self._floor_areas()

Also applies to: 282-352

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/area_occupancy/sensor.py` around lines 102 - 165,
extra_state_attributes is currently iterating the coordinator's full area set,
causing FloorAreas and AllAreas to report misleading aggregates; add public
accessors areas() on AllAreas and FloorAreas (returning the included/ floor
areas and respecting exclude_from_all_areas) and change extra_state_attributes
to iterate self._all_areas.areas() (and use those Area objects directly) when
self._all_areas is set, building area_attrs from that list; for the
non-aggregate branch keep using self._get_area() but compute combined_prior and
other priors from that Area instance as before. Ensure you reference the
AllAreas.areas(), FloorAreas.areas(), Sensor.extra_state_attributes, and the
_all_areas usage when making these changes.
🧹 Nitpick comments (3)
custom_components/area_occupancy/binary_sensor.py (1)

110-112: Finish the new inline comments with periods.

Logic is fine; just adjust the new comments to be full sentences ending with a period.

♻️ Example fix
-        # Let the coordinator know our entity_id (only for per-area entities, not aggregates)
+        # Let the coordinator know our entity_id (only for per-area entities, not aggregates).

As per coding guidelines, Comments should be full sentences ending with a period.

Also applies to: 127-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/area_occupancy/binary_sensor.py` around lines 110 - 112,
Update the inline comments in the binary_sensor entity registration so they are
full sentences ending with periods: adjust the comment above the per-area
registration referencing self._handle and _get_area (the line that assigns
area.occupancy_entity_id = self.entity_id) and any similar comments in the
nearby block (around the aggregate/per-area handling, e.g., the block spanning
the later 127-133 region) to end with a period while leaving the logic
unchanged.
custom_components/area_occupancy/sensor.py (1)

73-87: Add a trailing period to the new inline comment.

Logic is fine; just make the inline comment a full sentence ending with a period.

♻️ Example fix
-        # Only for specific areas, not "All Areas" or floor aggregates
+        # Only for specific areas, not "All Areas" or floor aggregates.

As per coding guidelines, Comments should be full sentences ending with a period.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/area_occupancy/sensor.py` around lines 73 - 87, Update the
inline comment inside async_added_to_hass (the comment starting with "Only for
specific areas, not "All Areas" or floor aggregates") to be a complete sentence
by adding a trailing period; ensure the comment that references
self._area_handle and the call to self._get_area() ends with a period to follow
the project's comment-style guideline.
custom_components/area_occupancy/coordinator.py (1)

66-67: End the new inline comments with periods.

Minor style: these new comments are fragments. Please make them full sentences ending with a period.

♻️ Example fix
-        # Floor-based aggregators: dict[floor_id, FloorAreas]
+        # Floor-based aggregators map floor_id to FloorAreas.

As per coding guidelines, Comments should be full sentences ending with a period.

Also applies to: 360-361, 460-463, 655-656

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/area_occupancy/coordinator.py` around lines 66 - 67, The
inline comment on the Floor-based aggregators declaration
(self._floor_aggregators) is a fragment — update it to be a full sentence ending
with a period (e.g., "Floor-based aggregators: dict[floor_id, FloorAreas].");
apply the same change to the other similar inline comments in this module so all
comments are full sentences that end with periods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@custom_components/area_occupancy/const.py`:
- Line 33: Add a new migration handler for v17→v18 in migrations.py that mirrors
the v16→v17 pattern: detect old_version == 17, copy existing config/data and
options entries intact, insert the new CONF_EXCLUDE_FROM_ALL_AREAS key into both
the data and options structures with its default value, and return {"version":
CONF_VERSION}; reference CONF_VERSION and CONF_EXCLUDE_FROM_ALL_AREAS and reuse
the same helper logic used by the v16_to_v17 migration to ensure no other keys
are changed.

In `@tests/test_area_all_areas.py`:
- Around line 17-23: The local helper function _mock_area should be replaced
with the shared test helper create_test_area to keep test setup consistent;
update tests that call _mock_area(**overrides) to call create_test_area(...) and
map the same override keys into create_test_area's parameters or its
post-creation mutation API, remove the _mock_area function, and ensure the
created area's config.exclude_from_all_areas is set to False via
create_test_area arguments or by setting area.config.exclude_from_all_areas
immediately after creation so behavior remains identical to the original helper.

---

Outside diff comments:
In `@custom_components/area_occupancy/sensor.py`:
- Around line 102-165: extra_state_attributes is currently iterating the
coordinator's full area set, causing FloorAreas and AllAreas to report
misleading aggregates; add public accessors areas() on AllAreas and FloorAreas
(returning the included/ floor areas and respecting exclude_from_all_areas) and
change extra_state_attributes to iterate self._all_areas.areas() (and use those
Area objects directly) when self._all_areas is set, building area_attrs from
that list; for the non-aggregate branch keep using self._get_area() but compute
combined_prior and other priors from that Area instance as before. Ensure you
reference the AllAreas.areas(), FloorAreas.areas(),
Sensor.extra_state_attributes, and the _all_areas usage when making these
changes.

---

Duplicate comments:
In `@custom_components/area_occupancy/translations/en.json`:
- Around line 727-739: Remove the stray duplicate marker/comment and ensure
there are no duplicated keys in the "data_description" block; specifically
verify the entries "threshold", "decay_enabled", "decay_half_life",
"min_prior_override", and "exclude_from_all_areas" are present exactly once,
remove the duplicate_comment artifact, and commit the cleaned en.json
translation block.

---

Nitpick comments:
In `@custom_components/area_occupancy/binary_sensor.py`:
- Around line 110-112: Update the inline comments in the binary_sensor entity
registration so they are full sentences ending with periods: adjust the comment
above the per-area registration referencing self._handle and _get_area (the line
that assigns area.occupancy_entity_id = self.entity_id) and any similar comments
in the nearby block (around the aggregate/per-area handling, e.g., the block
spanning the later 127-133 region) to end with a period while leaving the logic
unchanged.

In `@custom_components/area_occupancy/coordinator.py`:
- Around line 66-67: The inline comment on the Floor-based aggregators
declaration (self._floor_aggregators) is a fragment — update it to be a full
sentence ending with a period (e.g., "Floor-based aggregators: dict[floor_id,
FloorAreas]."); apply the same change to the other similar inline comments in
this module so all comments are full sentences that end with periods.

In `@custom_components/area_occupancy/sensor.py`:
- Around line 73-87: Update the inline comment inside async_added_to_hass (the
comment starting with "Only for specific areas, not "All Areas" or floor
aggregates") to be a complete sentence by adding a trailing period; ensure the
comment that references self._area_handle and the call to self._get_area() ends
with a period to follow the project's comment-style guideline.

…omment style

- Add v17→v18 migration handler to bump entry version for exclude_from_all_areas support
- Add areas() accessor to AllAreas and FloorAreas so extra_state_attributes
  iterates only relevant areas instead of all coordinator areas
- Fix comment style to use full sentences ending with periods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
custom_components/area_occupancy/area/all_areas.py (1)

177-188: Consider caching _floor_areas() results for performance.

Each aggregation method (probability(), occupied(), decay(), etc.) calls _floor_areas(), which performs registry lookups on every invocation. If floor aggregators are queried frequently during coordinator updates, this could become a hot path.

Since floor assignments only change on integration reload (as documented in the docstring), consider caching the area list after the first call, or computing it once in __init__ / a dedicated setup method.

♻️ Example caching approach
 class FloorAreas:
     def __init__(
         self,
         coordinator: AreaOccupancyCoordinator,
         floor_id: str,
         floor_name: str,
     ) -> None:
         self.coordinator = coordinator
         self.floor_id = floor_id
         self.floor_name = floor_name
+        self._cached_areas: list[Area] | None = None

     def _floor_areas(self) -> list[Area]:
         """Return areas that belong to this floor."""
+        if self._cached_areas is not None:
+            return self._cached_areas
         from homeassistant.helpers import area_registry as ar  # noqa: PLC0415

         area_reg = ar.async_get(self.coordinator.hass)
         result: list[Area] = []
         for area in self.coordinator.areas.values():
             if area.config.area_id:
                 area_entry = area_reg.async_get_area(area.config.area_id)
                 if area_entry and area_entry.floor_id == self.floor_id:
                     result.append(area)
+        self._cached_areas = result
         return result
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@custom_components/area_occupancy/area/all_areas.py` around lines 177 - 188,
The _floor_areas() method currently performs registry lookups on every call
(used by probability(), occupied(), decay(), etc.); cache its result to avoid
repeated lookups by computing the list once (e.g., in __init__ or on first call)
and store it on the instance (use a private attribute like _cached_floor_areas
or initialize it in __init__ using self.coordinator and self.floor_id), then
have _floor_areas() return the cached list; because floor assignments only
change on integration reload no invalidation is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@custom_components/area_occupancy/area/all_areas.py`:
- Around line 177-188: The _floor_areas() method currently performs registry
lookups on every call (used by probability(), occupied(), decay(), etc.); cache
its result to avoid repeated lookups by computing the list once (e.g., in
__init__ or on first call) and store it on the instance (use a private attribute
like _cached_floor_areas or initialize it in __init__ using self.coordinator and
self.floor_id), then have _floor_areas() return the cached list; because floor
assignments only change on integration reload no invalidation is required.

@Hankanman Hankanman merged commit 779198d into main Feb 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant