Skip to content
Open
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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## 1.6.0

### New Features

- **Path Tracking Timeout**: Automatically resets the state machine when someone enters halfway and turns back, preventing stuck states. Configurable via `path_tracking_timeout` (default: 3s).
- **Adaptive Thresholds**: Continuously adjusts idle baseline to handle environmental drift (temperature changes, lighting). Uses Exponential Moving Average for smooth updates. Configurable via `adaptive_threshold` block.
- **Timeout Event**: The `entry_exit_event` text sensor now publishes "Timeout" when an incomplete crossing is detected.

### Improvements

- Moved path tracking state from static variables to class members for better encapsulation and testability
- Added sanity checks for adaptive threshold updates (rejects readings >20% different from current idle)
- Enhanced logging for debugging path tracking and threshold updates
- Added configuration dump for new features in `dump_config()`

### Configuration

New configuration options:

```yaml
roode:
# Reset state machine if no activity within this period (prevents stuck states)
path_tracking_timeout: 3s

# Continuously adjust thresholds for environmental drift
adaptive_threshold:
enabled: true
update_interval: 60s # How long zones must be empty before updating
alpha: 0.05 # EMA smoothing factor (0.01-0.5)
```

## 1.5.4

- Fix ESPHome 2026.2+ compatibility by migrating ESP8266 configurations to new platform format
Expand Down
97 changes: 96 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![GitHub release](https://img.shields.io/github/v/tag/Lyr3x/Roode?style=flat-square)](https://GitHub.com/Lyr3x/Roode/releases/)
[![Build](https://img.shields.io/github/workflow/status/Lyr3x/Roode/CI?style=flat-square)](https://github.com/Lyr3x/Roode/blob/master/.github/workflows/ci.yml)
[![Maintenance](https://img.shields.io/maintenance/yes/2023?style=flat-square)](https://GitHub.com/Lyr3x/Roode/graphs/commit-activity)
[![Maintenance](https://img.shields.io/maintenance/yes/2026?style=flat-square)](https://GitHub.com/Lyr3x/Roode/graphs/commit-activity)

[![Roode community](https://img.shields.io/discord/879407995837087804.svg?label=Discord&logo=Discord&colorB=7289da&style=for-the-badge)](https://discord.gg/hU9SvSXMHs)

Expand All @@ -16,6 +16,7 @@ People counter working with any smart home system which supports ESPHome/MQTT li
- [Platform Setup](#platform-setup)
- [Sensors](#sensors)
- [Threshold distance](#threshold-distance)
- [Robustness Features](#robustness-features)
- [Algorithm](#algorithm)
- [FAQ/Troubleshoot](#faqtroubleshoot)

Expand Down Expand Up @@ -145,6 +146,18 @@ roode:
# min: 50mm
# max: 234cm

# Path tracking timeout: resets state machine if someone enters halfway and turns back.
# This prevents the algorithm from getting stuck waiting for a crossing to complete.
# Set to 0s to disable timeout (not recommended).
path_tracking_timeout: 3s

# Adaptive threshold: continuously adjusts idle baseline to handle environmental drift
# (temperature changes, lighting variations, etc.). Uses Exponential Moving Average.
adaptive_threshold:
enabled: true # Enable/disable adaptive updates
update_interval: 60s # How long zones must be empty before updating
alpha: 0.05 # EMA smoothing factor (0.01-0.5, lower = slower adaptation)

# The people counting algorithm works by splitting the sensor's capability reading area into two zones.
# This allows for detecting whether a crossing is an entry or exit based on which zones was crossed first.
zones:
Expand Down Expand Up @@ -244,6 +257,50 @@ min_threshold_percentage: 10% = 200
All distances smaller then 200mm and greater then 1760mm will be ignored.
```

### Robustness Features

Roode v1.6.0 introduces two features to improve counting reliability:

#### Path Tracking Timeout

**Problem:** If someone enters the detection area but turns back without completing a crossing, the state machine could get stuck waiting for the crossing to complete.

**Solution:** The `path_tracking_timeout` option (default: 3 seconds) automatically resets the state machine if no state change occurs within the timeout period.

```yaml
roode:
path_tracking_timeout: 3s # Reset if no activity for 3 seconds
```

When a timeout occurs:
- The state machine is reset to idle
- A "Timeout" event is published to the `entry_exit_event` sensor
- No count change occurs (the incomplete crossing is ignored)

You can monitor these events in Home Assistant to detect frequent turn-backs.

#### Adaptive Thresholds

**Problem:** Environmental changes (temperature drift, lighting variations) can cause the idle distance to shift over time, degrading detection accuracy.

**Solution:** The `adaptive_threshold` feature continuously updates the idle baseline using an Exponential Moving Average (EMA) when both zones are empty.

```yaml
roode:
adaptive_threshold:
enabled: true # Default: enabled
update_interval: 60s # Update after zones empty for 60s
alpha: 0.05 # Smoothing factor (lower = slower adaptation)
```

How it works:
1. When both zones are empty for the configured interval, the current distance is sampled
2. The idle baseline is updated: `new_idle = (1 - alpha) × old_idle + alpha × reading`
3. Min/max thresholds are recalculated based on the new idle value
4. A sanity check rejects readings that differ more than 20% from current idle

The threshold sensors will reflect the updated values in Home Assistant.

## Algorithm

The implemented Algorithm is an improved version of my own implementation which checks the direction of a movement through two defined zones. ST implemented a nice and efficient way to track the path from one to the other direction. I migrated the algorigthm with some changes into the Roode project.
Expand All @@ -256,6 +313,8 @@ The concept of path tracking is the detecion of a human:

That way we can ensure the direction of movement.

The algorithm includes timeout handling: if the sequence doesn't complete within the configured timeout (default 3s), the state machine resets. This handles cases where someone enters the detection area but turns back without completing a crossing.

The sensor creates a 16x16 grid and the final distance is computed by taking the average of the distance of the values of the grid.
We are defining two different Region of Interest (ROI) inside this grid. Then the sensor will measure the two distances in the two zones and will detect any presence and tracks the path to receive the direction.

Expand Down Expand Up @@ -330,6 +389,42 @@ lower right.
3. Light interference (You will see a lot of noise)
4. Bad connections

---

**Question:** The counter seems to get stuck or miss counts after some time?

**Answer:** This could be environmental drift affecting the thresholds. Check if:

1. **Adaptive thresholds are enabled** (default). If disabled, enable them:
```yaml
roode:
adaptive_threshold:
enabled: true
```
2. **Check the threshold sensors** in Home Assistant - if they're drifting significantly from initial calibration, adaptive thresholds should help.
3. **Recalibrate manually** if the environment has changed dramatically (e.g., new flooring, moved sensor).

---

**Question:** I see many "Timeout" events in the entry_exit_event sensor?

**Answer:** Timeout events occur when someone enters the detection area but doesn't complete a crossing. This is normal behavior. However, frequent timeouts might indicate:

1. **Timeout too short**: Increase `path_tracking_timeout` if people walk slowly through the doorway
2. **Threshold issues**: The detection thresholds might be triggering on objects that aren't people (pets, swinging doors)
3. **Sensor placement**: The sensor might be detecting movement outside the intended area

---

**Question:** The count seems to drift over long periods?

**Answer:** People counting will never be 100% accurate. To minimize drift:

1. Ensure adaptive thresholds are enabled
2. Adjust the detection thresholds based on your mounting height
3. Use the people_counter number entity to manually correct the count when needed
4. Consider automations that reset the count when the room is known to be empty (e.g., at night)

## Sponsors

Thank you very much for you sponsorship!
Expand Down
19 changes: 19 additions & 0 deletions ci/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ external_components:
esphome:
name: $devicename

vl53l1x:
calibration:
ranging: auto

roode:
id: roode_platform
sampling: 2
roi: { height: 16, width: 6 }
detection_thresholds:
max: 85%
# Test new v1.6.0 features
path_tracking_timeout: 3s
adaptive_threshold:
enabled: true
update_interval: 60s
alpha: 0.05
zones:
invert: false

button:
- platform: restart
name: $friendly_name Restart
Expand Down
7 changes: 0 additions & 7 deletions ci/esp32.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,3 @@ esp32:
i2c:
sda: 21
scl: 22

# VL53L1X sensor configuration is separate from Roode people counting algorithm
vl53l1x:
calibration:

roode:
id: roode_platform
7 changes: 0 additions & 7 deletions ci/esp8266.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,3 @@ esp8266:
i2c:
sda: 4
scl: 5

# VL53L1X sensor configuration is separate from Roode people counting algorithm
vl53l1x:
calibration:

roode:
id: roode_platform
30 changes: 30 additions & 0 deletions components/roode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@
CONF_SAMPLING = "sampling"
CONF_ZONES = "zones"

# New configuration options for high-priority features
CONF_PATH_TRACKING_TIMEOUT = "path_tracking_timeout"
CONF_ADAPTIVE_THRESHOLD = "adaptive_threshold"
CONF_ENABLED = "enabled"
CONF_UPDATE_INTERVAL = "update_interval"
CONF_ALPHA = "alpha"

Orientation = roode_ns.enum("Orientation")
ORIENTATION_VALUES = {
"parallel": Orientation.Parallel,
Expand Down Expand Up @@ -66,6 +73,15 @@
}
)

# Adaptive threshold configuration schema
ADAPTIVE_THRESHOLD_SCHEMA = NullableSchema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ALPHA, default=0.05): cv.float_range(min=0.01, max=0.5),
}
)

CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Roode),
Expand All @@ -81,6 +97,11 @@
cv.Optional(CONF_EXIT_ZONE, default={}): ZONE_SCHEMA,
}
),
# Path tracking timeout: resets state machine if no activity within this period
# Prevents stuck states when someone enters halfway and turns back
cv.Optional(CONF_PATH_TRACKING_TIMEOUT, default="3s"): cv.positive_time_period_milliseconds,
# Adaptive threshold: continuously adjusts idle baseline to handle environmental drift
cv.Optional(CONF_ADAPTIVE_THRESHOLD, default={}): ADAPTIVE_THRESHOLD_SCHEMA,
}
).extend(cv.COMPONENT_SCHEMA)

Expand All @@ -98,6 +119,15 @@ async def to_code(config: Dict):
setup_zone(CONF_ENTRY_ZONE, config, roode)
setup_zone(CONF_EXIT_ZONE, config, roode)

# Path tracking timeout configuration
cg.add(roode.set_path_tracking_timeout(config[CONF_PATH_TRACKING_TIMEOUT]))

# Adaptive threshold configuration
adaptive_config = config[CONF_ADAPTIVE_THRESHOLD]
cg.add(roode.set_adaptive_threshold_enabled(adaptive_config[CONF_ENABLED]))
cg.add(roode.set_adaptive_threshold_update_interval(adaptive_config[CONF_UPDATE_INTERVAL]))
cg.add(roode.set_adaptive_threshold_alpha(adaptive_config[CONF_ALPHA]))


def setup_zone(name: str, config: Dict, roode: cg.Pvariable):
zone_config = config[CONF_ZONES][name]
Expand Down
Loading