Skip to content

Claude's Publish Bayesian Room Probabilities - #1345

Open
DTTerastar wants to merge 16 commits into
mainfrom
claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X
Open

Claude's Publish Bayesian Room Probabilities#1345
DTTerastar wants to merge 16 commits into
mainfrom
claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X

Conversation

@DTTerastar

@DTTerastar DTTerastar commented Nov 17, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Bayesian probability output feature calculating per-room location probabilities and publishing them to MQTT
    • Introduced Home Assistant auto-discovery for room-based probability sensors with configurable thresholds
    • Added configuration options: bayesian_probabilities.enabled, discovery_threshold, and retain
    • Enhanced sensor auto-discovery with additional metadata support (device class, state class, unit of measurement, value template, icon)
  • Documentation

    • Added comprehensive documentation for Bayesian probability configuration and Home Assistant integration examples

@coderabbitai

coderabbitai Bot commented Nov 17, 2025

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a new Bayesian probability calculation and publishing feature for device location tracking. It adds configuration support, a new service to publish per-room probability sensors to Home Assistant via MQTT, extends data models to track Bayesian state, and integrates probability publishing into the locator service with comprehensive test coverage.

Changes

Cohort / File(s) Summary
Documentation & Configuration
README.md, src/config.example.yaml
Adds Bayesian probability output documentation with YAML examples and configuration reference for Home Assistant integration (enabled, discovery_threshold, retain).
Configuration Models
src/Models/Config.cs, src/Models/Config.Clone.cs
Introduces ConfigBayesianProbabilities class with Enabled, DiscoveryThreshold (clamped to [0,1]), and Retain properties; updates Clone methods to support null-conditional cloning for new Bayesian config.
Discovery & Device Models
src/Models/AutoDiscovery.cs, src/Models/Device.cs
Adds five new discovery properties (DeviceClass, StateClass, UnitOfMeasurement, ValueTemplate, Icon) to DiscoveryRecord; adds BayesianProbabilities and BayesianDiscoveries dictionaries to Device with ResetBayesianState() cleanup method.
Bayesian Probability Service
src/Services/BayesianProbabilityPublisher.cs
New service (242 lines) that builds normalized per-room probability vectors, publishes per-room sensors to Home Assistant via MQTT auto-discovery with threshold filtering, manages discovery lifecycle, and handles synthetic room cleanup.
Locator Integration
src/Services/MultiScenarioLocator.cs, src/Services/DeviceTracker.cs, src/Locators/NearestNode.cs
Integrates BayesianProbabilityPublisher into MultiScenarioLocator constructor; adds conditional probability publication flow; updates payload construction to include probability attributes; resets Bayesian state on device untrack; simplifies floor resolution logic in NearestNode.
Dependency Injection
src/Program.cs
Registers BayesianProbabilityPublisher as singleton service.
Tests
tests/ESPresense.Companion.Tests/FilteringTests.cs, tests/ESPresense.Companion.Tests/MultiScenarioLocatorTests.cs
Updates test fixtures to pass BayesianProbabilityPublisher to MultiScenarioLocator; adds new FixedRoomLocator helper; includes comprehensive test scenarios validating discovery payload structure, sticky sensor behavior, probability normalization, and state cleanup.

Sequence Diagram

sequenceDiagram
    participant ML as MultiScenarioLocator
    participant BP as BayesianProbabilityPublisher
    participant Dev as Device
    participant MQTT as MQTT/Home Assistant
    
    ML->>BP: BuildProbabilityVector(device, scenario)
    Note over BP: Aggregate scenarios by room<br/>Normalize to [0,1]
    BP-->>ML: probabilities dict
    
    alt Bayesian Enabled
        ML->>BP: PublishProbabilitySensorsAsync(device, probabilities, config)
        BP->>Dev: Update BayesianProbabilities
        loop For each room above threshold
            BP->>BP: CreateProbabilityDiscovery(room)
            BP->>Dev: Add to BayesianDiscoveries
            BP->>MQTT: Publish discovery config
        end
        BP->>MQTT: Publish probability state
        BP-->>ML: changes occurred
    else Bayesian Disabled
        ML->>BP: ClearProbabilityOutputsAsync(device)
        BP->>Dev: ResetBayesianState()
        BP->>MQTT: Remove discovery entries
    end
    
    ML->>MQTT: Publish device attributes with probabilities
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested labels

codex

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main feature addition: publishing Bayesian room probabilities, which aligns with the substantial changes across configuration, models, and services throughout the pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@DTTerastar
DTTerastar changed the base branch from codex/implement-changes-from-issue-#1278 to main November 17, 2025 20:28
@DTTerastar DTTerastar changed the title Claude's helping Claude's Publish Bayesian Room Probabilities Nov 17, 2025
@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 00238f8 to 93cc908 Compare November 17, 2025 20:29
@DTTerastar
DTTerastar temporarily deployed to CI - release environment November 17, 2025 20:30 — with GitHub Actions Inactive
@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 93cc908 to 84c3bb6 Compare December 16, 2025 23:32
@DTTerastar
DTTerastar had a problem deploying to CI - release environment December 16, 2025 23:32 — with GitHub Actions Failure

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/Services/MultiScenarioLocator.cs (3)

306-306: Consider using ProbabilityEpsilon constant for consistency.

The hardcoded value 0.0001 differs from the ProbabilityEpsilon constant (0.001) defined at line 34. For consistency, consider using the constant or documenting why a different threshold is needed here.

Apply this diff if you want to use the existing constant:

-            if (remainder > 0.0001)
+            if (remainder > ProbabilityEpsilon)

339-351: Minor: Else-if clause catches both conditions.

The else if at line 346 will execute for both synthetic rooms and rooms below the discovery threshold, which is correct but slightly less explicit than separate conditions. The current implementation works correctly since TryRemove handles missing keys gracefully.

If you prefer more explicit logic, consider:

-            if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
-            {
-                var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
-                if (!device.HassAutoDiscovery.Contains(discovery))
-                    device.HassAutoDiscovery.Add(discovery);
-                await discovery.Send(mqtt);
-            }
-            else if (device.BayesianDiscoveries.TryRemove(roomName, out var staleDiscovery))
-            {
-                device.HassAutoDiscovery.Remove(staleDiscovery);
-                await staleDiscovery.Delete(mqtt);
-                changed = true;
-            }
+            var shouldHaveDiscovery = !IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold;
+            if (shouldHaveDiscovery)
+            {
+                var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
+                if (!device.HassAutoDiscovery.Contains(discovery))
+                    device.HassAutoDiscovery.Add(discovery);
+                await discovery.Send(mqtt);
+            }
+            else if (device.BayesianDiscoveries.TryRemove(roomName, out var staleDiscovery))
+            {
+                device.HassAutoDiscovery.Remove(staleDiscovery);
+                await staleDiscovery.Delete(mqtt);
+                changed = true;
+            }

411-411: Centralize the version constant to avoid hardcoded duplication.

SwVersion is hardcoded as "1.0.0" in this file and also in src/Models/AutoDiscovery.cs (line 34), with the same version also appearing in src/Program.cs (line 98). Consider creating a single version constant or leveraging a configuration value to ensure all locations stay synchronized and simplify future version updates.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1185b3 and 84c3bb6.

📒 Files selected for processing (9)
  • README.md (2 hunks)
  • src/Models/AutoDiscovery.cs (1 hunks)
  • src/Models/Config.Clone.cs (4 hunks)
  • src/Models/Config.cs (2 hunks)
  • src/Models/Device.cs (2 hunks)
  • src/Services/DeviceTracker.cs (1 hunks)
  • src/Services/MultiScenarioLocator.cs (5 hunks)
  • src/config.example.yaml (1 hunks)
  • tests/MultiScenarioLocatorTests.cs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Models/Config.cs
  • src/Models/AutoDiscovery.cs
  • src/Services/DeviceTracker.cs
  • src/config.example.yaml
  • src/Services/MultiScenarioLocator.cs
  • src/Models/Config.Clone.cs
  • src/Models/Device.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • src/Models/Config.cs
  • src/Models/AutoDiscovery.cs
  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Models/Config.Clone.cs
  • tests/MultiScenarioLocatorTests.cs
  • src/Models/Device.cs
src/config.example.yaml

📄 CodeRabbit inference engine (AGENTS.md)

Use src/config.example.yaml as a template and do not commit real secrets

Files:

  • src/config.example.yaml
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/MultiScenarioLocatorTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/MultiScenarioLocatorTests.cs
🧬 Code graph analysis (6)
src/Models/Config.cs (1)
src/Models/Config.Clone.cs (2)
  • ConfigBayesianProbabilities (120-131)
  • ConfigBayesianProbabilities (122-130)
src/Services/DeviceTracker.cs (1)
src/Models/Device.cs (1)
  • ResetBayesianState (152-161)
src/Services/MultiScenarioLocator.cs (3)
src/Models/Config.cs (2)
  • Config (8-61)
  • ConfigBayesianProbabilities (149-165)
src/Models/Device.cs (2)
  • Device (11-209)
  • Device (23-28)
src/Models/AutoDiscovery.cs (6)
  • AutoDiscovery (9-156)
  • AutoDiscovery (17-40)
  • AutoDiscovery (42-47)
  • DiscoveryRecord (110-137)
  • DeviceRecord (139-150)
  • OriginRecord (152-155)
src/Models/Config.Clone.cs (1)
src/Models/Config.cs (1)
  • ConfigBayesianProbabilities (149-165)
tests/MultiScenarioLocatorTests.cs (2)
src/Models/Room.cs (2)
  • Room (6-32)
  • ToString (28-31)
src/Models/Floor.cs (2)
  • Floor (7-47)
  • ToString (36-39)
src/Models/Device.cs (2)
src/Services/MultiScenarioLocator.cs (1)
  • AutoDiscovery (395-420)
src/Models/AutoDiscovery.cs (3)
  • AutoDiscovery (9-156)
  • AutoDiscovery (17-40)
  • AutoDiscovery (42-47)
🪛 GitHub Actions: Build and test
src/Services/MultiScenarioLocator.cs

[error] 228-228: CS0173: Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''

src/Models/Config.Clone.cs

[warning] 39-39: CS8601: Possible null reference assignment.


[warning] 40-40: CS8601: Possible null reference assignment.


[warning] 41-41: CS8601: Possible null reference assignment.


[warning] 68-68: CS8601: Possible null reference assignment.


[warning] 15-15: CS8601: Possible null reference assignment.


[warning] 16-16: CS8601: Possible null reference assignment.


[warning] 19-19: CS8601: Possible null reference assignment.


[warning] 20-20: CS8601: Possible null reference assignment.


[warning] 21-21: CS8601: Possible null reference assignment.


[warning] 22-22: CS8601: Possible null reference assignment.


[warning] 23-23: CS8601: Possible null reference assignment.


[warning] 24-24: CS8601: Possible null reference assignment.

🪛 GitHub Actions: Deploy to Docker
src/Services/MultiScenarioLocator.cs

[error] 228-230: CS0173: Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''.

🪛 GitHub Check: build
src/Services/MultiScenarioLocator.cs

[failure] 230-230:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''


[failure] 229-229:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''


[failure] 228-228:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''


[failure] 230-230:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''


[failure] 229-229:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''


[failure] 228-228:
Type of conditional expression cannot be determined because there is no implicit conversion between 'double' and ''

src/Models/Config.Clone.cs

[warning] 41-41:
Possible null reference assignment.


[warning] 40-40:
Possible null reference assignment.


[warning] 39-39:
Possible null reference assignment.


[warning] 68-68:
Possible null reference assignment.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (23)
src/config.example.yaml (1)

53-57: LGTM! Clear configuration structure.

The new bayesian_probabilities configuration block follows the established YAML structure and provides clear inline documentation for each field.

README.md (1)

23-58: Excellent documentation with practical examples.

The documentation clearly explains the feature, configuration options, topic structure, and provides a concrete Home Assistant integration example. This will help users understand and implement the feature effectively.

src/Services/DeviceTracker.cs (1)

246-246: LGTM! Proper cleanup on untrack.

The call to ResetBayesianState() is correctly placed in the untrack path after removing HassAutoDiscovery entries, ensuring complete cleanup of Bayesian-related state when a device stops being tracked.

src/Models/Device.cs (2)

85-90: LGTM! Well-designed state management.

The concurrent dictionaries with case-insensitive comparers are appropriate for thread-safe room name lookups. The JsonIgnore attributes correctly prevent serialization of internal state.


152-161: LGTM! Correct cleanup implementation.

The method properly uses ToList() to avoid collection modification during enumeration, removes discoveries from HassAutoDiscovery, and clears both dictionaries. Good defensive programming.

src/Models/Config.cs (2)

59-60: LGTM! Consistent configuration structure.

The property follows the established pattern for configuration sections and is properly initialized.


149-165: Excellent input validation!

The Math.Clamp in the DiscoveryThreshold setter ensures values are always within the valid [0.0, 1.0] range, preventing invalid configuration from causing runtime issues. The defaults are sensible (feature disabled by default, retain enabled).

src/Models/AutoDiscovery.cs (1)

122-130: LGTM! Proper Home Assistant discovery extensions.

The new properties correctly follow Home Assistant's MQTT discovery specification with snake_case JSON property names and nullable types for optional fields.

tests/MultiScenarioLocatorTests.cs (2)

22-36: LGTM! Clean test helper implementation.

The FixedRoomLocator uses modern C# primary constructor syntax and provides deterministic scenario setup for reliable testing.


85-193: Excellent test coverage!

This comprehensive test verifies all critical aspects of the Bayesian probability feature:

  • Probability topic publishing with correct retain flags
  • Home Assistant discovery message creation
  • Attributes payload structure
  • Cleanup behavior with tombstone messages on untrack

The test setup is thorough and assertions are specific, providing strong confidence in the implementation.

src/Models/Config.Clone.cs (3)

19-28: LGTM! Null-conditional operators are used correctly.

The static analysis warnings (CS8601) about "Possible null reference assignment" are false positives. The null-conditional operator ?. correctly handles null cases, and the properties have non-null defaults in their declarations. This is the proper pattern for deep cloning nullable nested configuration objects.


39-41: LGTM! Consistent null-safe cloning pattern.

The null-conditional operators follow the same correct pattern as the parent Config.Clone method. The static analysis warnings are false positives.

Also applies to: 68-68


120-131: LGTM! Proper shallow clone for value types.

The ConfigBayesianProbabilities.Clone method correctly implements shallow copying of value-type properties, consistent with other simple configuration Clone methods in the codebase.

src/Services/MultiScenarioLocator.cs (10)

7-9: LGTM: New imports support the Bayesian probability features.

The added using directives are all utilized in the new functionality.


34-34: LGTM: Epsilon value is appropriate for probability comparison.

The value aligns well with the 4-decimal rounding used in probability calculations.


58-62: LGTM: Correctly clears Bayesian state for anchored devices.

Anchored devices have fixed locations, so clearing probability outputs is appropriate. The guard condition avoids unnecessary async operations.


165-184: LGTM: Bayesian probability handling is well-structured.

The logic correctly gates probability publishing based on configuration and manages state appropriately for both enabled and disabled scenarios.


206-264: LGTM: Conditional location reporting logic is sound.

The approach of publishing attributes when either movement or probability changes occur is correct. Gating location data, GPS coordinates, and device history recording based on bestScenario presence ensures consistency and avoids publishing incomplete data.


267-320: LGTM: Probability vector construction is comprehensive.

The method correctly handles multiple edge cases including inactive scenarios, zero-sum probabilities, normalization, and floating-point precision issues. The case-insensitive dictionary and defensive null/whitespace handling are appropriate.


322-371: LGTM: Probability sensor publishing logic is thorough.

The method correctly manages the full lifecycle of probability sensors: creation, updates, discovery management, and cleanup. The use of epsilon for change detection prevents unnecessary MQTT traffic, and the tombstone pattern (null with retain) properly clears stale data.


373-393: LGTM: Probability cleanup is implemented correctly.

The method properly clears all Bayesian state using MQTT tombstones (null with retain) and removes Home Assistant discoveries. The use of ToArray() prevents collection modification exceptions during enumeration.


395-420: LGTM: Home Assistant discovery configuration is well-formed.

The discovery record includes all essential fields for proper Home Assistant integration. The state class "measurement" is appropriate for probability values.


422-449: LGTM: Utility methods are well-implemented.

All three helper methods are defensive and handle edge cases appropriately:

  • BuildProbabilityTopic constructs proper MQTT topic paths
  • SanitizeSegment thoroughly sanitizes room names for use in topics/IDs
  • IsSyntheticRoom provides clear identification of special room names

The use of static is appropriate for these pure utility functions.

Comment thread src/Services/MultiScenarioLocator.cs Outdated
@DTTerastar
DTTerastar temporarily deployed to CI - release environment December 16, 2025 23:38 — with GitHub Actions Inactive
@DTTerastar
DTTerastar temporarily deployed to CI - release environment December 16, 2025 23:44 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 541c4c4 and a071bf2.

📒 Files selected for processing (1)
  • tests/MultiScenarioLocatorTests.cs (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/MultiScenarioLocatorTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/MultiScenarioLocatorTests.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • tests/MultiScenarioLocatorTests.cs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Deploy to Docker add-on with tag
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (1)
tests/MultiScenarioLocatorTests.cs (1)

22-36: LGTM! Well-designed test helper.

The FixedRoomLocator class provides deterministic room assignment for testing Bayesian probability computation. Using a primary constructor and readonly fields is idiomatic modern C#, and the implementation correctly updates all scenario properties needed for the test.

Comment thread tests/MultiScenarioLocatorTests.cs Outdated
@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from a071bf2 to e8a6dc8 Compare January 4, 2026 19:10
@DTTerastar
DTTerastar temporarily deployed to CI - release environment January 4, 2026 19:10 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/MultiScenarioLocatorTests.cs (1)

11-11: Duplicate using directive remains unresolved.

This duplicate using MathNet.Spatial.Euclidean; (also present on line 9) was flagged in a previous review and should be removed.

🔎 Proposed fix
-using MathNet.Spatial.Euclidean;
🧹 Nitpick comments (2)
src/Services/MultiScenarioLocator.cs (2)

267-320: Consider removing redundant clamp operation.

The normalization logic divides each probability by the sum of all probabilities (line 298), which guarantees the result is in the range [0, 1] for positive inputs. The Math.Clamp(normalized, 0, 1) on line 299 is therefore redundant, though harmless.

🔎 Proposed refactor
         foreach (var key in result.Keys.ToList())
         {
             var normalized = result[key] / sum;
-            result[key] = Math.Clamp(normalized, 0, 1);
+            result[key] = normalized;
         }

339-345: Consider simplifying the discovery management pattern.

The GetOrAdd followed by Contains check and Add works correctly, but since discoveries are created once and reused, you could track whether it's newly created to avoid the redundant Contains check.

🔎 Proposed refactor
-            if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
-            {
-                var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
-                if (!device.HassAutoDiscovery.Contains(discovery))
-                    device.HassAutoDiscovery.Add(discovery);
-                await discovery.Send(mqtt);
-            }
+            if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
+            {
+                var isNew = !device.BayesianDiscoveries.ContainsKey(roomName);
+                var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
+                if (isNew)
+                    device.HassAutoDiscovery.Add(discovery);
+                await discovery.Send(mqtt);
+            }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a071bf2 and e8a6dc8.

📒 Files selected for processing (9)
  • README.md
  • src/Models/AutoDiscovery.cs
  • src/Models/Config.Clone.cs
  • src/Models/Config.cs
  • src/Models/Device.cs
  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/config.example.yaml
  • tests/MultiScenarioLocatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/config.example.yaml
  • src/Models/AutoDiscovery.cs
  • README.md
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Models/Config.cs
  • src/Models/Config.Clone.cs
  • src/Models/Device.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Models/Config.cs
  • tests/MultiScenarioLocatorTests.cs
  • src/Models/Config.Clone.cs
  • src/Models/Device.cs
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/MultiScenarioLocatorTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/MultiScenarioLocatorTests.cs
🧬 Code graph analysis (5)
src/Services/DeviceTracker.cs (1)
src/Models/Device.cs (1)
  • ResetBayesianState (152-161)
src/Services/MultiScenarioLocator.cs (3)
src/Models/Config.cs (2)
  • Config (8-61)
  • ConfigBayesianProbabilities (149-165)
src/Models/Device.cs (2)
  • Device (11-209)
  • Device (23-28)
src/Models/AutoDiscovery.cs (6)
  • AutoDiscovery (9-156)
  • AutoDiscovery (17-40)
  • AutoDiscovery (42-47)
  • DiscoveryRecord (110-137)
  • DeviceRecord (139-150)
  • OriginRecord (152-155)
src/Models/Config.cs (1)
src/Models/Config.Clone.cs (2)
  • ConfigBayesianProbabilities (120-131)
  • ConfigBayesianProbabilities (122-130)
src/Models/Config.Clone.cs (1)
src/Models/Config.cs (1)
  • ConfigBayesianProbabilities (149-165)
src/Models/Device.cs (2)
src/Services/MultiScenarioLocator.cs (1)
  • AutoDiscovery (395-420)
src/Models/AutoDiscovery.cs (3)
  • AutoDiscovery (9-156)
  • AutoDiscovery (17-40)
  • AutoDiscovery (42-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Analyze (csharp)
  • GitHub Check: Deploy to Docker add-on with tag
🔇 Additional comments (17)
src/Services/DeviceTracker.cs (1)

246-246: LGTM! Bayesian state cleanup is correctly integrated.

The call to ResetBayesianState() is appropriately placed after removing HassAutoDiscovery entries when a device is untracked, ensuring all Bayesian-related resources are cleaned up.

src/Models/Config.cs (2)

59-60: LGTM! Property addition is consistent with existing patterns.

The new BayesianProbabilities property follows the same pattern as other configuration sections, with proper YAML aliasing and initialization.


149-165: LGTM! Well-implemented configuration class with proper validation.

The ConfigBayesianProbabilities class correctly implements:

  • Clamping of DiscoveryThreshold to the valid probability range [0.0, 1.0]
  • Sensible defaults (enabled=false, threshold=0.1, retain=true)
  • Proper YAML serialization attributes
src/Models/Config.Clone.cs (4)

19-28: LGTM! Null-safe cloning improvements are well-implemented.

The addition of null-conditional operators across all property clones prevents potential NullReferenceException issues. The new BayesianProbabilities?.Clone() follows the established pattern consistently.


39-41: LGTM! Null-safe cloning for nested locator components.

The null-conditional operators ensure safe cloning of locator configurations when they may be absent.


68-68: LGTM! Null-safe Weighting clone.

Consistent with the null-safety improvements applied throughout this file.


120-131: LGTM! Clone implementation correctly copies all properties.

The ConfigBayesianProbabilities.Clone() method properly duplicates all three properties (Enabled, DiscoveryThreshold, Retain), matching the class definition in Config.cs.

src/Models/Device.cs (2)

85-89: LGTM! Thread-safe Bayesian state storage.

The new properties use ConcurrentDictionary with case-insensitive comparers, which is:

  • Consistent with the existing Nodes property pattern (line 51)
  • Thread-safe for concurrent access
  • Properly marked with [STJ.JsonIgnore] to exclude from serialization

152-161: LGTM! Safe cleanup implementation with proper ordering.

The ResetBayesianState() method correctly:

  • Calls .ToList() before iteration to prevent collection modification exceptions
  • Removes discoveries from HassAutoDiscovery before clearing local dictionaries
  • Ensures complete cleanup of Bayesian-related state
tests/MultiScenarioLocatorTests.cs (2)

22-36: LGTM! Clean and focused test helper.

The FixedRoomLocator class provides a deterministic way to assign room/floor/confidence in tests, making the Bayesian probability tests predictable and maintainable.


85-204: LGTM! Comprehensive test validates complete Bayesian probability workflow.

The BayesianProbabilitiesPublishedWhenEnabled test thoroughly validates:

  • ✅ Probability message publishing with non-empty payloads
  • ✅ Retain flag behavior on probability topics
  • ✅ Discovery message generation for rooms above threshold
  • ✅ Attributes payload structure with probability data
  • ✅ Proper cleanup (tombstone messages) when device is untracked

The test uses proper assertions with descriptive messages and validates both the happy path and cleanup behavior.

src/Services/MultiScenarioLocator.cs (6)

58-62: LGTM!

Properly clears Bayesian state for anchored devices, ensuring no stale probability data persists.


165-184: LGTM!

Clean integration of Bayesian probability logic with proper handling of enabled/disabled configuration states.


206-264: LGTM!

The conditional payload construction properly handles both location-based updates and probability-only updates. The null-conditional logic ensures that location data is only included when a best scenario exists, while probability data can be published independently.

Note: The compilation error from the previous review (lines 228-230) has been correctly addressed with (double?) casts.


373-393: LGTM!

Properly clears all Bayesian probability outputs by publishing tombstone messages and cleaning up discovery entities.


395-420: LGTM!

Properly constructs Home Assistant auto-discovery entities for probability sensors with appropriate device class and icon.


422-449: LGTM!

Well-implemented helper methods with proper defensive coding and case-insensitive string handling for room names.

Comment thread src/Services/MultiScenarioLocator.cs Outdated
@DTTerastar
DTTerastar had a problem deploying to CI - release environment January 4, 2026 21:33 — with GitHub Actions Failure
@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 8c5a295 to 299c7b8 Compare January 5, 2026 01:28
@DTTerastar
DTTerastar temporarily deployed to CI - release environment January 5, 2026 01:28 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5a295 and 299c7b8.

📒 Files selected for processing (5)
  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
  • tests/FilteringTests.cs
  • tests/MultiScenarioLocatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/MultiScenarioLocatorTests.cs
  • src/Program.cs
🧰 Additional context used
📓 Path-based instructions (4)
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/FilteringTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/FilteringTests.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • tests/FilteringTests.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
🧬 Code graph analysis (3)
tests/FilteringTests.cs (2)
src/Services/BayesianProbabilityPublisher.cs (2)
  • BayesianProbabilityPublisher (12-232)
  • BayesianProbabilityPublisher (19-22)
src/Services/MultiScenarioLocator.cs (1)
  • MultiScenarioLocator (23-333)
src/Services/BayesianProbabilityPublisher.cs (2)
src/Models/Scenario.cs (1)
  • Scenario (8-59)
src/Models/AutoDiscovery.cs (3)
  • DiscoveryRecord (110-137)
  • DeviceRecord (139-150)
  • OriginRecord (152-155)
src/Services/MultiScenarioLocator.cs (2)
src/Services/BayesianProbabilityPublisher.cs (3)
  • BayesianProbabilityPublisher (12-232)
  • BayesianProbabilityPublisher (19-22)
  • Dictionary (28-81)
src/Events/GlobalEventDispatcher.cs (1)
  • OnDeviceChanged (20-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Deploy to Docker add-on with tag
  • GitHub Check: build
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (9)
tests/FilteringTests.cs (1)

108-109: LGTM! Test setup correctly updated.

The BayesianProbabilityPublisher is properly instantiated with the mocked MqttCoordinator and passed to the MultiScenarioLocator constructor, aligning with the expanded constructor signature.

src/Services/BayesianProbabilityPublisher.cs (4)

28-81: LGTM! Robust probability normalization logic.

The method correctly handles edge cases:

  • Returns not_home = 1 when no active scenarios exist
  • Uses a reasonable fallback chain (Room → Floor → Scenario → "unknown")
  • Normalizes probabilities to sum to 1.0
  • Distributes remainder to an "other" bucket to ensure exact sum of 1.0
  • Handles floating-point precision issues with re-normalization

87-143: LGTM! Well-designed hysteresis prevents sensor flapping.

The publishing logic correctly:

  • Detects and publishes changed probability values (rounded to 4 decimals)
  • Implements hysteresis (create at threshold, remove at 80% threshold) to prevent rapid discovery add/remove cycles
  • Filters synthetic rooms ("other", "not_home") from Home Assistant discovery
  • Cleans up removed rooms by publishing null with retain flag and deleting discovery entries

149-169: LGTM! Comprehensive cleanup implementation.

The method properly clears all probability state by:

  • Publishing null with retain flag to remove MQTT topics
  • Removing entries from device state collections
  • Deleting all Home Assistant discovery entries

171-231: LGTM! Helper methods are well-implemented.

The helper methods provide robust support for:

  • Creating properly structured Home Assistant discovery records with device metadata
  • Building sanitized MQTT topic paths using snake_case conversion and character filtering
  • Identifying synthetic rooms to exclude from discovery (well-documented rationale)
src/Services/MultiScenarioLocator.cs (4)

28-29: LGTM! Constructor signature properly expanded.

The BayesianProbabilityPublisher dependency is correctly added to the constructor using primary constructor syntax, enabling dependency injection of the new service.


55-59: LGTM! Proper cleanup for anchored devices.

Bayesian probability state is correctly cleared when a device is anchored, preventing stale probability data. The conditional check avoids unnecessary cleanup calls when the state is already empty.


162-181: LGTM! Bayesian integration follows clean enable/disable pattern.

The probability publishing logic correctly:

  • Builds and publishes probability vectors when the feature is enabled
  • Stores rounded probabilities for inclusion in MQTT attribute payloads
  • Clears all probability state when the feature is disabled
  • Tracks changes to trigger attribute updates

204-266: LGTM! Conditional publishing logic handles edge cases correctly.

The modified publishing logic properly:

  • Publishes attributes when either location changes or probabilities change
  • Uses includeLocation flag to conditionally include location data based on bestScenario presence
  • Properly casts coordinates to nullable doubles (lines 230-232) to avoid compilation errors
  • Includes probability data in the attributes payload
  • Only triggers events and history recording when actual location data exists
  • Documents the behavior with clear inline comments

Comment thread src/Services/BayesianProbabilityPublisher.cs Outdated
@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 299c7b8 to 204d407 Compare January 5, 2026 01:49
@DTTerastar
DTTerastar temporarily deployed to CI - release environment January 5, 2026 01:49 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
tests/MultiScenarioLocatorTests.cs (1)

9-11: Remove duplicate import statement.

The using MathNet.Spatial.Euclidean; directive appears on both lines 9 and 11.

 using Moq;
 using Newtonsoft.Json.Linq;
 using MathNet.Spatial.Euclidean;
 using SQLite;
-using MathNet.Spatial.Euclidean;
🧹 Nitpick comments (1)
src/Services/BayesianProbabilityPublisher.cs (1)

172-197: Consider using dynamic version for discovery metadata.

The SwVersion = "1.0.0" is hardcoded. For better maintainability, consider using the assembly version or a shared constant that can be updated centrally.

🔎 Suggested improvement
+    private static readonly string AssemblyVersion = 
+        typeof(BayesianProbabilityPublisher).Assembly.GetName().Version?.ToString() ?? "1.0.0";
+
     private AutoDiscovery CreateProbabilityDiscovery(Device device, string roomName)
     {
         // ...
             Device = new AutoDiscovery.DeviceRecord
             {
                 // ...
-                SwVersion = "1.0.0",
+                SwVersion = AssemblyVersion,
                 // ...
             },
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 299c7b8 and 204d407.

📒 Files selected for processing (5)
  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
  • tests/FilteringTests.cs
  • tests/MultiScenarioLocatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/FilteringTests.cs
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/MultiScenarioLocator.cs
  • tests/MultiScenarioLocatorTests.cs
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/MultiScenarioLocatorTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/MultiScenarioLocatorTests.cs
🧬 Code graph analysis (3)
src/Program.cs (1)
src/Services/BayesianProbabilityPublisher.cs (2)
  • BayesianProbabilityPublisher (12-233)
  • BayesianProbabilityPublisher (19-22)
src/Services/BayesianProbabilityPublisher.cs (2)
src/Models/Scenario.cs (1)
  • Scenario (8-59)
src/Models/AutoDiscovery.cs (3)
  • DiscoveryRecord (110-137)
  • DeviceRecord (139-150)
  • OriginRecord (152-155)
src/Services/MultiScenarioLocator.cs (3)
src/Services/BayesianProbabilityPublisher.cs (3)
  • BayesianProbabilityPublisher (12-233)
  • BayesianProbabilityPublisher (19-22)
  • Dictionary (28-81)
src/Utils/JsonSerializerSettings.cs (1)
  • SerializerSettings (6-9)
src/Events/GlobalEventDispatcher.cs (1)
  • OnDeviceChanged (20-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Deploy to Docker add-on with tag
  • GitHub Check: build
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (15)
src/Program.cs (1)

78-78: LGTM!

The singleton registration follows the established pattern and is correctly placed after MqttCoordinator (its dependency) in the DI container setup.

tests/MultiScenarioLocatorTests.cs (4)

22-36: LGTM!

The FixedRoomLocator helper is well-designed for deterministic test scenarios. It correctly handles nullable Fixes with null coalescing and sets all required scenario properties.


59-61: LGTM!

The test correctly integrates the new BayesianProbabilityPublisher dependency while maintaining its original purpose of verifying the "not_home" state transition.


86-206: Comprehensive test coverage for Bayesian probability publishing.

This test thoroughly validates the Bayesian probability workflow including:

  • Probability topic publishing with correct retain flags
  • Home Assistant discovery message creation
  • Probability attributes in the payload
  • Tombstone cleanup when untracking devices

The use of Guid.NewGuid() for the work directory (line 89) is a good practice to avoid test isolation issues.


229-230: LGTM!

Correctly updated to include the new BayesianProbabilityPublisher dependency.

src/Services/BayesianProbabilityPublisher.cs (6)

14-22: LGTM!

Constants are well-documented and the hysteresis ratio (0.8) is a reasonable choice to prevent sensor flapping. The constructor follows standard DI patterns.


28-81: LGTM!

The probability vector building logic handles edge cases well:

  • No active scenarios → not_home
  • Zero sum → other
  • Proper normalization with remainder allocation to "other" bucket

The case-insensitive dictionary is appropriate for room name aggregation.


92-103: LGTM - Previous concern addressed.

The direct equality comparison at line 96 (Math.Round(existing, 4) != roundedProbability) correctly resolves the earlier epsilon precision issue. Both values are rounded to 4 decimals before comparison, ensuring all changes are detected.


105-124: LGTM!

The hysteresis logic effectively prevents sensor flapping by using different thresholds for creation vs. removal. The synthetic room exclusion keeps the Home Assistant UI clean.


150-170: LGTM!

Proper cleanup implementation using MQTT tombstone pattern (null payload with retain). The ToArray() call correctly handles modification during iteration of the concurrent dictionary.


199-232: LGTM!

The helper methods are well-implemented:

  • SanitizeSegment provides robust sanitization with appropriate fallbacks
  • IsSyntheticRoom correctly identifies synthetic rooms excluded from discovery
  • The XML documentation on IsSyntheticRoom helpfully explains the design decision about HA sensor sums.
src/Services/MultiScenarioLocator.cs (4)

28-29: LGTM!

The BayesianProbabilityPublisher dependency is appropriately added to the primary constructor, following the existing pattern.


55-59: LGTM!

Good optimization to check for existing outputs before calling the async cleanup method. This ensures anchored devices have clean Bayesian state.


162-181: LGTM!

Clean integration of Bayesian probability publishing:

  • Properly gated by configuration
  • Tracks changes for attribute publication decisions
  • Cleans up outputs when feature is disabled (important for configuration changes at runtime)

204-266: LGTM - Previous compilation error addressed.

The conditional attribute publishing logic is well-designed:

  • Allows probability-only updates when no location exists (lines 208, 226-232)
  • The (double?) casts on lines 230-232 correctly resolve the previous compilation error
  • History recording is appropriately gated by includeLocation to only log real location updates
  • SerializerSettings.NullIgnore ensures clean JSON output without null fields

@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 204d407 to eccda05 Compare January 5, 2026 02:15
@DTTerastar
DTTerastar temporarily deployed to CI - release environment January 5, 2026 02:15 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
tests/MultiScenarioLocatorTests.cs (1)

9-11: Remove duplicate import statement.

The using MathNet.Spatial.Euclidean; directive appears on both lines 9 and 11.

🔎 Proposed fix
 using Moq;
 using Newtonsoft.Json.Linq;
 using MathNet.Spatial.Euclidean;
 using SQLite;
-using MathNet.Spatial.Euclidean;
🧹 Nitpick comments (1)
src/Services/BayesianProbabilityPublisher.cs (1)

183-190: Consider extracting hardcoded version string.

The SwVersion = "1.0.0" is hardcoded. For consistency and maintainability, consider deriving this from the assembly version or a shared constant.

🔎 Proposed improvement
+    private static readonly string SwVersion = typeof(BayesianProbabilityPublisher).Assembly
+        .GetName().Version?.ToString() ?? "1.0.0";
+
     private AutoDiscovery CreateProbabilityDiscovery(Device device, string roomName)
     {
         // ...
             Device = new AutoDiscovery.DeviceRecord
             {
                 Name = device.Name ?? device.Id,
                 Manufacturer = "ESPresense",
                 Model = "Companion",
-                SwVersion = "1.0.0",
+                SwVersion = SwVersion,
                 Identifiers = new[] { $"espresense-{device.Id}" }
             },
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 204d407 and eccda05.

📒 Files selected for processing (12)
  • README.md
  • src/Models/AutoDiscovery.cs
  • src/Models/Config.Clone.cs
  • src/Models/Config.cs
  • src/Models/Device.cs
  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/config.example.yaml
  • tests/FilteringTests.cs
  • tests/MultiScenarioLocatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/FilteringTests.cs
  • src/Models/Config.cs
  • README.md
  • src/config.example.yaml
  • src/Services/DeviceTracker.cs
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Place backend C# ASP.NET Core code under src/ (controllers, services, models, utils)

Files:

  • src/Models/Config.Clone.cs
  • src/Models/AutoDiscovery.cs
  • src/Program.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Models/Device.cs
{src,tests}/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

{src,tests}/**/*.cs: C#: Use spaces with an indent size of 4
C#: Use PascalCase for types and methods
C#: Use camelCase for local variables and parameters

Files:

  • src/Models/Config.Clone.cs
  • src/Models/AutoDiscovery.cs
  • tests/MultiScenarioLocatorTests.cs
  • src/Program.cs
  • src/Services/MultiScenarioLocator.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Models/Device.cs
tests/**/*.cs

📄 CodeRabbit inference engine (AGENTS.md)

Place backend NUnit tests under tests/

Files:

  • tests/MultiScenarioLocatorTests.cs
tests/**/*Tests.cs

📄 CodeRabbit inference engine (AGENTS.md)

Name backend NUnit test files with the *Tests.cs suffix (e.g., TimeSpanExtensionsTests.cs)

Files:

  • tests/MultiScenarioLocatorTests.cs
🧬 Code graph analysis (4)
src/Models/Config.Clone.cs (1)
src/Models/Config.cs (1)
  • ConfigBayesianProbabilities (149-165)
src/Program.cs (1)
src/Services/BayesianProbabilityPublisher.cs (2)
  • BayesianProbabilityPublisher (12-233)
  • BayesianProbabilityPublisher (19-22)
src/Services/BayesianProbabilityPublisher.cs (2)
src/Models/Scenario.cs (1)
  • Scenario (8-59)
src/Models/AutoDiscovery.cs (3)
  • DiscoveryRecord (110-137)
  • DeviceRecord (139-150)
  • OriginRecord (152-155)
src/Models/Device.cs (1)
src/Models/AutoDiscovery.cs (3)
  • AutoDiscovery (9-156)
  • AutoDiscovery (17-40)
  • AutoDiscovery (42-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Deploy to Docker add-on with tag
  • GitHub Check: build
  • GitHub Check: Analyze (csharp)
🔇 Additional comments (19)
src/Models/Config.Clone.cs (2)

19-28: LGTM! Defensive null-conditional cloning pattern.

The switch to null-conditional cloning (?.Clone()) across all properties prevents potential NullReferenceException issues and properly integrates the new BayesianProbabilities configuration into the clone flow.


120-131: LGTM! Clone implementation is correct and consistent.

The ConfigBayesianProbabilities.Clone() method properly copies all properties and follows the established pattern used by other configuration Clone methods in this file.

src/Models/AutoDiscovery.cs (1)

122-130: LGTM! Standard Home Assistant discovery attributes.

The added properties (DeviceClass, StateClass, UnitOfMeasurement, ValueTemplate, Icon) are standard Home Assistant auto-discovery attributes, properly declared as nullable strings with correct JSON serialization attributes.

src/Program.cs (1)

78-78: LGTM! Proper DI registration.

The BayesianProbabilityPublisher is correctly registered as a singleton and positioned before the hosted services that depend on it.

src/Models/Device.cs (2)

85-89: LGTM! Thread-safe dictionaries with proper configuration.

The BayesianProbabilities and BayesianDiscoveries dictionaries use ConcurrentDictionary for thread safety and StringComparer.OrdinalIgnoreCase for case-insensitive room name matching, which is appropriate for this use case.


152-161: LGTM! Proper cleanup of Bayesian state.

The ResetBayesianState() method correctly:

  • Uses ToList() to avoid collection modification during enumeration
  • Removes discoveries from HassAutoDiscovery before clearing
  • Clears both dictionaries to fully reset the Bayesian state
tests/MultiScenarioLocatorTests.cs (4)

22-36: LGTM! Well-designed test helper.

The FixedRoomLocator class provides deterministic localization behavior for testing Bayesian probability outputs. The use of primary constructor syntax (C# 12) is concise and appropriate for a test helper.


59-61: LGTM! Test updated for new dependency.

The test properly instantiates and injects BayesianProbabilityPublisher to match the updated MultiScenarioLocator constructor signature.


86-206: LGTM! Comprehensive test coverage.

The BayesianProbabilitiesPublishedWhenEnabled test thoroughly validates:

  • Probability topic publishing with correct payloads
  • Retain flag behavior (retain=true for all probability messages)
  • Discovery message creation for rooms above threshold
  • Attributes message structure and probability values
  • Cleanup behavior on untracking (tombstone messages and discovery deletion)

The test setup is extensive but appropriate for integration testing of the Bayesian probability publishing flow.


229-230: LGTM! Test updated for new dependency.

Consistent with the other test updates, properly injects BayesianProbabilityPublisher to match the updated constructor signature.

src/Services/BayesianProbabilityPublisher.cs (5)

1-22: LGTM!

The class structure, constants, and constructor are well-designed. The NormalizationEpsilon and DiscoveryHysteresisRatio constants are appropriately documented, and the dependency injection pattern is correctly implemented.


28-81: LGTM!

The BuildProbabilityVector method handles edge cases well: empty scenarios fallback to "not_home", zero-sum protection returns "other", and the normalization logic correctly handles both under-sum (adding "other") and over-sum (re-normalizing) cases.


92-124: LGTM!

The publishing logic is well-implemented:

  • Direct equality comparison for 4-decimal rounded values (addressing the previous epsilon concern)
  • Hysteresis pattern (80% threshold for removal) prevents sensor flapping
  • Proper guard against duplicate discovery registrations at lines 112-113

150-170: LGTM!

The cleanup method correctly handles collection modification by using ToArray(), properly tombstones retained MQTT messages, and cleans up both probability state and discovery entities.


205-233: LGTM!

The helper methods are well-implemented:

  • SanitizeSegment properly handles edge cases (null, whitespace, invalid chars) and produces consistent MQTT-safe topic segments
  • IsSyntheticRoom is clearly documented explaining why synthetic rooms are excluded from HA discovery
src/Services/MultiScenarioLocator.cs (4)

23-30: LGTM!

The constructor correctly adds the BayesianProbabilityPublisher dependency following the existing primary constructor pattern.


55-59: LGTM!

Properly clears Bayesian state for anchored devices, with an efficient guard to avoid unnecessary async operations when there's no state to clear.


162-181: LGTM!

The Bayesian probability integration is well-structured:

  • Conditional execution based on configuration
  • Probability vector is built and published only when enabled
  • Cleanup is triggered when the feature is disabled, preventing stale data

204-266: LGTM!

The attribute publication logic is well-designed:

  • Correctly separates location-dependent from probability-only updates
  • The includeLocation flag properly gates GPS coordinates, device events, and history entries
  • The nullable double cast issue from previous reviews is addressed at lines 230-232
  • Probabilities are included in the attributes payload for Home Assistant integration

@DTTerastar
DTTerastar force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from eccda05 to 07441cd Compare February 1, 2026 16:06
@DTTerastar
DTTerastar temporarily deployed to CI - release environment February 1, 2026 16:06 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 44-58: The example uses invalid Bayesian sensor syntax and a wrong
entity type: replace the top-level "sensor" with "binary_sensor" and add a
probability_threshold; in each observation change "probability" to the required
"prob_given_true" and "prob_given_false" fields and make the observation
value_template return a boolean (e.g., compare the probability sensor with >
0.5) instead of returning a raw float; finally update the automation example to
trigger on the created binary_sensor's state ("binary_sensor.pat_in_kitchen" is
"on"/"off") or, if you need numeric comparisons, use the original probability
sensor with a numeric_state condition.

Comment thread README.md Outdated
@DTTerastar
DTTerastar temporarily deployed to CI - release environment February 1, 2026 16:25 — with GitHub Actions Inactive
@DTTerastar
DTTerastar temporarily deployed to CI - release environment February 1, 2026 20:26 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
src/Services/MultiScenarioLocator.cs (1)

178-208: ⚠️ Potential issue | 🟡 Minor

Disabling probabilities can leave retained attributes stale in not_home.

When Bayesian probabilities are disabled and bestScenario is null, no attributes are republished, so retained payloads can still include old probabilities. Consider forcing a publish with an empty probabilities payload when a clear occurred.

🔧 Proposed fix
         else
         {
             probabilityChanged = await bayesianPublisher.ClearProbabilityOutputsAsync(device);
+            if (probabilityChanged && bestScenario == null)
+                probabilityAttributes = new Dictionary<string, double>();
         }
🤖 Fix all issues with AI agents
In `@src/Services/BayesianProbabilityPublisher.cs`:
- Around line 91-110: The discovery check uses the unrounded probability causing
inconsistencies with the rounded payloads; update the condition in the foreach
inside BayesianProbabilityPublisher (the loop that computes roundedProbability)
to compare roundedProbability (or a value rounded to the same 4-decimal
precision) against config.DiscoveryThreshold instead of the raw probability, so
IsSyntheticRoom(... ) && roundedProbability >= config.DiscoveryThreshold
triggers discovery and keeps payload and discovery logic consistent.

In `@src/Services/MultiScenarioLocator.cs`:
- Around line 171-175: probabilityVector keys are being sanitized via
BayesianProbabilityPublisher.SanitizeSegment before building
probabilityAttributes with ToDictionary, which throws if distinct original keys
collide after sanitization; change the construction of probabilityAttributes to
first group probabilityVector by the sanitized key (use
BayesianProbabilityPublisher.SanitizeSegment for grouping) and then coalesce
each group's values (e.g., sum or otherwise aggregate the probabilities) and
round the result (Math.Round(..., 4)) so duplicate sanitized keys do not cause
exceptions.

Comment on lines +91 to +110
foreach (var (roomName, probability) in probabilities)
{
var roundedProbability = Math.Round(probability, 4);
// Direct equality comparison since both values are rounded to 4 decimals
var payloadChanged = !device.BayesianProbabilities.TryGetValue(roomName, out var existing) || Math.Round(existing, 4) != roundedProbability;
if (payloadChanged)
{
device.BayesianProbabilities[roomName] = roundedProbability;
changed = true;
}

var hasDiscovery = device.BayesianDiscoveries.ContainsKey(roomName);

if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
{
var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
if (!device.HassAutoDiscovery.Contains(discovery))
device.HassAutoDiscovery.Add(discovery);
await discovery.Send(_mqtt);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align discovery threshold with rounded values.

Discovery uses the raw probability while payloads use rounded values, which can omit sensors that appear to meet the threshold. Consider comparing against the rounded value for consistency.

🔧 Proposed fix
-            if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
+            if (!IsSyntheticRoom(roomName) && roundedProbability >= config.DiscoveryThreshold)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (var (roomName, probability) in probabilities)
{
var roundedProbability = Math.Round(probability, 4);
// Direct equality comparison since both values are rounded to 4 decimals
var payloadChanged = !device.BayesianProbabilities.TryGetValue(roomName, out var existing) || Math.Round(existing, 4) != roundedProbability;
if (payloadChanged)
{
device.BayesianProbabilities[roomName] = roundedProbability;
changed = true;
}
var hasDiscovery = device.BayesianDiscoveries.ContainsKey(roomName);
if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
{
var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
if (!device.HassAutoDiscovery.Contains(discovery))
device.HassAutoDiscovery.Add(discovery);
await discovery.Send(_mqtt);
}
foreach (var (roomName, probability) in probabilities)
{
var roundedProbability = Math.Round(probability, 4);
// Direct equality comparison since both values are rounded to 4 decimals
var payloadChanged = !device.BayesianProbabilities.TryGetValue(roomName, out var existing) || Math.Round(existing, 4) != roundedProbability;
if (payloadChanged)
{
device.BayesianProbabilities[roomName] = roundedProbability;
changed = true;
}
var hasDiscovery = device.BayesianDiscoveries.ContainsKey(roomName);
if (!IsSyntheticRoom(roomName) && roundedProbability >= config.DiscoveryThreshold)
{
var discovery = device.BayesianDiscoveries.GetOrAdd(roomName, key => CreateProbabilityDiscovery(device, key));
if (!device.HassAutoDiscovery.Contains(discovery))
device.HassAutoDiscovery.Add(discovery);
await discovery.Send(_mqtt);
}
🤖 Prompt for AI Agents
In `@src/Services/BayesianProbabilityPublisher.cs` around lines 91 - 110, The
discovery check uses the unrounded probability causing inconsistencies with the
rounded payloads; update the condition in the foreach inside
BayesianProbabilityPublisher (the loop that computes roundedProbability) to
compare roundedProbability (or a value rounded to the same 4-decimal precision)
against config.DiscoveryThreshold instead of the raw probability, so
IsSyntheticRoom(... ) && roundedProbability >= config.DiscoveryThreshold
triggers discovery and keeps payload and discovery logic consistent.

Comment thread src/Services/MultiScenarioLocator.cs Outdated
Comment on lines +171 to +175
if (probabilityVector.Count > 0)
{
probabilityAttributes = probabilityVector.ToDictionary(
kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key),
kvp => Math.Round(kvp.Value, 4));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sanitized-key collisions can throw when building probability attributes.

Distinct room names can sanitize to the same key (e.g., spaces vs dashes), which makes ToDictionary throw and can break processing. Consider grouping by sanitized key (or pre-sanitizing upstream) to coalesce duplicates safely.

🛠️ Proposed fix
-                probabilityAttributes = probabilityVector.ToDictionary(
-                    kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key),
-                    kvp => Math.Round(kvp.Value, 4));
+                probabilityAttributes = probabilityVector
+                    .GroupBy(kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key), StringComparer.OrdinalIgnoreCase)
+                    .ToDictionary(
+                        g => g.Key,
+                        g => Math.Round(g.Sum(x => x.Value), 4),
+                        StringComparer.OrdinalIgnoreCase);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (probabilityVector.Count > 0)
{
probabilityAttributes = probabilityVector.ToDictionary(
kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key),
kvp => Math.Round(kvp.Value, 4));
if (probabilityVector.Count > 0)
{
probabilityAttributes = probabilityVector
.GroupBy(kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key), StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => Math.Round(g.Sum(x => x.Value), 4),
StringComparer.OrdinalIgnoreCase);
🤖 Prompt for AI Agents
In `@src/Services/MultiScenarioLocator.cs` around lines 171 - 175,
probabilityVector keys are being sanitized via
BayesianProbabilityPublisher.SanitizeSegment before building
probabilityAttributes with ToDictionary, which throws if distinct original keys
collide after sanitization; change the construction of probabilityAttributes to
first group probabilityVector by the sanitized key (use
BayesianProbabilityPublisher.SanitizeSegment for grouping) and then coalesce
each group's values (e.g., sum or otherwise aggregate the probabilities) and
round the result (Math.Round(..., 4)) so duplicate sanitized keys do not cause
exceptions.

sensiebot Bot and others added 16 commits March 11, 2026 10:01
* fix: Handle MQTT DNS failures gracefully (fixes #1463)

When MQTT broker DNS resolution fails (e.g., mqtt.z13.org cannot resolve),
ESPresense Companion was treating publish attempts as fatal exceptions that
escaped background services and triggered host shutdown per default
HostOptions.BackgroundServiceExceptionBehavior.

Root cause:
- MqttCoordinator.EnqueueAsync logs and rethrows exceptions
- TelemetryService.ExecuteAsync publishes every 30s without try/catch
- MultiScenarioLocator.ProcessDevice publishes state updates without protection
- Unhandled MqttCommunicationException → BackgroundService failed → host shutdown

Changes:
1. Added IMqttCoordinator.TryEnqueueAsync method for best-effort publishes
   that don't throw exceptions (returns bool success instead)
2. Updated TelemetryService to use TryEnqueueAsync - telemetry is best-effort
   and should never crash the host
3. Updated MultiScenarioLocator to use TryEnqueueAsync for state/attribute
   publishes - ensures a single MQTT failure doesn't abort device processing
4. EnqueueAsync still throws for critical operations that need error handling

Result: MQTT failures (DNS, network, broker down) are now logged but don't
crash background services or trigger host shutdown. Services continue running
and will retry when connection is restored.

* test: Update NotHomeStateWhenAllScenariosExpire to use TryEnqueueAsync

Fix test failure due to EnqueueAsync -> TryEnqueueAsync migration.
TryEnqueueAsync returns Task<bool> instead of Task.

---------

Co-authored-by: Darrell <DT@Terastar.biz>
This change adds an additional condition in InitializeScenario to only include nodes whose Z coordinate lies within the floor's bounds when bounds are defined. This prevents nodes from other floors (especially when assigned to multiple floors) from participating in a floor's localization, which caused room flip-flopping in vertically stacked rooms.

Fixes #2220 (ESPresense/ESPresense#2220)

Co-authored-by: Sensie <sensie@openclaw.ai>
* Bump the nuget group with 1 update

Bumps AutoMapper from 16.1.0 to 16.1.1

---
updated-dependencies:
- dependency-name: AutoMapper
  dependency-version: 16.1.1
  dependency-type: direct:production
  dependency-group: nuget
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: update AutoMapper in test project to match main

The test project still referenced AutoMapper 16.1.0 while main was updated to 16.1.1, causing a package downgrade error (NU1605). This blocked the Dependabot PR from merging.

Fixes #1507

Therefore: update test project to 16.1.1.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sensie <sensie@openclaw.ai>
Bumps the npm_and_yarn group with 1 update in the /src/ui directory: [devalue](https://github.com/sveltejs/devalue).


Updates `devalue` from 5.6.3 to 5.6.4
- [Release notes](https://github.com/sveltejs/devalue/releases)
- [Changelog](https://github.com/sveltejs/devalue/blob/main/CHANGELOG.md)
- [Commits](sveltejs/devalue@v5.6.3...v5.6.4)

---
updated-dependencies:
- dependency-name: devalue
  dependency-version: 5.6.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Slider for calibration

* Trigger CI rebuild - retrigger
…ui (#1517)

Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.1.18 to 4.2.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.1/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
updated-dependencies:
- dependency-name: Swashbuckle.AspNetCore
  dependency-version: 10.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…d test coverage

This commit addresses all the feedback from PR #1279 review:

**Compilation Fixes:**
- Fix CS0173 type inference errors in MultiScenarioLocator.cs by casting double values to nullable (double?)
- Properly handle x, y, z coordinate nullable types in JSON serialization

**Logic Improvements:**
- Fix MQTT retain logic for tombstone messages (always use retain:true when clearing topics)
- Add validation clamping for DiscoveryThreshold to ensure 0.0-1.0 range
- Round probability values to 4 decimal places before comparison to reduce publish churn

**Code Quality:**
- Add null-guards in Config.Clone.cs to prevent NullReferenceExceptions
- Use null-conditional operators for all Clone() calls

**Documentation:**
- Clarify threshold range (0.0-1.0) in README and example config
- Document room name normalization to lowercase for MQTT topics
- Add inline comments about configuration ranges

**Test Coverage:**
- Add assertions to verify retain flag is honored on probability topics
- Add assertions to verify discovery configs are published for rooms above threshold
- Add test scenario for untracking devices to verify probability sensors are deleted
- Add assertions to verify tombstone messages use retain:true
…tion

Fixes CS0173 compilation errors where ternary operator couldn't determine
type between double and null. Added explicit (double?) casts for x, y, z
coordinates in the attributes payload.
…ve room name matching

The test was failing because it expected exact case-sensitive room name matches
in the JSON probabilities object. Added case-insensitive lookup and better error
messages to show available keys when assertions fail.
…eric_state` observations and add an automation example.
…s and enable sticky Home Assistant sensors via `value_template`.
…y names

- Keep HA sensor discoveries when device leaves a room (set probability to 0 instead of deleting)
- Remove device name from sensor Name to prevent duplicate names like
  `sensor.device_name_device_name_room_probability`
- Include sticky rooms with 0 probability in attributes payload
- Update tests to verify sticky behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- NearestNode now finds rooms by location or by matching node ID/name
- Filter out low-confidence fallback scenarios from Bayesian probabilities
  when better locators are working (confidence threshold = 5)
- Prevents "NearestNode" from appearing as a fake room in probabilities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sensiebot
sensiebot Bot force-pushed the claude/fix-espresense-build-01Cn9KwMrsqzZjQJ6RZxrs6X branch from 97806fd to cc0c4ce Compare March 18, 2026 14:23
@sensiebot
sensiebot Bot temporarily deployed to CI - release environment March 18, 2026 14:23 Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (2)
src/Models/Config.Clone.cs (1)

35-42: ⚠️ Potential issue | 🟠 Major

Clone the remaining locator sub-configs here.

Lines 37-41 only copy NadarayaWatson, NelderMead, and NearestNode. ConfigLocators still has Bfgs, Mle, and MultiFloor in src/Models/Config.cs:63-82, so cloning now resets those settings to defaults instead of preserving the loaded config.

🛠️ Suggested fix
         return new ConfigLocators
         {
             NadarayaWatson = NadarayaWatson?.Clone(),
             NelderMead = NelderMead?.Clone(),
+            Bfgs = Bfgs?.Clone(),
+            Mle = Mle?.Clone(),
+            MultiFloor = MultiFloor?.Clone(),
             NearestNode = NearestNode?.Clone()
         };
public partial class BfgsConfig
{
    public BfgsConfig Clone() => new()
    {
        Enabled = Enabled,
        Floors = Floors?.ToArray(),
        Weighting = Weighting?.Clone()
    };
}

Apply the same pattern to MleConfig and MultiFloorConfig.

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

In `@src/Models/Config.Clone.cs` around lines 35 - 42, ConfigLocators.Clone
currently only copies NadarayaWatson, NelderMead, and NearestNode, which drops
Bfgs, Mle, and MultiFloor settings; add cloning for the Bfgs, Mle, and
MultiFloor properties in ConfigLocators.Clone and implement Clone methods on
BfgsConfig, MleConfig, and MultiFloorConfig (e.g., BfgsConfig.Clone,
MleConfig.Clone, MultiFloorConfig.Clone) following the existing pattern: return
a new instance copying simple fields, cloning nested objects (like
Weighting.Clone()) and copying arrays with .ToArray() (e.g., Floors?.ToArray())
so loaded config values are preserved rather than reset.
src/Services/MultiScenarioLocator.cs (1)

180-210: ⚠️ Potential issue | 🟠 Major

Clearing Bayesian state can leave stale retained attributes behind.

When ClearProbabilityOutputsAsync() returns true and bestScenario is null, Line 210 skips the /attributes republish because probabilityAttributes is still null. The broker keeps the previous retained probabilities object, so disabling Bayesian publishing or clearing state while a device is not_home leaks stale probabilities to subscribers.

🧹 Suggested fix
         else
         {
             probabilityChanged = await bayesianPublisher.ClearProbabilityOutputsAsync(device);
+            if (probabilityChanged)
+            {
+                probabilityAttributes = new Dictionary<string, double>();
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Services/MultiScenarioLocator.cs` around lines 180 - 210, When
ClearProbabilityOutputsAsync (referenced as
bayesianPublisher.ClearProbabilityOutputsAsync) returns true but bestScenario is
null and probabilityAttributes is null, the existing condition prevents sending
an /attributes republish and leaves stale retained probabilities on the broker;
update the logic so that when probabilityChanged is true you still enqueue an
attributes publish (using mqtt.TryEnqueueAsync for the
"espresense/companion/{device.Id}/attributes" topic) containing null/empty
probability and location fields to clear retained values even if
probabilityAttributes is null and bestScenario is null, ensuring retained
probabilities are removed when Bayesian outputs are cleared.
♻️ Duplicate comments (2)
src/Services/BayesianProbabilityPublisher.cs (1)

100-118: ⚠️ Potential issue | 🟡 Minor

Use the rounded probability for discovery gating.

Line 113 still compares the raw value even though Lines 102-107 round and persist the 4-decimal payload value. A room at 0.09996 is exposed as 0.1000 in device.BayesianProbabilities, but it still misses discovery when the threshold is 0.1.

🔧 Suggested fix
-            if (!IsSyntheticRoom(roomName) && probability >= config.DiscoveryThreshold)
+            if (!IsSyntheticRoom(roomName) && roundedProbability >= config.DiscoveryThreshold)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Services/BayesianProbabilityPublisher.cs` around lines 100 - 118, The
discovery gating currently uses the raw probability variable (probability) but
you round and store the 4-decimal payload (roundedProbability); change the
gating to use roundedProbability when checking against config.DiscoveryThreshold
so values like 0.09996 that become 0.1000 will pass the threshold; update the
condition in the block that calls IsSyntheticRoom, CreateProbabilityDiscovery,
and discovery.Send to compare roundedProbability >= config.DiscoveryThreshold
(leave the rest of the logic with device.BayesianDiscoveries and
device.HassAutoDiscovery unchanged).
src/Services/MultiScenarioLocator.cs (1)

171-177: ⚠️ Potential issue | 🟠 Major

Sanitized-key collisions can still break probability publishing.

SanitizeSegment is not one-to-one: Living Room and Living-Room both collapse to the same key. ToDictionary will throw here, and src/Services/BayesianProbabilityPublisher.cs:185-210 derives discovery IDs from the same sanitized segment, so this becomes both a runtime failure and an entity-ID collision. Group or disambiguate by the sanitized key before materializing the payload, and use that same normalized key for discovery creation.

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

In `@src/Services/MultiScenarioLocator.cs` around lines 171 - 177, The current
ToDictionary call can throw when BayesianProbabilityPublisher.SanitizeSegment
produces duplicate keys (e.g., "Living Room" vs "Living-Room"); fix by grouping
device.BayesianProbabilities by the sanitized key (use
BayesianProbabilityPublisher.SanitizeSegment(kvp.Key)), then materialize
probabilityAttributes from the groups: if a group has a single member use the
sanitized key, if multiple members disambiguate by appending a stable suffix
(e.g., incrementing index or short hash of the original key) to produce unique
normalized keys, assign the aggregated/selected probability value (e.g., sum or
chosen strategy) rounded as before, and ensure the same normalized keys are used
when creating discoveries in BayesianProbabilityPublisher so IDs remain
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Locators/NearestNode.cs`:
- Around line 37-38: The code currently sets scenario.Floor =
node.Floors?.FirstOrDefault(), which chooses by array order and can pick the
wrong floor when multiple floors attach to a node; replace this with geometric
floor resolution: iterate node.Floors and select the floor whose geometry
contains the node's position (e.g., using a Floor.Contains(node.Position) or
checking a polygon/mesh containment test), and if containment is ambiguous
choose a deterministic tiebreaker (closest vertical distance or highest
intersection confidence); update the assignment to set scenario.Floor to that
contained floor (fall back to FirstOrDefault only if no containment test
passes).

In `@src/Services/MultiScenarioLocator.cs`:
- Around line 162-183: The new ConfigBayesianProbabilities.Retain flag is never
applied in MultiScenarioLocator: thread its value into the bayesianPublisher
publish/clear calls instead of only checking Enabled. Update
MultiScenarioLocator to read state.Config.BayesianProbabilities?.Retain and pass
that boolean into bayesianPublisher.PublishProbabilitySensorsAsync and
bayesianPublisher.ClearProbabilityOutputsAsync (or add overloads/parameters on
those methods if needed), and ensure the subsequent shared attributes publish
uses that retain value when publishing MQTT attributes; alternatively remove
Retain from the config contract if you decide not to support non-retained
probability publishing yet.

---

Outside diff comments:
In `@src/Models/Config.Clone.cs`:
- Around line 35-42: ConfigLocators.Clone currently only copies NadarayaWatson,
NelderMead, and NearestNode, which drops Bfgs, Mle, and MultiFloor settings; add
cloning for the Bfgs, Mle, and MultiFloor properties in ConfigLocators.Clone and
implement Clone methods on BfgsConfig, MleConfig, and MultiFloorConfig (e.g.,
BfgsConfig.Clone, MleConfig.Clone, MultiFloorConfig.Clone) following the
existing pattern: return a new instance copying simple fields, cloning nested
objects (like Weighting.Clone()) and copying arrays with .ToArray() (e.g.,
Floors?.ToArray()) so loaded config values are preserved rather than reset.

In `@src/Services/MultiScenarioLocator.cs`:
- Around line 180-210: When ClearProbabilityOutputsAsync (referenced as
bayesianPublisher.ClearProbabilityOutputsAsync) returns true but bestScenario is
null and probabilityAttributes is null, the existing condition prevents sending
an /attributes republish and leaves stale retained probabilities on the broker;
update the logic so that when probabilityChanged is true you still enqueue an
attributes publish (using mqtt.TryEnqueueAsync for the
"espresense/companion/{device.Id}/attributes" topic) containing null/empty
probability and location fields to clear retained values even if
probabilityAttributes is null and bestScenario is null, ensuring retained
probabilities are removed when Bayesian outputs are cleared.

---

Duplicate comments:
In `@src/Services/BayesianProbabilityPublisher.cs`:
- Around line 100-118: The discovery gating currently uses the raw probability
variable (probability) but you round and store the 4-decimal payload
(roundedProbability); change the gating to use roundedProbability when checking
against config.DiscoveryThreshold so values like 0.09996 that become 0.1000 will
pass the threshold; update the condition in the block that calls
IsSyntheticRoom, CreateProbabilityDiscovery, and discovery.Send to compare
roundedProbability >= config.DiscoveryThreshold (leave the rest of the logic
with device.BayesianDiscoveries and device.HassAutoDiscovery unchanged).

In `@src/Services/MultiScenarioLocator.cs`:
- Around line 171-177: The current ToDictionary call can throw when
BayesianProbabilityPublisher.SanitizeSegment produces duplicate keys (e.g.,
"Living Room" vs "Living-Room"); fix by grouping device.BayesianProbabilities by
the sanitized key (use BayesianProbabilityPublisher.SanitizeSegment(kvp.Key)),
then materialize probabilityAttributes from the groups: if a group has a single
member use the sanitized key, if multiple members disambiguate by appending a
stable suffix (e.g., incrementing index or short hash of the original key) to
produce unique normalized keys, assign the aggregated/selected probability value
(e.g., sum or chosen strategy) rounded as before, and ensure the same normalized
keys are used when creating discoveries in BayesianProbabilityPublisher so IDs
remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bc466e86-5fb9-4d99-b9b2-bde8ed1efbc7

📥 Commits

Reviewing files that changed from the base of the PR and between 648eaba and cc0c4ce.

📒 Files selected for processing (13)
  • README.md
  • src/Locators/NearestNode.cs
  • src/Models/AutoDiscovery.cs
  • src/Models/Config.Clone.cs
  • src/Models/Config.cs
  • src/Models/Device.cs
  • src/Program.cs
  • src/Services/BayesianProbabilityPublisher.cs
  • src/Services/DeviceTracker.cs
  • src/Services/MultiScenarioLocator.cs
  • src/config.example.yaml
  • tests/ESPresense.Companion.Tests/FilteringTests.cs
  • tests/ESPresense.Companion.Tests/MultiScenarioLocatorTests.cs
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Models/Config.cs
  • src/Services/DeviceTracker.cs
  • src/config.example.yaml
  • src/Program.cs
  • src/Models/Device.cs
  • README.md

Comment on lines +37 to +38
// Find the floor containing the node
scenario.Floor = node.Floors?.FirstOrDefault();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restore geometric floor resolution instead of array-order selection

FirstOrDefault() on node.Floors can assign the wrong floor when multiple floors are attached to a node. Line 37 says “find the floor containing the node,” but Line 38 does not perform containment at all. This can propagate incorrect floor state into MultiScenarioLocator and Bayesian probability aggregation.

Proposed fix
-        // Find the floor containing the node
-        scenario.Floor = node.Floors?.FirstOrDefault();
+        // Find the floor containing the node
+        scenario.Floor = node.Floors == null
+            ? null
+            : SpatialUtils.FindFloorContaining(location, node.Floors) ?? node.Floors.FirstOrDefault();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Locators/NearestNode.cs` around lines 37 - 38, The code currently sets
scenario.Floor = node.Floors?.FirstOrDefault(), which chooses by array order and
can pick the wrong floor when multiple floors attach to a node; replace this
with geometric floor resolution: iterate node.Floors and select the floor whose
geometry contains the node's position (e.g., using a
Floor.Contains(node.Position) or checking a polygon/mesh containment test), and
if containment is ambiguous choose a deterministic tiebreaker (closest vertical
distance or highest intersection confidence); update the assignment to set
scenario.Floor to that contained floor (fall back to FirstOrDefault only if no
containment test passes).

Comment on lines +162 to +183
var probabilityConfig = state.Config?.BayesianProbabilities;
Dictionary<string, double>? probabilityAttributes = null;
var probabilityChanged = false;

if (probabilityConfig is { Enabled: true })
{
var probabilityVector = bayesianPublisher.BuildProbabilityVector(device, bestScenario);
probabilityChanged = await bayesianPublisher.PublishProbabilitySensorsAsync(device, probabilityVector, probabilityConfig);

// Use device.BayesianProbabilities (not probabilityVector) to include sticky rooms with 0 probability
// This ensures all discovered sensors appear in the attributes payload
if (device.BayesianProbabilities.Count > 0)
{
probabilityAttributes = device.BayesianProbabilities.ToDictionary(
kvp => BayesianProbabilityPublisher.SanitizeSegment(kvp.Key),
kvp => Math.Round(kvp.Value, 4));
}
}
else
{
probabilityChanged = await bayesianPublisher.ClearProbabilityOutputsAsync(device);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

bayesian_probabilities.retain is a no-op right now.

This branch only looks at Enabled. The new Retain field on ConfigBayesianProbabilities is never applied here, discovery configs are still retained in src/Models/AutoDiscovery.cs:49-56, and the shared /attributes payload is still published with retain: true at Line 242. Setting bayesian_probabilities.retain: false therefore won't change MQTT behavior.

Either thread the setting into the actual publish path or remove it from the config contract until non-retained probability publishing is supported.

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

In `@src/Services/MultiScenarioLocator.cs` around lines 162 - 183, The new
ConfigBayesianProbabilities.Retain flag is never applied in
MultiScenarioLocator: thread its value into the bayesianPublisher publish/clear
calls instead of only checking Enabled. Update MultiScenarioLocator to read
state.Config.BayesianProbabilities?.Retain and pass that boolean into
bayesianPublisher.PublishProbabilitySensorsAsync and
bayesianPublisher.ClearProbabilityOutputsAsync (or add overloads/parameters on
those methods if needed), and ensure the subsequent shared attributes publish
uses that retain value when publishing MQTT attributes; alternatively remove
Retain from the config contract if you decide not to support non-retained
probability publishing yet.

@DTTerastar
DTTerastar force-pushed the main branch 3 times, most recently from e2b2560 to 8231de3 Compare March 18, 2026 22:36
@sensiebot

sensiebot Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Sensie: This PR is currently conflicting with main and needs to be rebased. The feature looks good but requires conflict resolution before merge. Deferring for maintainer action. 📡

@sensiebot

sensiebot Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

@dependabot rebase

@sensiebot

sensiebot Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Build Status: 2 Tests Failing ❌

The latest build shows 2 unit test failures that need to be addressed before merge:

1. (NearestNodeTests.cs:144)

  • Error: Expected floor to be <Id: inside> but was <Id: outside>
  • Cause: Polygon containment check for floor selection is returning the wrong result

2. (MultiScenarioLocatorTests.cs:176)

  • Error: Expected: not null, But was: null
  • Cause: Bayesian probability publish is not producing the expected output

Additionally, CodeRabbit has flagged these unresolved issues:

  • ConfigLocators.Clone is missing Bfgs, Mle, and MultiFloor sub-configs — settings get reset to defaults
  • Stale retained attributes — ClearProbabilityOutputsAsync path can leave stale MQTT retained messages
  • Discovery gating uses raw probability instead of rounded — misses threshold (0.09996 → 0.1000)
  • Sanitized-key collisions — "Living Room" vs "Living-Room" both collapse to same key causing ToDictionary throw

Please address the test failures and CodeRabbit issues, then the PR should be ready to merge. 146/150 tests pass — just these 2 need fixing.

@stale

stale Bot commented Jun 29, 2026

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the stale label Jun 29, 2026
@stale stale Bot closed this Jul 6, 2026
@DTTerastar DTTerastar reopened this Jul 21, 2026
@stale stale Bot removed the stale label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants