Stop flooding the recorder with timestamp and false-precision noise

Measured on a real installation with two bikes: the two "Last Seen"
sensors were the number one and number two writers in the entire Home
Assistant database, ahead of a three-phase energy meter. The integration
accounted for 26.5% of all recorder writes on that system.

Last Seen moved on every BLE notification, about once a second, or 2834
rows an hour per bike. The entity exists to say how fresh the values are,
which needs nothing near that resolution, so it now advances at
LAST_SEEN_RESOLUTION granularity. `self._last_seen` stays exact - that is
what gets persisted and restored - only the entity is throttled. Measured
after: 120 rows an hour, the theoretical maximum for a 30s window.

The distance sensors carried four decimals of false precision, because
the ebikemotion frames divide by 10000: a range that is really "14.6" was
reported as 14.6065, then 14.6529, then 14.6297, each one another state
write and another database row. They are rounded to 0.1 km now.

Home Assistant offers integrations no way to exclude an entity from the
recorder - `entity_filter` comes from user configuration only, and
`_unrecorded_attributes` covers attributes rather than states - so
reducing how often the state changes is the only fix that works without
asking every user to edit configuration.yaml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyabWZzd7HoLpyBwEa5Zzh
This commit is contained in:
Rene Nulsch
2026-08-28 08:36:09 +02:00
co-authored by Claude Opus 5
parent a38b2efef5
commit 62bc1c150a
4 changed files with 107 additions and 6 deletions
@@ -537,3 +537,68 @@ async def test_no_fast_reconnect_when_link_died_immediately(
coordinator._async_client_disconnected(coordinator._client)
await hass.async_block_till_done()
mock_connect.assert_not_called()
async def test_last_seen_is_throttled(
hass: HomeAssistant,
restore_config_entry: MockConfigEntry,
mock_bleak_client,
mock_device_in_range,
freezer,
) -> None:
"""A timestamp that moves on every packet floods the recorder.
Notifications arrive about once a second; the entity only needs to say how
fresh the values are, so it moves at LAST_SEEN_RESOLUTION granularity while
the exact time is still what gets persisted.
"""
from custom_components.mysmartbike_ble.const import LAST_SEEN_RESOLUTION
await setup_offline(hass, restore_config_entry)
coordinator = restore_config_entry.runtime_data
frame = bytearray.fromhex("246a245a2300008402e1000001f50148494a2340")
coordinator._notification_handler(0, frame)
first = coordinator.data["last_seen"]
assert first is not None
# A burst of notifications inside the window must not move the entity
for _ in range(10):
freezer.tick(timedelta(seconds=1))
coordinator._notification_handler(0, frame)
assert coordinator.data["last_seen"] == first
# ...while the persisted timestamp keeps tracking reality
assert coordinator.last_seen > first
# Past the window it moves again
freezer.tick(timedelta(seconds=LAST_SEEN_RESOLUTION))
coordinator._notification_handler(0, frame)
assert coordinator.data["last_seen"] > first
async def test_distances_are_rounded_to_100m(
hass: HomeAssistant,
hass_storage,
restore_config_entry: MockConfigEntry,
mock_bleak_client,
mock_device_out_of_range,
) -> None:
"""Four decimals of false precision would be a database row each."""
seed_storage(
hass_storage,
{
**STORED_STATE,
"ebm": {
**STORED_STATE["ebm"],
"odometry": 806.1488,
"autonomy": 14.6065,
"trip_odometry": 12.3456,
"trip_autonomy": 58.9876,
},
},
)
await setup_offline(hass, restore_config_entry)
assert hass.states.get(entity_id_for(hass, "_odometer")).state == "806.1"
assert hass.states.get(entity_id_for(hass, "_range")).state == "14.6"
assert hass.states.get(entity_id_for(hass, "_trip_distance")).state == "12.3"