Skip to content
Merged
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
35 changes: 34 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The current filter accepts the first valid point, then rejects updates when:
- GPS accuracy exceeds the configured maximum accuracy
- The incoming point is an exact duplicate of the last accepted point
- The calculated movement speed exceeds the configured maximum speed
- The calculated movement speed and reported source speed differ by more than
the configured maximum speed difference

The integration tracks both calculated speed and reported source speed. Reported
speed is converted from m/s to km/h when present.
Expand Down Expand Up @@ -61,14 +63,25 @@ the exact entity IDs from the configured GPS Filter device name.
- `sensor.gps_filter_last_received_timestamp`
- `sensor.gps_filter_last_accepted_timestamp`
- `sensor.gps_filter_acceptance_rate`
- `sensor.gps_filter_seconds_since_last_accepted`
- `sensor.gps_filter_max_speed_threshold`
- `sensor.gps_filter_max_speed_difference_threshold`
- `sensor.gps_filter_max_accuracy_threshold`
- `sensor.gps_filter_total_received_count`
- `sensor.gps_filter_total_rejected_count`
- `sensor.gps_filter_max_distance`
- `sensor.gps_filter_max_calculated_speed`
- `sensor.gps_filter_max_reported_speed`
- `sensor.gps_filter_max_accuracy`
- `sensor.gps_filter_max_rejected_distance`
- `sensor.gps_filter_max_rejected_calculated_speed`
- `sensor.gps_filter_max_rejected_reported_speed`
- `sensor.gps_filter_max_rejected_accuracy`
- `sensor.gps_filter_accepted_count`
- `sensor.gps_filter_duplicate_count`
- `sensor.gps_filter_accuracy_rejections`
- `sensor.gps_filter_speed_rejections`
- `sensor.gps_filter_speed_consistency_rejections`

These sensors are intended for a developer/debug dashboard during drive testing.

Expand All @@ -87,14 +100,25 @@ For one configured tracker, the default generated names are expected to be:
- `sensor.gps_filter_last_received_timestamp`
- `sensor.gps_filter_last_accepted_timestamp`
- `sensor.gps_filter_acceptance_rate`
- `sensor.gps_filter_seconds_since_last_accepted`
- `sensor.gps_filter_max_speed_threshold`
- `sensor.gps_filter_max_speed_difference_threshold`
- `sensor.gps_filter_max_accuracy_threshold`
- `sensor.gps_filter_total_received_count`
- `sensor.gps_filter_total_rejected_count`
- `sensor.gps_filter_max_distance`
- `sensor.gps_filter_max_calculated_speed`
- `sensor.gps_filter_max_reported_speed`
- `sensor.gps_filter_max_accuracy`
- `sensor.gps_filter_max_rejected_distance`
- `sensor.gps_filter_max_rejected_calculated_speed`
- `sensor.gps_filter_max_rejected_reported_speed`
- `sensor.gps_filter_max_rejected_accuracy`
- `sensor.gps_filter_accepted_count`
- `sensor.gps_filter_duplicate_count`
- `sensor.gps_filter_accuracy_rejections`
- `sensor.gps_filter_speed_rejections`
- `sensor.gps_filter_speed_consistency_rejections`

When multiple GPS Filter entries are configured, each entry uses its config
entry title as the Home Assistant device name, for example
Expand All @@ -116,15 +140,17 @@ use `TOTAL_INCREASING`.
6. Set:
- Maximum speed in km/h
- Maximum GPS accuracy in meters
- Maximum speed difference in km/h

Both threshold values must be greater than zero.
All threshold values must be greater than zero.

## Options

After setup, thresholds can be edited from the integration options:

- Maximum speed
- Maximum GPS accuracy
- Maximum speed difference

Changing options reloads the config entry so the coordinator and filter engine
use the updated values.
Expand Down Expand Up @@ -172,22 +198,29 @@ decisions. Each timeline entry contains:
- distance_m
- calculated_speed_kmh
- reported_speed_kmh
- seconds_since_last_accepted

The timeline is not persisted and is reset when Home Assistant restarts or when
`gps_filter.reset_filter` is called.

The diagnostics summary includes:

- total_received_count
- total_rejected_count
- accepted_count
- duplicate_count
- accuracy_rejections
- speed_rejections
- speed_consistency_rejections
- acceptance_rate_percent
- max_distance_m
- max_calculated_speed_kmh
- max_reported_speed_kmh
- max_accuracy_m
- max_rejected_distance_m
- max_rejected_calculated_speed_kmh
- max_rejected_reported_speed_kmh
- max_rejected_accuracy_m

Summary statistics are in-memory only. They reset when
`gps_filter.reset_statistics` or `gps_filter.reset_filter` is called.
Expand Down
17 changes: 17 additions & 0 deletions custom_components/gps_filter/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
from .const import (
CONF_MAX_ACCURACY,
CONF_MAX_SPEED,
CONF_MAX_SPEED_DIFFERENCE,
CONF_SOURCE,
DEFAULT_MAX_ACCURACY,
DEFAULT_MAX_SPEED,
DEFAULT_MAX_SPEED_DIFFERENCE,
DOMAIN,
)

Expand All @@ -36,6 +38,13 @@ def _options_defaults(entry: ConfigEntry) -> dict[str, float]:
CONF_MAX_ACCURACY,
entry.data.get(CONF_MAX_ACCURACY, DEFAULT_MAX_ACCURACY),
),
CONF_MAX_SPEED_DIFFERENCE: entry.options.get(
CONF_MAX_SPEED_DIFFERENCE,
entry.data.get(
CONF_MAX_SPEED_DIFFERENCE,
DEFAULT_MAX_SPEED_DIFFERENCE,
),
),
}


Expand All @@ -56,6 +65,10 @@ def _get_user_data_schema() -> vol.Schema:
CONF_MAX_ACCURACY,
default=DEFAULT_MAX_ACCURACY,
): POSITIVE_FLOAT,
vol.Required(
CONF_MAX_SPEED_DIFFERENCE,
default=DEFAULT_MAX_SPEED_DIFFERENCE,
): POSITIVE_FLOAT,
}
)

Expand All @@ -72,6 +85,10 @@ def _get_options_schema(defaults: dict[str, float]) -> vol.Schema:
CONF_MAX_ACCURACY,
default=defaults[CONF_MAX_ACCURACY],
): POSITIVE_FLOAT,
vol.Required(
CONF_MAX_SPEED_DIFFERENCE,
default=defaults[CONF_MAX_SPEED_DIFFERENCE],
): POSITIVE_FLOAT,
}
)

Expand Down
2 changes: 2 additions & 0 deletions custom_components/gps_filter/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
CONF_SOURCE: Final = "source"
CONF_MAX_SPEED: Final = "max_speed"
CONF_MAX_ACCURACY: Final = "max_accuracy"
CONF_MAX_SPEED_DIFFERENCE: Final = "max_speed_difference_kmh"

DEFAULT_MAX_SPEED: Final = 220.0
DEFAULT_MAX_ACCURACY: Final = 30.0
DEFAULT_MAX_SPEED_DIFFERENCE: Final = 40.0

PLATFORMS: Final = ["button", "device_tracker", "sensor"]
52 changes: 51 additions & 1 deletion custom_components/gps_filter/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import CONF_MAX_ACCURACY, CONF_MAX_SPEED
from .const import CONF_MAX_ACCURACY, CONF_MAX_SPEED, CONF_MAX_SPEED_DIFFERENCE
from .filter_engine import GPSFilterEngine
from .helpers import get_config_value
from .models import (
Expand Down Expand Up @@ -59,6 +59,10 @@ def __init__(
self.engine = GPSFilterEngine(
max_speed=get_config_value(entry, CONF_MAX_SPEED),
max_accuracy=get_config_value(entry, CONF_MAX_ACCURACY),
max_speed_difference_kmh=get_config_value(
entry,
CONF_MAX_SPEED_DIFFERENCE,
),
)

self._remove_listener = None
Expand Down Expand Up @@ -103,6 +107,16 @@ def acceptance_rate_percent(self) -> float:
* 100
)

@property
def total_rejected_count(self) -> int:
"""Return the total number of rejected points."""
return (
self.data.engine_stats.duplicate
+ self.data.engine_stats.accuracy_rejections
+ self.data.engine_stats.speed_rejections
+ self.data.engine_stats.speed_consistency_rejections
)

async def async_start(self) -> None:
"""Start listening for GPS updates."""

Expand Down Expand Up @@ -164,6 +178,7 @@ def _state_changed(self, event: Event) -> None:
distance_m=result.distance_m,
calculated_speed_kmh=result.calculated_speed_kmh,
reported_speed_kmh=result.reported_speed_kmh,
seconds_since_last_accepted=result.seconds_since_last_accepted,
)
)

Expand Down Expand Up @@ -231,6 +246,10 @@ def reset_filter(self) -> None:
self.engine = GPSFilterEngine(
max_speed=get_config_value(self.entry, CONF_MAX_SPEED),
max_accuracy=get_config_value(self.entry, CONF_MAX_ACCURACY),
max_speed_difference_kmh=get_config_value(
self.entry,
CONF_MAX_SPEED_DIFFERENCE,
),
)
self.summary_stats = SummaryStats()
self.filter_timeline.clear()
Expand All @@ -239,6 +258,37 @@ def reset_filter(self) -> None:
def _update_summary_stats(self, point: GPSPoint, result: FilterResult) -> None:
"""Update post-drive summary statistics from a filter decision."""
self.summary_stats.total_received_count += 1

if not result.accepted:
self.summary_stats.max_rejected_accuracy_m = max(
self.summary_stats.max_rejected_accuracy_m,
point.accuracy,
)

if result.distance_m is not None:
self.summary_stats.max_rejected_distance_m = max(
self.summary_stats.max_rejected_distance_m,
result.distance_m,
)

if result.calculated_speed_kmh is not None:
self.summary_stats.max_rejected_calculated_speed_kmh = max(
self.summary_stats.max_rejected_calculated_speed_kmh,
result.calculated_speed_kmh,
)

reported_speed_kmh = result.reported_speed_kmh
if reported_speed_kmh is None and point.speed is not None:
reported_speed_kmh = point.speed * 3.6

if reported_speed_kmh is not None:
self.summary_stats.max_rejected_reported_speed_kmh = max(
self.summary_stats.max_rejected_reported_speed_kmh,
reported_speed_kmh,
)

return

self.summary_stats.max_accuracy_m = max(
self.summary_stats.max_accuracy_m,
point.accuracy,
Expand Down
11 changes: 11 additions & 0 deletions custom_components/gps_filter/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ def extra_state_attributes(self):
attributes["duplicate_count"] = stats.duplicate
attributes["accuracy_rejections"] = stats.accuracy_rejections
attributes["speed_rejections"] = stats.speed_rejections
attributes["speed_consistency_rejections"] = (
stats.speed_consistency_rejections
)

attributes["last_received_accuracy"] = None
attributes["last_received_timestamp"] = None
Expand All @@ -66,6 +69,7 @@ def extra_state_attributes(self):
attributes["last_distance_m"] = None
attributes["last_calculated_speed_kmh"] = None
attributes["last_reported_speed_kmh"] = None
attributes["last_seconds_since_accepted"] = None

if self.coordinator.last_received_point is not None:
attributes["last_received_accuracy"] = (
Expand Down Expand Up @@ -94,6 +98,13 @@ def extra_state_attributes(self):
attributes["last_reported_speed_kmh"] = (
self.coordinator.last_result.reported_speed_kmh
)
attributes["last_seconds_since_accepted"] = (
getattr(
self.coordinator.last_result,
"seconds_since_last_accepted",
None,
)
)

return attributes

Expand Down
34 changes: 34 additions & 0 deletions custom_components/gps_filter/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def _serialize_filter_result(result: FilterResult | None) -> dict[str, Any] | No
"distance_m": result.distance_m,
"calculated_speed_kmh": result.calculated_speed_kmh,
"reported_speed_kmh": result.reported_speed_kmh,
"seconds_since_last_accepted": result.seconds_since_last_accepted,
}


Expand All @@ -61,6 +62,7 @@ def _serialize_timeline_entry(entry: FilterTimelineEntry) -> dict[str, Any]:
"distance_m": entry.distance_m,
"calculated_speed_kmh": entry.calculated_speed_kmh,
"reported_speed_kmh": entry.reported_speed_kmh,
"seconds_since_last_accepted": entry.seconds_since_last_accepted,
}


Expand All @@ -69,10 +71,16 @@ def _serialize_summary(coordinator: Any, stats: Any) -> dict[str, Any]:
summary_stats = getattr(coordinator, "summary_stats", None)
return {
"total_received_count": getattr(summary_stats, "total_received_count", 0),
"total_rejected_count": getattr(coordinator, "total_rejected_count", 0),
"accepted_count": getattr(stats, "accepted", 0),
"duplicate_count": getattr(stats, "duplicate", 0),
"accuracy_rejections": getattr(stats, "accuracy_rejections", 0),
"speed_rejections": getattr(stats, "speed_rejections", 0),
"speed_consistency_rejections": getattr(
stats,
"speed_consistency_rejections",
0,
),
"acceptance_rate_percent": getattr(
coordinator,
"acceptance_rate_percent",
Expand All @@ -90,6 +98,26 @@ def _serialize_summary(coordinator: Any, stats: Any) -> dict[str, Any]:
0.0,
),
"max_accuracy_m": getattr(summary_stats, "max_accuracy_m", 0.0),
"max_rejected_distance_m": getattr(
summary_stats,
"max_rejected_distance_m",
0.0,
),
"max_rejected_calculated_speed_kmh": getattr(
summary_stats,
"max_rejected_calculated_speed_kmh",
0.0,
),
"max_rejected_reported_speed_kmh": getattr(
summary_stats,
"max_rejected_reported_speed_kmh",
0.0,
),
"max_rejected_accuracy_m": getattr(
summary_stats,
"max_rejected_accuracy_m",
0.0,
),
}


Expand Down Expand Up @@ -121,6 +149,7 @@ async def async_get_config_entry_diagnostics(
"duplicate_count": 0,
"accuracy_rejections": 0,
"speed_rejections": 0,
"speed_consistency_rejections": 0,
"summary": _serialize_summary(coordinator, None),
"last_received_point": None,
"last_accepted_point": None,
Expand All @@ -138,6 +167,11 @@ async def async_get_config_entry_diagnostics(
"duplicate_count": getattr(stats, "duplicate", 0),
"accuracy_rejections": getattr(stats, "accuracy_rejections", 0),
"speed_rejections": getattr(stats, "speed_rejections", 0),
"speed_consistency_rejections": getattr(
stats,
"speed_consistency_rejections",
0,
),
"summary": _serialize_summary(coordinator, stats),
"last_received_point": _serialize_gps_point(data.last_received_point),
"last_accepted_point": _serialize_gps_point(data.last_accepted_point),
Expand Down
Loading
Loading