Add UUID filter to Beacon Monitor (#3178)

This commit is contained in:
Kostas Chatzikokolakis 2023-01-13 17:06:29 +01:00 committed by GitHub
parent a5cbe04315
commit bca80f6391
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 67 additions and 15 deletions

View file

@ -16,6 +16,7 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.common.sensors.BluetoothSensorManager
import io.homeassistant.companion.android.common.sensors.NetworkSensorManager
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
@ -332,6 +333,13 @@ class SensorDetailViewModel @Inject constructor(
}
SensorSettingType.LIST_ZONES ->
entries ?: zones
SensorSettingType.LIST_BEACONS -> {
// show current beacons and also previously selected UUIDs
entries ?: (sensorManager as BluetoothSensorManager).getBeaconUUIDs()
.plus(setting.value.split(", ").filter { it.isNotEmpty() })
.sorted()
.distinct()
}
else ->
emptyList()
}

View file

@ -207,7 +207,7 @@ fun SensorDetailView(
}
)
}
SensorSettingType.LIST, SensorSettingType.LIST_APPS, SensorSettingType.LIST_BLUETOOTH, SensorSettingType.LIST_ZONES -> {
SensorSettingType.LIST, SensorSettingType.LIST_APPS, SensorSettingType.LIST_BLUETOOTH, SensorSettingType.LIST_ZONES, SensorSettingType.LIST_BEACONS -> {
val summaryValues = setting.value.split(", ").mapNotNull { it.ifBlank { null } }
SensorDetailRow(
title = viewModel.getSettingTranslatedTitle(setting.name),

View file

@ -22,17 +22,32 @@ data class IBeacon(
class IBeaconMonitor {
lateinit var sensorManager: BluetoothSensorManager
var beacons: List<IBeacon> = listOf()
var lastSeenBeacons: Collection<Beacon> = listOf()
private var uuidFilter = listOf<String>()
private var uuidFilterExclude = false
private fun sort(tmp: Collection<IBeacon>): Collection<IBeacon> {
return tmp.sortedBy { it.distance }
}
private fun ignoreBeacon(uuid: String): Boolean {
val inList = uuidFilter.contains(uuid)
return if (uuidFilterExclude)
inList // exclude filter, keep those not in list
else
!(inList || uuidFilter.isEmpty()) // include filter, keep those in list (or all if the list is empty)
}
fun setBeacons(context: Context, newBeacons: Collection<Beacon>) {
lastSeenBeacons = newBeacons // unfiltered list, for the settings UI
var requireUpdate = false
val tmp = mutableMapOf<String, IBeacon>()
for (existingBeacon in beacons) {
existingBeacon.skippedUpdated++
tmp += existingBeacon.name to existingBeacon
if (++existingBeacon.skippedUpdated > MAX_SKIPPED_UPDATED) { // an old beacon expired
requireUpdate = true
} else {
tmp += existingBeacon.name to existingBeacon
}
}
for (newBeacon in newBeacons) {
val uuid = newBeacon.id1.toString()
@ -42,8 +57,12 @@ class IBeaconMonitor {
val rssi = newBeacon.runningAverageRssi
val beacon = IBeacon(uuid, major, minor, distance, rssi, 0)
if (beacon.name !in tmp) { // we found a new beacon
val existing = tmp[beacon.name]
if (existing == null) { // we found a new beacon
if (ignoreBeacon(uuid)) continue // UUID filter (note: no need to check old beacons)
requireUpdate = true
} else {
existing.skippedUpdated = 0 // beacon seen, make sure skippedUpdated=0, even if requireUpdate stays false (and beacons list is not replaced)
}
tmp += beacon.name to beacon
}
@ -52,16 +71,6 @@ class IBeaconMonitor {
sendUpdate(context, sorted)
return
}
for (i in sorted.indices.reversed()) {
if (sorted[i].skippedUpdated > MAX_SKIPPED_UPDATED) { // a old beacon expired
sorted.removeAt(i)
requireUpdate = true
}
}
if (requireUpdate) {
sendUpdate(context, sorted)
return
}
for ((i, existingBeacon) in beacons.withIndex()) {
if (i < sorted.size) {
if (sorted[i].name != existingBeacon.name || // the distance order switched
@ -85,4 +94,14 @@ class IBeaconMonitor {
intent.action = SensorReceiverBase.ACTION_UPDATE_SENSORS
context.sendBroadcast(intent)
}
fun setUUIDFilter(uuidFilter: List<String>, uuidFilterExclude: Boolean) {
this.uuidFilter = uuidFilter
this.uuidFilterExclude = uuidFilterExclude
// existing beacons are only filtered when the filter changes
beacons
.filter { ignoreBeacon(it.uuid) }
.forEach { it.skippedUpdated = MAX_SKIPPED_UPDATED } // delete in the next update
}
}

View file

@ -16,6 +16,9 @@ import io.homeassistant.companion.android.common.bluetooth.ble.name
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.SensorSetting
import io.homeassistant.companion.android.database.sensor.SensorSettingType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.UUID
import io.homeassistant.companion.android.common.R as commonR
@ -41,6 +44,8 @@ class BluetoothSensorManager : SensorManager {
private const val SETTING_BEACON_MONITOR_SCAN_INTERVAL = "beacon_monitor_scan_interval"
private const val SETTING_BEACON_MONITOR_FILTER_ITERATIONS = "beacon_monitor_filter_iterations"
private const val SETTING_BEACON_MONITOR_FILTER_RSSI_MULTIPLIER = "beacon_monitor_filter_rssi_multiplier"
private const val SETTING_BEACON_MONITOR_UUID_FILTER = "beacon_monitor_uuid_filter"
private const val SETTING_BEACON_MONITOR_UUID_FILTER_EXCLUDE = "beacon_monitor_uuid_filter_exclude"
private const val DEFAULT_BLE_TRANSMIT_POWER = "ultraLow"
private const val DEFAULT_BLE_ADVERTISE_MODE = "lowPower"
@ -126,6 +131,8 @@ class BluetoothSensorManager : SensorManager {
}
}
private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
override fun docsLink(): String {
return "https://companion.home-assistant.io/docs/core/sensors#bluetooth-sensors"
}
@ -301,6 +308,15 @@ class BluetoothSensorManager : SensorManager {
KalmanFilter.maxIterations = getSetting(context, beaconMonitor, SETTING_BEACON_MONITOR_FILTER_ITERATIONS, SensorSettingType.NUMBER, DEFAULT_BEACON_MONITOR_FILTER_ITERATIONS).toIntOrNull() ?: DEFAULT_BEACON_MONITOR_FILTER_ITERATIONS.toInt()
KalmanFilter.rssiMultiplier = getSetting(context, beaconMonitor, SETTING_BEACON_MONITOR_FILTER_RSSI_MULTIPLIER, SensorSettingType.NUMBER, DEFAULT_BEACON_MONITOR_FILTER_RSSI_MULTIPLIER).toDoubleOrNull() ?: DEFAULT_BEACON_MONITOR_FILTER_RSSI_MULTIPLIER.toDouble()
val uuidFilter = getSetting(context, beaconMonitor, SETTING_BEACON_MONITOR_UUID_FILTER, SensorSettingType.LIST_BEACONS, "").split(", ").filter { it.isNotEmpty() }
beaconMonitoringDevice.setUUIDFilter(
uuidFilter,
getSetting(context, beaconMonitor, SETTING_BEACON_MONITOR_UUID_FILTER_EXCLUDE, SensorSettingType.TOGGLE, "false").toBoolean()
)
ioScope.launch {
enableDisableSetting(context, beaconMonitor, SETTING_BEACON_MONITOR_UUID_FILTER_EXCLUDE, uuidFilter.isNotEmpty())
}
val restart = monitoringManager.isMonitoring() &&
(monitoringManager.scanPeriod != scanPeriod || monitoringManager.scanInterval != scanInterval)
monitoringManager.scanPeriod = scanPeriod
@ -378,6 +394,12 @@ class BluetoothSensorManager : SensorManager {
)
}
fun getBeaconUUIDs(): List<String> {
return beaconMonitoringDevice.beacons
.map { it.uuid }
.plus(beaconMonitoringDevice.lastSeenBeacons.map { it.id1.toString() }) // include ignored
}
private fun checkNameAddress(bt: BluetoothDevice): String {
return if (bt.address != bt.name) "${bt.address} (${bt.name})" else bt.address
}

View file

@ -12,6 +12,7 @@ enum class SensorSettingType(val string: String, val listType: Boolean = false)
LIST_APPS("list-apps", listType = true),
LIST_BLUETOOTH("list-bluetooth", listType = true),
LIST_ZONES("list-zones", listType = true),
LIST_BEACONS("list-beacons", listType = true),
}
@Entity(tableName = "sensor_settings", primaryKeys = ["sensor_id", "name"])

View file

@ -486,7 +486,7 @@
<string name="sensor_description_battery_state">The current charging state of the battery</string>
<string name="sensor_description_battery_temperature">The current battery temperature</string>
<string name="sensor_description_bluetooth_ble_emitter">Send BLE iBeacon with configured interval, used to track presence around house, e.g. together with roomassistant, esp32-mqtt-room or espresence projects.\n\nWarning: this can affect battery life, particularly if the \"Transmitter power\" setting is set to High or \"Advertise Mode\" is set to Low latency.\n\nSettings allow for specifying:\n- \"UUID\" (standard UUID format), \"Major\" and \"Minor\" (should be 0 - 65535), to tailor identifiers and groups\n- \"Transmitter Power\" and \"Advertise Mode\" to help to preserve battery life (use lowest values if possible)\n - \"Measured Power\" to specify power measured at 1m (initial default -59)\n\nNote:\nAdditionally a separate setting exists (\"Enable Transmitter\") to stop or start transmitting.</string>
<string name="sensor_description_bluetooth_ble_beacon_monitor">Scans for iBeacons and shows the IDs of nearby beacons and their distance in meters.\n\nWarning: this can affect battery life, especially with a short \"Scan Interval\".\n\nSettings allow for specifying:\n- \"Filter Iterations\" (should be 1 - 100, default: 10), higher values will result in more stable measurements but also less responsiveness.\n- \"Filter RSSI Multiplier\" (should be 1.0 - 2.0, default: 1.05), can be used to archive more stable measurements when beacons are farther away. This will also affect responsiveness.\n - \"Scan Interval\" (default: 500) milliseconds between scans. Shorter intervals will drain the battery more quickly.\n - \"Scan Period\" (default: 1100) milliseconds to scan for beacons. Most beacons will send a signal every second so this value should be at least 1100ms.\n\nNote:\nAdditionally a separate setting exists (\"Enable Beacon Monitor\") to stop or start scanning.</string>
<string name="sensor_description_bluetooth_ble_beacon_monitor">Scans for iBeacons and shows the IDs of nearby beacons and their distance in meters.\n\nWarning: this can affect battery life, especially with a short \"Scan Interval\".\n\nSettings allow for specifying:\n- \"Filter Iterations\" (should be 1 - 100, default: 10), higher values will result in more stable measurements but also less responsiveness.\n- \"Filter RSSI Multiplier\" (should be 1.0 - 2.0, default: 1.05), can be used to archive more stable measurements when beacons are farther away. This will also affect responsiveness.\n- \"Scan Interval\" (default: 500) milliseconds between scans. Shorter intervals will drain the battery more quickly.\n- \"Scan Period\" (default: 1100) milliseconds to scan for beacons. Most beacons will send a signal every second so this value should be at least 1100ms.\n- \"UUID Filter\" allows to restrict the reported beacons by including (or excluding) those with the selected UUIDs.\n- \"Exclude selected UUIDs\", if false (default) only the beacons with the selected UUIDs are reported. If true all beacons except the selected ones are reported. Not available when \"UUID Filter\" is empty.\n\nNote:\nAdditionally a separate setting exists (\"Enable Beacon Monitor\") to stop or start scanning.</string>
<string name="sensor_description_bluetooth_connection">Information about currently connected Bluetooth devices</string>
<string name="sensor_description_bluetooth_state">Whether Bluetooth is enabled on the device</string>
<string name="sensor_description_charger_type">The type of charger plugged into the device currently</string>
@ -608,6 +608,8 @@
<string name="sensor_setting_beacon_monitor_scan_interval_title">Scan Interval</string>
<string name="sensor_setting_beacon_monitor_filter_iterations_title">Filter Iterations</string>
<string name="sensor_setting_beacon_monitor_filter_rssi_multiplier_title">Filter RSSI Multiplier</string>
<string name="sensor_setting_beacon_monitor_uuid_filter_title">UUID Filter</string>
<string name="sensor_setting_beacon_monitor_uuid_filter_exclude_title">Exclude selected UUIDs</string>
<string name="sensor_setting_geocode_minimum_accuracy_title">Minimum Accuracy</string>
<string name="sensor_setting_geocode_include_location_updates_title">Update sensor with location sensors</string>
<string name="sensor_setting_lastreboot_deadband_title">Deadband</string>