Skip to content

Commit

Permalink
Add UUID filter to Beacon Monitor (home-assistant#3178)
Browse files Browse the repository at this point in the history
  • Loading branch information
chatziko authored Jan 13, 2023
1 parent a5cbe04 commit bca80f6
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
4 changes: 3 additions & 1 deletion common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit bca80f6

Please sign in to comment.