Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A Home Assistant Add-on / Docker container that solves indoor positions using MQ
- Manages and configures ESPresense nodes
- Updates node firmware
- Adjusts device-specific settings
- Publishes Bayesian room probabilities for fuzzy automations
- Monitors and controls automatic node optimization

![image](https://user-images.githubusercontent.com/1491145/208942192-d8716e50-c822-48a7-a6d3-46b53ab9373e.png)
Expand All @@ -19,6 +20,42 @@ A Home Assistant Add-on / Docker container that solves indoor positions using MQ
3. [Node Setup](https://espresense.com/companion/configuration#node-placement)
4. [Optimization Guide](https://espresense.com/companion/optimization)

## Bayesian probability output

Enable the optional Bayesian publisher to expose per-room probability vectors alongside the traditional `device_tracker` state.
Set `bayesian_probabilities.enabled: true` in `config.yaml` to turn it on:

```yaml
bayesian_probabilities:
enabled: true
discovery_threshold: 0.1 # auto-create sensors above this probability
retain: true # keep MQTT state so HA restores after restart
```

When enabled the companion:

- Publishes `espresense/companion/<device_id>/probabilities/<room>` topics containing a `0.0-1.0` float for each room.
- Adds a `probabilities` object to the device attribute payload (`espresense/companion/<device_id>/attributes`).
- Auto-discovers Home Assistant `sensor` entities for any room whose probability crosses the configured threshold.

You can fuse multiple device probabilities into a person-level Bayesian sensor in Home Assistant:

```yaml
sensor:
- platform: bayesian
name: "Pat in Kitchen"
prior: 0.5
observations:
- platform: template
value_template: "{{ states('sensor.pat_phone_kitchen_probability') | float }}"
probability: 0.6
- platform: template
value_template: "{{ states('sensor.pat_watch_kitchen_probability') | float }}"
probability: 0.9
```

Automations can then trigger on thresholds (for example, turn on lights when `sensor.pat_in_kitchen > 0.7`).

## Need Help?
- Join our [Discord Community](https://discord.gg/jbqmn7V6n6)
- Check the [Troubleshooting Guide](https://espresense.com/companion/troubleshooting)
Expand Down
10 changes: 10 additions & 0 deletions src/Models/AutoDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ public class DiscoveryRecord

[JsonProperty("status_topic")] public string? EntityStatusTopic { get; set; }

[JsonProperty("device_class")] public string? DeviceClass { get; set; }

[JsonProperty("state_class")] public string? StateClass { get; set; }

[JsonProperty("unit_of_measurement")] public string? UnitOfMeasurement { get; set; }

[JsonProperty("value_template")] public string? ValueTemplate { get; set; }

[JsonProperty("icon")] public string? Icon { get; set; }

[JsonProperty("device")] public DeviceRecord? Device { get; set; }

[JsonProperty("origin")] public OriginRecord? Origin { get; set; }
Expand Down
16 changes: 15 additions & 1 deletion src/Models/Config.Clone.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
{
return new Config
{
Mqtt = Mqtt?.Clone(),

Check warning on line 15 in src/Models/Config.Clone.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference assignment.
Bounds = Bounds?.Select(b => b.ToArray()).ToArray(),
Timeout = Timeout,
AwayTimeout = AwayTimeout,
Expand All @@ -24,7 +24,8 @@
ExcludeDevices = ExcludeDevices.Select(d => d.Clone()).ToArray(),
History = History.Clone(),
Locators = Locators.Clone(),
Optimization = Optimization.Clone()
Optimization = Optimization.Clone(),
BayesianProbabilities = BayesianProbabilities.Clone()
};
}
}
Expand Down Expand Up @@ -116,6 +117,19 @@
}
}

public partial class ConfigBayesianProbabilities
{
public ConfigBayesianProbabilities Clone()
{
return new ConfigBayesianProbabilities
{
Enabled = Enabled,
DiscoveryThreshold = DiscoveryThreshold,
Retain = Retain
};
}
}

public partial class ConfigHistory
{
public ConfigHistory Clone()
Expand Down
15 changes: 15 additions & 0 deletions src/Models/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public partial class Config
[YamlMember(Alias = "history")]
public ConfigHistory History { get; set; } = new();

[YamlMember(Alias = "bayesian_probabilities")]
public ConfigBayesianProbabilities BayesianProbabilities { get; set; } = new();

[YamlMember(Alias = "bounds")]
public double[][] Bounds { get; set; } = [];

Expand Down Expand Up @@ -143,6 +146,18 @@ public partial class ConfigOptimization
[YamlIgnore] public double RmseWeight => Weights.TryGetValue("rmse", out var val) ? val : 0.5;
}

public partial class ConfigBayesianProbabilities
{
[YamlMember(Alias = "enabled")]
public bool Enabled { get; set; } = false;

[YamlMember(Alias = "discovery_threshold")]
public double DiscoveryThreshold { get; set; } = 0.1;

[YamlMember(Alias = "retain")]
public bool Retain { get; set; } = true;
}

public partial class ConfigHistory
{
[YamlMember(Alias = "enabled")] public bool Enabled { get; set; } = false;
Expand Down
21 changes: 19 additions & 2 deletions src/Models/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public DateTime? LastSeen
{
get
{
var lastSeen = BestScenario?.LastHit ?? Nodes.Values.Max(a => a.LastHit);
var lastSeen = BestScenario?.LastHit ?? Nodes.Values.Max(a => a.LastHit);
if (_lastSeen == null || lastSeen > _lastSeen) _lastSeen = lastSeen;
return _lastSeen;
}
Expand All @@ -82,6 +82,12 @@ public DateTime? LastSeen
[STJ.JsonIgnore] public Scenario? BestScenario { get; set; }
[STJ.JsonIgnore] public IList<Scenario> Scenarios { get; } = new List<Scenario>();

[STJ.JsonIgnore]
public ConcurrentDictionary<string, double> BayesianProbabilities { get; } = new(StringComparer.OrdinalIgnoreCase);

[STJ.JsonIgnore]
public ConcurrentDictionary<string, AutoDiscovery> BayesianDiscoveries { get; } = new(StringComparer.OrdinalIgnoreCase);

[STJ.JsonConverter(typeof(Point3DConverter))]
public Point3D? Location => Anchor?.Location ?? (BestScenario == null ? null : _kalmanLocation.Location);

Expand Down Expand Up @@ -113,7 +119,7 @@ public double? MeasuredRefRssi
{
var currentNodes = Nodes.Values.Where(dn => dn.Current).ToList();
if (currentNodes.Count == 0) return null;

var refRssiValues = currentNodes.Where(dn => dn.RefRssi != 0).Select(dn => dn.RefRssi).ToList();
return refRssiValues.Count > 0 ? refRssiValues.Average() : null;
}
Expand Down Expand Up @@ -143,6 +149,17 @@ public void SetAnchor(DeviceAnchor? anchor)
}
}

public void ResetBayesianState()
{
foreach (var discovery in BayesianDiscoveries.Values.ToList())
{
HassAutoDiscovery.Remove(discovery);
}

BayesianDiscoveries.Clear();
BayesianProbabilities.Clear();
}

public virtual IEnumerable<KeyValuePair<string, string>> GetDetails()
{
yield return new KeyValuePair<string, string>("Best Scenario", $"{BestScenario?.Name}");
Expand Down
1 change: 1 addition & 0 deletions src/Services/DeviceTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ private async Task<bool> CheckDeviceAsync(Device device)
Log.Information("[-] Track {Device}", device);
foreach (var ad in device.HassAutoDiscovery)
await ad.Delete(mqtt);
device.ResetBayesianState();
}
return true;
}
Expand Down
Loading
Loading