Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 61ace19

Browse files
committed
Kalman filtering. Code cleaning.
1 parent 6523fe9 commit 61ace19

File tree

9 files changed

+245
-119
lines changed

9 files changed

+245
-119
lines changed

custom_components/format_ble_tracker/__init__.py

Lines changed: 112 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from __future__ import annotations
33

44
import asyncio
5-
from asyncio import events
65
import json
7-
import time
86
import logging
7+
import time
98
from typing import Any
9+
#import numpy as np
10+
import math
1011

1112
import voluptuous as vol
1213

@@ -20,19 +21,15 @@
2021
ALIVE_NODES_TOPIC,
2122
DOMAIN,
2223
MAC,
24+
MERGE_IDS,
2325
NAME,
2426
ROOM,
2527
ROOT_TOPIC,
2628
RSSI,
2729
TIMESTAMP,
28-
MERGE_IDS,
2930
)
3031

31-
PLATFORMS: list[Platform] = [
32-
Platform.DEVICE_TRACKER,
33-
Platform.SENSOR,
34-
Platform.NUMBER
35-
]
32+
PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.NUMBER]
3633
_LOGGER = logging.getLogger(__name__)
3734

3835
MQTT_PAYLOAD = vol.Schema(
@@ -41,7 +38,7 @@
4138
vol.Schema(
4239
{
4340
vol.Required(RSSI): vol.Coerce(int),
44-
vol.Optional(TIMESTAMP): vol.Coerce(int)
41+
vol.Optional(TIMESTAMP): vol.Coerce(int),
4542
},
4643
extra=vol.ALLOW_EXTRA,
4744
),
@@ -68,7 +65,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
6865
elif MERGE_IDS in entry.data:
6966
hass.config_entries.async_setup_platforms(entry, [Platform.DEVICE_TRACKER])
7067

71-
7268
return True
7369

7470

@@ -80,7 +76,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
8076
else:
8177
platforms = [Platform.DEVICE_TRACKER]
8278

83-
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms) and entry.entry_id in hass.data[DOMAIN]:
79+
if (
80+
unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms)
81+
and entry.entry_id in hass.data[DOMAIN]
82+
):
8483
hass.data[DOMAIN].pop(entry.entry_id)
8584

8685
if MAC in entry.data:
@@ -93,29 +92,34 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
9392

9493

9594
class BeaconCoordinator(DataUpdateCoordinator[dict[str, Any]]):
96-
"""Class to arrange interaction with MQTT"""
95+
"""Class to arrange interaction with MQTT."""
9796

9897
def __init__(self, hass: HomeAssistant, data) -> None:
98+
"""Initialise coordinator."""
9999
self.mac = data[MAC]
100-
self.expiration_time : int
101-
self.default_expiration_time : int = 2
100+
self.expiration_time: int
101+
self.default_expiration_time: int = 2
102102
given_name = data[NAME] if data.__contains__(NAME) else self.mac
103103
self.room_data = dict[str, int]()
104+
self.filtered_room_data = dict[str, int]()
105+
self.room_filters = dict[str, KalmanFilter]()
104106
self.room_expiration_timers = dict[str, asyncio.TimerHandle]()
105-
self.room = None
107+
self.room: str | None = None
108+
self.last_received_adv_time = None
106109

107110
super().__init__(hass, _LOGGER, name=given_name)
108111

109112
async def _async_update_data(self) -> dict[str, Any]:
110113
"""Update data via library."""
111-
if len(self.room_data) == 0:
114+
if len(self.filtered_room_data) == 0:
112115
self.room = None
116+
self.last_received_adv_time = None
113117
else:
114118
self.room = next(
115119
iter(
116120
dict(
117121
sorted(
118-
self.room_data.items(),
122+
self.filtered_room_data.items(),
119123
key=lambda item: item[1],
120124
reverse=True,
121125
)
@@ -125,7 +129,7 @@ async def _async_update_data(self) -> dict[str, Any]:
125129
return {**{ROOM: self.room}}
126130

127131
async def subscribe_to_mqtt(self) -> None:
128-
"""Subscribe coordinator to MQTT messages"""
132+
"""Subscribe coordinator to MQTT messages."""
129133

130134
@callback
131135
async def message_received(self, msg):
@@ -136,20 +140,27 @@ async def message_received(self, msg):
136140
_LOGGER.debug("Skipping update because of malformatted data: %s", error)
137141
return
138142
msg_time = data.get(TIMESTAMP)
139-
if (msg_time is not None):
143+
if msg_time is not None:
140144
current_time = int(time.time())
141-
if (current_time - msg_time >= self.get_expiration_time()):
145+
if current_time - msg_time >= self.get_expiration_time():
142146
_LOGGER.info("Received message with old timestamp, skipping")
143147
return
144148

149+
self.time_from_previous = None if self.last_received_adv_time is None else (current_time - self.last_received_adv_time)
150+
self.last_received_adv_time = current_time
151+
145152
room_topic = msg.topic.split("/")[2]
146153

147154
await self.schedule_data_expiration(room_topic)
148-
self.room_data[room_topic] = data.get(RSSI)
155+
156+
rssi = data.get(RSSI)
157+
self.room_data[room_topic] = rssi
158+
self.filtered_room_data[room_topic] = self.get_filtered_value(room_topic, rssi)
159+
149160
await self.async_refresh()
150161

151162
async def schedule_data_expiration(self, room):
152-
"""Start timer for data expiration for certain room"""
163+
"""Start timer for data expiration for certain room."""
153164
if room in self.room_expiration_timers:
154165
self.room_expiration_timers[room].cancel()
155166
loop = asyncio.get_event_loop()
@@ -159,20 +170,95 @@ async def schedule_data_expiration(self, room):
159170
)
160171
self.room_expiration_timers[room] = timer
161172

173+
def get_filtered_value(self, room, value) -> int:
174+
"""Apply Kalman filter"""
175+
k_filter: KalmanFilter
176+
if room in self.room_filters:
177+
k_filter = self.room_filters[room]
178+
else:
179+
k_filter = KalmanFilter(0.01, 5)
180+
self.room_filters[room] = k_filter
181+
return int(k_filter.filter(value))
182+
162183
def get_expiration_time(self):
163-
"""Calculate current expiration delay"""
184+
"""Calculate current expiration delay."""
164185
return getattr(self, "expiration_time", self.default_expiration_time) * 60
165186

166187
async def expire_data(self, room):
167-
"""Set data for certain room expired"""
188+
"""Set data for certain room expired."""
168189
del self.room_data[room]
190+
del self.filtered_room_data[room]
191+
del self.room_filters[room]
169192
del self.room_expiration_timers[room]
170193
await self.async_refresh()
171194

172-
async def on_expiration_time_changed(self, new_time : int):
173-
"""Respond to expiration time changed by user"""
195+
async def on_expiration_time_changed(self, new_time: int):
196+
"""Respond to expiration time changed by user."""
174197
if new_time is None:
175198
return
176199
self.expiration_time = new_time
177200
for room in self.room_expiration_timers.keys():
178201
await self.schedule_data_expiration(room)
202+
203+
class KalmanFilter:
204+
"""Filtering RSSI data."""
205+
206+
cov = float('nan')
207+
x = float('nan')
208+
209+
def __init__(self, R, Q):
210+
"""
211+
Constructor
212+
:param R: Process Noise
213+
:param Q: Measurement Noise
214+
"""
215+
self.A = 1
216+
self.B = 0
217+
self.C = 1
218+
219+
self.R = R
220+
self.Q = Q
221+
222+
def filter(self, measurement):
223+
"""
224+
Filters a measurement
225+
:param measurement: The measurement value to be filtered
226+
:return: The filtered value
227+
"""
228+
u = 0
229+
if math.isnan(self.x):
230+
self.x = (1 / self.C) * measurement
231+
self.cov = (1 / self.C) * self.Q * (1 / self.C)
232+
else:
233+
pred_x = (self.A * self.x) + (self.B * u)
234+
pred_cov = ((self.A * self.cov) * self.A) + self.R
235+
236+
# Kalman Gain
237+
K = pred_cov * self.C * (1 / ((self.C * pred_cov * self.C) + self.Q));
238+
239+
# Correction
240+
self.x = pred_x + K * (measurement - (self.C * pred_x));
241+
self.cov = pred_cov - (K * self.C * pred_cov);
242+
243+
return self.x
244+
245+
def last_measurement(self):
246+
"""
247+
Returns the last measurement fed into the filter
248+
:return: The last measurement fed into the filter
249+
"""
250+
return self.x
251+
252+
def set_measurement_noise(self, noise):
253+
"""
254+
Sets measurement noise
255+
:param noise: The new measurement noise
256+
"""
257+
self.Q = noise
258+
259+
def set_process_noise(self, noise):
260+
"""
261+
Sets process noise
262+
:param noise: The new process noise
263+
"""
264+
self.R = noise
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
"""Common values"""
1+
"""Common values."""
22
from homeassistant.helpers.device_registry import format_mac
3-
from .const import DOMAIN
4-
from .__init__ import BeaconCoordinator
53
from homeassistant.helpers.update_coordinator import CoordinatorEntity
64

5+
from .__init__ import BeaconCoordinator
6+
from .const import DOMAIN
7+
8+
79
class BeaconDeviceEntity(CoordinatorEntity[BeaconCoordinator]):
8-
"""Base device class"""
10+
"""Base device class."""
911

1012
def __init__(self, coordinator: BeaconCoordinator) -> None:
1113
"""Initialize."""
@@ -14,10 +16,11 @@ def __init__(self, coordinator: BeaconCoordinator) -> None:
1416

1517
@property
1618
def device_info(self):
19+
"""Device info creation."""
1720
return {
1821
"identifiers": {
1922
# MAC addresses are unique identifiers within a specific domain
2023
(DOMAIN, self.formatted_mac_address)
2124
},
2225
"name": self.coordinator.name,
23-
}
26+
}

0 commit comments

Comments
 (0)