2
2
from __future__ import annotations
3
3
4
4
import asyncio
5
- from asyncio import events
6
5
import json
7
- import time
8
6
import logging
7
+ import time
9
8
from typing import Any
9
+ #import numpy as np
10
+ import math
10
11
11
12
import voluptuous as vol
12
13
20
21
ALIVE_NODES_TOPIC ,
21
22
DOMAIN ,
22
23
MAC ,
24
+ MERGE_IDS ,
23
25
NAME ,
24
26
ROOM ,
25
27
ROOT_TOPIC ,
26
28
RSSI ,
27
29
TIMESTAMP ,
28
- MERGE_IDS ,
29
30
)
30
31
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 ]
36
33
_LOGGER = logging .getLogger (__name__ )
37
34
38
35
MQTT_PAYLOAD = vol .Schema (
41
38
vol .Schema (
42
39
{
43
40
vol .Required (RSSI ): vol .Coerce (int ),
44
- vol .Optional (TIMESTAMP ): vol .Coerce (int )
41
+ vol .Optional (TIMESTAMP ): vol .Coerce (int ),
45
42
},
46
43
extra = vol .ALLOW_EXTRA ,
47
44
),
@@ -68,7 +65,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
68
65
elif MERGE_IDS in entry .data :
69
66
hass .config_entries .async_setup_platforms (entry , [Platform .DEVICE_TRACKER ])
70
67
71
-
72
68
return True
73
69
74
70
@@ -80,7 +76,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
80
76
else :
81
77
platforms = [Platform .DEVICE_TRACKER ]
82
78
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
+ ):
84
83
hass .data [DOMAIN ].pop (entry .entry_id )
85
84
86
85
if MAC in entry .data :
@@ -93,29 +92,34 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
93
92
94
93
95
94
class BeaconCoordinator (DataUpdateCoordinator [dict [str , Any ]]):
96
- """Class to arrange interaction with MQTT"""
95
+ """Class to arrange interaction with MQTT. """
97
96
98
97
def __init__ (self , hass : HomeAssistant , data ) -> None :
98
+ """Initialise coordinator."""
99
99
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
102
102
given_name = data [NAME ] if data .__contains__ (NAME ) else self .mac
103
103
self .room_data = dict [str , int ]()
104
+ self .filtered_room_data = dict [str , int ]()
105
+ self .room_filters = dict [str , KalmanFilter ]()
104
106
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
106
109
107
110
super ().__init__ (hass , _LOGGER , name = given_name )
108
111
109
112
async def _async_update_data (self ) -> dict [str , Any ]:
110
113
"""Update data via library."""
111
- if len (self .room_data ) == 0 :
114
+ if len (self .filtered_room_data ) == 0 :
112
115
self .room = None
116
+ self .last_received_adv_time = None
113
117
else :
114
118
self .room = next (
115
119
iter (
116
120
dict (
117
121
sorted (
118
- self .room_data .items (),
122
+ self .filtered_room_data .items (),
119
123
key = lambda item : item [1 ],
120
124
reverse = True ,
121
125
)
@@ -125,7 +129,7 @@ async def _async_update_data(self) -> dict[str, Any]:
125
129
return {** {ROOM : self .room }}
126
130
127
131
async def subscribe_to_mqtt (self ) -> None :
128
- """Subscribe coordinator to MQTT messages"""
132
+ """Subscribe coordinator to MQTT messages. """
129
133
130
134
@callback
131
135
async def message_received (self , msg ):
@@ -136,20 +140,27 @@ async def message_received(self, msg):
136
140
_LOGGER .debug ("Skipping update because of malformatted data: %s" , error )
137
141
return
138
142
msg_time = data .get (TIMESTAMP )
139
- if ( msg_time is not None ) :
143
+ if msg_time is not None :
140
144
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 ():
142
146
_LOGGER .info ("Received message with old timestamp, skipping" )
143
147
return
144
148
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
+
145
152
room_topic = msg .topic .split ("/" )[2 ]
146
153
147
154
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
+
149
160
await self .async_refresh ()
150
161
151
162
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. """
153
164
if room in self .room_expiration_timers :
154
165
self .room_expiration_timers [room ].cancel ()
155
166
loop = asyncio .get_event_loop ()
@@ -159,20 +170,95 @@ async def schedule_data_expiration(self, room):
159
170
)
160
171
self .room_expiration_timers [room ] = timer
161
172
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
+
162
183
def get_expiration_time (self ):
163
- """Calculate current expiration delay"""
184
+ """Calculate current expiration delay. """
164
185
return getattr (self , "expiration_time" , self .default_expiration_time ) * 60
165
186
166
187
async def expire_data (self , room ):
167
- """Set data for certain room expired"""
188
+ """Set data for certain room expired. """
168
189
del self .room_data [room ]
190
+ del self .filtered_room_data [room ]
191
+ del self .room_filters [room ]
169
192
del self .room_expiration_timers [room ]
170
193
await self .async_refresh ()
171
194
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. """
174
197
if new_time is None :
175
198
return
176
199
self .expiration_time = new_time
177
200
for room in self .room_expiration_timers .keys ():
178
201
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
0 commit comments