Skip to content

Commit

Permalink
Additional settings. Fix for HA 2023.5
Browse files Browse the repository at this point in the history
  • Loading branch information
formatBCE committed May 6, 2023
1 parent 4205682 commit f8a051f
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 50 deletions.
109 changes: 68 additions & 41 deletions custom_components/format_ble_tracker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import asyncio
import json
import logging

# import numpy as np
import math
import time
from typing import Any
#import numpy as np
import math

import voluptuous as vol

Expand Down Expand Up @@ -61,9 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Notifying alive to %s", alive_topic)
await mqtt.async_publish(hass, alive_topic, True, 1, retain=True)
hass.data[DOMAIN][entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
elif MERGE_IDS in entry.data:
hass.config_entries.async_setup_platforms(entry, [Platform.DEVICE_TRACKER])
await hass.config_entries.async_forward_entry_setups(
entry, [Platform.DEVICE_TRACKER]
)

return True

Expand Down Expand Up @@ -98,14 +101,17 @@ def __init__(self, hass: HomeAssistant, data) -> None:
"""Initialise coordinator."""
self.mac = data[MAC]
self.expiration_time: int
self.min_rssi: int
self.default_expiration_time: int = 2
self.default_min_rssi: int = -80
given_name = data[NAME] if data.__contains__(NAME) else self.mac
self.room_data = dict[str, int]()
self.filtered_room_data = dict[str, int]()
self.room_filters = dict[str, KalmanFilter]()
self.room_expiration_timers = dict[str, asyncio.TimerHandle]()
self.room: str | None = None
self.last_received_adv_time = None
self.time_from_previous = None

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

Expand Down Expand Up @@ -137,23 +143,29 @@ async def message_received(self, msg):
try:
data = MQTT_PAYLOAD(msg.payload)
except vol.MultipleInvalid as error:
_LOGGER.debug("Skipping update because of malformatted data: %s", error)
_LOGGER.debug("Skipping malformed message: %s", error)
return
msg_time = data.get(TIMESTAMP)
if msg_time is not None:
current_time = int(time.time())
if current_time - msg_time >= self.get_expiration_time():
_LOGGER.info("Received message with old timestamp, skipping")
_LOGGER.info("Skipping message with old timestamp")
return

self.time_from_previous = None if self.last_received_adv_time is None else (current_time - self.last_received_adv_time)
rssi = data.get(RSSI)
if rssi < self.get_min_rssi():
_LOGGER.info("Skipping message with low RSSI (%s)", rssi)
return
self.time_from_previous = (
None
if self.last_received_adv_time is None
else (current_time - self.last_received_adv_time)
)
self.last_received_adv_time = current_time

room_topic = msg.topic.split("/")[2]

await self.schedule_data_expiration(room_topic)

rssi = data.get(RSSI)
self.room_data[room_topic] = rssi
self.filtered_room_data[room_topic] = self.get_filtered_value(room_topic, rssi)

Expand All @@ -171,7 +183,7 @@ async def schedule_data_expiration(self, room):
self.room_expiration_timers[room] = timer

def get_filtered_value(self, room, value) -> int:
"""Apply Kalman filter"""
"""Apply Kalman filter."""
k_filter: KalmanFilter
if room in self.room_filters:
k_filter = self.room_filters[room]
Expand All @@ -184,6 +196,10 @@ def get_expiration_time(self):
"""Calculate current expiration delay."""
return getattr(self, "expiration_time", self.default_expiration_time) * 60

def get_min_rssi(self):
"""Calculate current minimum RSSI to take."""
return getattr(self, "min_rssi", self.default_min_rssi)

async def expire_data(self, room):
"""Set data for certain room expired."""
del self.room_data[room]
Expand All @@ -200,65 +216,76 @@ async def on_expiration_time_changed(self, new_time: int):
for room in self.room_expiration_timers.keys():
await self.schedule_data_expiration(room)

async def on_min_rssi_changed(self, new_min_rssi: int):
"""Respond to min RSSI changed by user."""
if new_min_rssi is None:
return
self.min_rssi = new_min_rssi


class KalmanFilter:
"""Filtering RSSI data."""

cov = float('nan')
x = float('nan')
cov = float("nan")
param_x = float("nan")

def __init__(self, param_r, param_q):
"""Initialize filter.
def __init__(self, R, Q):
"""
Constructor
:param R: Process Noise
:param Q: Measurement Noise
"""
self.A = 1
self.B = 0
self.C = 1
self.param_a = 1
self.param_b = 0
self.param_c = 1

self.R = R
self.Q = Q
self.param_r = param_r
self.param_q = param_q

def filter(self, measurement):
"""
Filters a measurement
"""Filter measurement.
:param measurement: The measurement value to be filtered
:return: The filtered value
"""
u = 0
if math.isnan(self.x):
self.x = (1 / self.C) * measurement
self.cov = (1 / self.C) * self.Q * (1 / self.C)
param_u = 0
if math.isnan(self.param_x):
self.param_x = (1 / self.param_c) * measurement
self.cov = (1 / self.param_c) * self.param_q * (1 / self.param_c)
else:
pred_x = (self.A * self.x) + (self.B * u)
pred_cov = ((self.A * self.cov) * self.A) + self.R
pred_x = (self.param_a * self.param_x) + (self.param_b * param_u)
pred_cov = ((self.param_a * self.cov) * self.param_a) + self.param_r

# Kalman Gain
K = pred_cov * self.C * (1 / ((self.C * pred_cov * self.C) + self.Q));
param_k = (
pred_cov
* self.param_c
* (1 / ((self.param_c * pred_cov * self.param_c) + self.param_q))
)

# Correction
self.x = pred_x + K * (measurement - (self.C * pred_x));
self.cov = pred_cov - (K * self.C * pred_cov);
self.param_x = pred_x + param_k * (measurement - (self.param_c * pred_x))
self.cov = pred_cov - (param_k * self.param_c * pred_cov)

return self.x
return self.param_x

def last_measurement(self):
"""
Returns the last measurement fed into the filter
"""Return the last measurement fed into the filter.
:return: The last measurement fed into the filter
"""
return self.x
return self.param_x

def set_measurement_noise(self, noise):
"""
Sets measurement noise
"""Set measurement noise.
:param noise: The new measurement noise
"""
self.Q = noise
self.param_q = noise

def set_process_noise(self, noise):
"""
Sets process noise
"""Set process noise.
:param noise: The new process noise
"""
self.R = noise
self.param_r = noise
6 changes: 3 additions & 3 deletions custom_components/format_ble_tracker/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from homeassistant.helpers import selector

from .const import (
DOMAIN,
AWAY_WHEN_OR,
AWAY_WHEN_AND,
AWAY_WHEN_OR,
DOMAIN,
MAC,
MAC_REGEX,
MERGE_IDS,
Expand Down Expand Up @@ -47,7 +47,7 @@

CONF_MERGE_LOGIC = {
AWAY_WHEN_OR: "Show as away, when ANY tracker is away",
AWAY_WHEN_AND: "Show as away, when ALL trackers are away"
AWAY_WHEN_AND: "Show as away, when ALL trackers are away",
}

MERGE_SCHEMA = vol.Schema(
Expand Down
4 changes: 2 additions & 2 deletions custom_components/format_ble_tracker/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from .__init__ import BeaconCoordinator
from .common import BeaconDeviceEntity
from .const import (
AWAY_WHEN_AND,
AWAY_WHEN_OR,
DOMAIN,
ENTITY_ID,
AWAY_WHEN_OR,
AWAY_WHEN_AND,
MERGE_IDS,
MERGE_LOGIC,
NAME,
Expand Down
2 changes: 1 addition & 1 deletion custom_components/format_ble_tracker/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "format_ble_tracker",
"name": "Format BLE Tracker",
"version": "0.0.7",
"version": "0.0.8",
"config_flow": true,
"documentation": "https://github.com/formatBCE/Format-BLE-Tracker/blob/main/README.md",
"issue_tracker": "https://github.com/formatBCE/Format-BLE-Tracker/issues",
Expand Down
51 changes: 48 additions & 3 deletions custom_components/format_ble_tracker/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ async def async_setup_entry(
"""Add sensor entities from a config_entry."""

coordinator: BeaconCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([BleDataExpirationNumber(coordinator)], True)
async_add_entities(
[BleDataExpirationNumber(coordinator), BleMinimumRssiNumber(coordinator)], True
)


class BleDataExpirationNumber(BeaconDeviceEntity, RestoreNumber, NumberEntity):
"""Define an room sensor entity."""
"""Define expiration time number entity."""

_attr_should_poll = False

Expand All @@ -39,7 +41,11 @@ def __init__(self, coordinator: BeaconCoordinator) -> None:
async def async_added_to_hass(self):
"""Entity has been added to hass, restoring state."""
restored = await self.async_get_last_number_data()
native_value = 2 if restored is None else restored.native_value
native_value = (
self.coordinator.default_expiration_time
if restored is None
else restored.native_value
)
await self.update_value(native_value)

async def async_set_native_value(self, value: float) -> None:
Expand All @@ -52,3 +58,42 @@ async def update_value(self, value: int):
self._attr_native_value = value
await self.coordinator.on_expiration_time_changed(value)
self.async_write_ha_state()


class BleMinimumRssiNumber(BeaconDeviceEntity, RestoreNumber, NumberEntity):
"""Define minimum RSSI number entity."""

_attr_should_poll = False

def __init__(self, coordinator: BeaconCoordinator) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_name = coordinator.name + " minimum RSSI"
self._attr_mode = NumberMode.SLIDER
self._attr_native_unit_of_measurement = "dBm"
self._attr_native_max_value = -20
self._attr_native_min_value = -100
self._attr_native_step = 1
self._attr_unique_id = self.formatted_mac_address + "_min_rssi"
self.entity_id = f"{input_number.DOMAIN}.{self._attr_unique_id}"

async def async_added_to_hass(self):
"""Entity has been added to hass, restoring state."""
restored = await self.async_get_last_number_data()
native_value = (
self.coordinator.default_min_rssi
if restored is None
else restored.native_value
)
await self.update_value(native_value)

async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
val = min(-20, max(-100, int(value)))
await self.update_value(val)

async def update_value(self, value: int):
"""Set value to HA and coordinator."""
self._attr_native_value = value
await self.coordinator.on_min_rssi_changed(value)
self.async_write_ha_state()
4 changes: 4 additions & 0 deletions custom_components/format_ble_tracker/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ def extra_state_attributes(self):
attr["current_rooms"] = {}
for key, value in self.coordinator.filtered_room_data.items():
attr["current_rooms"][key] = f"{value} dBm"
attr["current_rooms_raw"] = {}
for key, value in self.coordinator.room_data.items():
attr["current_rooms_raw"][key] = f"{value} dBm"
attr["last_adv"] = self.coordinator.time_from_previous
return attr

0 comments on commit f8a051f

Please sign in to comment.