diff --git a/custom_components/format_ble_tracker/__init__.py b/custom_components/format_ble_tracker/__init__.py index eb12238..cb05049 100644 --- a/custom_components/format_ble_tracker/__init__.py +++ b/custom_components/format_ble_tracker/__init__.py @@ -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 @@ -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 @@ -98,7 +101,9 @@ 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]() @@ -106,6 +111,7 @@ def __init__(self, hass: HomeAssistant, data) -> None: 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) @@ -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) @@ -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] @@ -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] @@ -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 \ No newline at end of file + self.param_r = noise diff --git a/custom_components/format_ble_tracker/config_flow.py b/custom_components/format_ble_tracker/config_flow.py index c70c4c2..a7f1be1 100644 --- a/custom_components/format_ble_tracker/config_flow.py +++ b/custom_components/format_ble_tracker/config_flow.py @@ -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, @@ -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( diff --git a/custom_components/format_ble_tracker/device_tracker.py b/custom_components/format_ble_tracker/device_tracker.py index 7e1c7eb..d7e771a 100644 --- a/custom_components/format_ble_tracker/device_tracker.py +++ b/custom_components/format_ble_tracker/device_tracker.py @@ -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, diff --git a/custom_components/format_ble_tracker/manifest.json b/custom_components/format_ble_tracker/manifest.json index 5513e8c..9122b60 100644 --- a/custom_components/format_ble_tracker/manifest.json +++ b/custom_components/format_ble_tracker/manifest.json @@ -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", diff --git a/custom_components/format_ble_tracker/number.py b/custom_components/format_ble_tracker/number.py index 5204130..e9c8d23 100644 --- a/custom_components/format_ble_tracker/number.py +++ b/custom_components/format_ble_tracker/number.py @@ -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 @@ -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: @@ -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() diff --git a/custom_components/format_ble_tracker/sensor.py b/custom_components/format_ble_tracker/sensor.py index 9302bef..227edd8 100644 --- a/custom_components/format_ble_tracker/sensor.py +++ b/custom_components/format_ble_tracker/sensor.py @@ -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