Support zone name only location tracking (#2969)

* Support zone name only location tracking

* Update strings to match iOS

* Only add Sent location setting on supported versions

* String updates

* Simplify code as the setting is always on
This commit is contained in:
Joris Pelgröm 2022-10-21 19:59:02 +02:00 committed by GitHub
parent bd26435eb8
commit e637b423f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 30 deletions

View file

@ -27,6 +27,7 @@ import io.homeassistant.companion.android.common.bluetooth.BluetoothUtils
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.UpdateLocation
import io.homeassistant.companion.android.common.data.integration.ZoneAttributes
import io.homeassistant.companion.android.common.data.integration.containsWithAccuracy
import io.homeassistant.companion.android.common.sensors.LocationSensorManagerBase
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.common.sensors.SensorReceiverBase
@ -41,12 +42,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeUnit
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
class LocationSensorManager : LocationSensorManagerBase() {
companion object {
private const val SETTING_SEND_LOCATION_AS = "location_send_as"
private const val SETTING_ACCURACY = "location_minimum_accuracy"
private const val SETTING_ACCURATE_UPDATE_TIME = "location_minimum_time_updates"
private const val SETTING_INCLUDE_SENSOR_UPDATE = "location_include_sensor_update"
@ -57,6 +60,8 @@ class LocationSensorManager : LocationSensorManagerBase() {
private const val SETTING_HIGH_ACCURACY_BT_ZONE_COMBINED = "location_ham_zone_bt_combined"
private const val SETTING_HIGH_ACCURACY_MODE_TRIGGER_RANGE_ZONE = "location_ham_trigger_range"
private const val SEND_LOCATION_AS_EXACT = "exact"
private const val SEND_LOCATION_AS_ZONE_ONLY = "zone_only"
private const val DEFAULT_MINIMUM_ACCURACY = 200
private const val DEFAULT_UPDATE_INTERVAL_HA_SECONDS = 5
private const val DEFAULT_TRIGGER_RANGE_METERS = 300
@ -65,6 +70,8 @@ class LocationSensorManager : LocationSensorManagerBase() {
private const val DEFAULT_LOCATION_FAST_INTERVAL: Long = 30000
private const val DEFAULT_LOCATION_MAX_WAIT_TIME: Long = 200000
private const val ZONE_NAME_NOT_HOME = "not_home"
const val ACTION_REQUEST_LOCATION_UPDATES =
"io.homeassistant.companion.android.background.REQUEST_UPDATES"
const val ACTION_REQUEST_ACCURATE_LOCATION_UPDATE =
@ -135,6 +142,9 @@ class LocationSensorManager : LocationSensorManagerBase() {
private var lastLocationReceived = 0L
private var lastUpdateLocation = ""
private var zones: Array<Entity<ZoneAttributes>> = emptyArray()
private var zonesLastReceived = 0L
private var geofenceRegistered = false
private var lastHighAccuracyMode = false
@ -327,6 +337,8 @@ class LocationSensorManager : LocationSensorManagerBase() {
lastHighAccuracyTriggerRange = highAccuracyTriggerRange
lastHighAccuracyMode = highAccuracyModeEnabled
lastHighAccuracyUpdateInterval = updateIntervalHighAccuracySeconds
getSendLocationAsSetting() // Sets up the setting, value isn't used right now
}
}
@ -540,6 +552,21 @@ class LocationSensorManager : LocationSensorManagerBase() {
).toBoolean()
}
private suspend fun getSendLocationAsSetting(): String {
return if (integrationUseCase.isHomeAssistantVersionAtLeast(2022, 2, 0)) {
getSetting(
context = latestContext,
sensor = backgroundLocation,
settingName = SETTING_SEND_LOCATION_AS,
settingType = SensorSettingType.LIST,
entries = listOf(
SEND_LOCATION_AS_EXACT, SEND_LOCATION_AS_ZONE_ONLY
),
default = SEND_LOCATION_AS_EXACT
)
} else SEND_LOCATION_AS_EXACT
}
private fun removeAllLocationUpdateRequests() {
Log.d(TAG, "Removing all location requests.")
removeBackgroundUpdateRequests()
@ -735,14 +762,39 @@ class LocationSensorManager : LocationSensorManagerBase() {
if (location.accuracy.toInt() >= 0) {
accuracy = location.accuracy.toInt()
}
val updateLocation = UpdateLocation(
arrayOf(location.latitude, location.longitude),
accuracy,
location.speed.toInt(),
location.altitude.toInt(),
location.bearing.toInt(),
if (Build.VERSION.SDK_INT >= 26) location.verticalAccuracyMeters.toInt() else 0
)
val updateLocation: UpdateLocation
val updateLocationAs: String
val updateLocationString: String
runBlocking {
updateLocationAs = getSendLocationAsSetting()
if (updateLocationAs == SEND_LOCATION_AS_ZONE_ONLY) {
val zones = getZones()
val locationZone = zones
.filter { !it.attributes.passive && it.containsWithAccuracy(location) }
.minByOrNull { it.attributes.radius }
updateLocation = UpdateLocation(
gps = null,
gpsAccuracy = null,
locationName = locationZone?.entityId?.split(".")?.get(1) ?: ZONE_NAME_NOT_HOME,
speed = null,
altitude = null,
course = null,
verticalAccuracy = null
)
updateLocationString = updateLocation.locationName!!
} else {
updateLocation = UpdateLocation(
gps = arrayOf(location.latitude, location.longitude),
gpsAccuracy = accuracy,
locationName = null,
speed = location.speed.toInt(),
altitude = location.altitude.toInt(),
course = location.bearing.toInt(),
verticalAccuracy = if (Build.VERSION.SDK_INT >= 26) location.verticalAccuracyMeters.toInt() else 0
)
updateLocationString = updateLocation.gps.contentToString()
}
}
val now = System.currentTimeMillis()
@ -765,7 +817,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
TAG,
"Received location that is ${now - location.time} milliseconds old, ${location.time} compared to $now with source ${location.provider}"
)
if (lastUpdateLocation == updateLocation.gps.contentToString()) {
if (lastUpdateLocation == updateLocationString) {
if (now < lastLocationSend + 900000) {
Log.d(TAG, "Duplicate location received, not sending to HA")
return
@ -784,7 +836,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
return
}
lastLocationSend = now
lastUpdateLocation = updateLocation.gps.contentToString()
lastUpdateLocation = updateLocationString
val geocodeIncludeLocation = getSetting(
latestContext,
@ -797,7 +849,7 @@ class LocationSensorManager : LocationSensorManagerBase() {
ioScope.launch {
try {
integrationUseCase.updateLocation(updateLocation)
Log.d(TAG, "Location update sent successfully")
Log.d(TAG, "Location update sent successfully as $updateLocationAs")
// Update Geocoded Location Sensor
if (geocodeIncludeLocation) {
@ -833,17 +885,28 @@ class LocationSensorManager : LocationSensorManagerBase() {
return locationRequest
}
private suspend fun getZones(forceRefresh: Boolean = false): Array<Entity<ZoneAttributes>> {
if (
forceRefresh || zones.isEmpty() ||
zonesLastReceived < (System.currentTimeMillis() - TimeUnit.HOURS.toMillis(4))
) {
try {
zones = integrationUseCase.getZones()
zonesLastReceived = System.currentTimeMillis()
} catch (e: Exception) {
Log.e(TAG, "Error receiving zones from Home Assistant", e)
if (forceRefresh) zones = emptyArray()
}
}
return zones
}
private suspend fun createGeofencingRequest(): GeofencingRequest {
val geofencingRequestBuilder = GeofencingRequest.Builder()
.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER)
// TODO cache the zones on device so we don't need to reach out each time
var configuredZones: Array<Entity<ZoneAttributes>> = emptyArray()
try {
configuredZones = integrationUseCase.getZones()
} catch (e: Exception) {
Log.e(TAG, "Error receiving zones from Home Assistant", e)
}
val configuredZones = getZones(forceRefresh = true)
val highAccuracyTriggerRange = getHighAccuracyModeTriggerRange()
val highAccuracyZones = getHighAccuracyModeZones(false)

View file

@ -1,10 +1,11 @@
package io.homeassistant.companion.android.common.data.integration
data class UpdateLocation(
val gps: Array<Double>,
val gpsAccuracy: Int,
val speed: Int,
val altitude: Int,
val course: Int,
val verticalAccuracy: Int
val gps: Array<Double>?,
val gpsAccuracy: Int?,
val locationName: String?,
val speed: Int?,
val altitude: Int?,
val course: Int?,
val verticalAccuracy: Int?
)

View file

@ -1,10 +1,26 @@
package io.homeassistant.companion.android.common.data.integration
import android.location.Location
data class ZoneAttributes(
val hidden: Boolean,
val passive: Boolean,
val latitude: Double,
val longitude: Double,
val radius: Float,
val friendlyName: String,
val icon: String?
)
/**
* Returns if the provided location is estimated to be in the zone.
* This function will also consider accuracy, so if the GPS location is outside the zone but the
* accuracy suggests that it could be in the zone, this function will still return `true`.
*/
fun Entity<ZoneAttributes>.containsWithAccuracy(location: Location): Boolean {
val zoneCenter = Location("").apply {
latitude = attributes.latitude
longitude = attributes.longitude
}
return (location.distanceTo(zoneCenter) - attributes.radius - location.accuracy.coerceAtLeast(0f)) <= 0
}

View file

@ -766,6 +766,7 @@ class IntegrationRepositoryImpl @Inject constructor(
UpdateLocationRequest(
updateLocation.gps,
updateLocation.gpsAccuracy,
updateLocation.locationName,
updateLocation.speed,
updateLocation.altitude,
updateLocation.course,

View file

@ -1,10 +1,11 @@
package io.homeassistant.companion.android.common.data.integration.impl.entities
data class UpdateLocationRequest(
val gps: Array<Double>,
val gpsAccuracy: Int,
val speed: Int,
val altitude: Int,
val course: Int,
val verticalAccuracy: Int
val gps: Array<Double>?,
val gpsAccuracy: Int?,
val locationName: String?,
val speed: Int?,
val altitude: Int?,
val course: Int?,
val verticalAccuracy: Int?
)

View file

@ -499,7 +499,7 @@
<string name="sensor_description_last_update">The intent action for the last update that was sent, periodic updates will show as \"SensorWorker\".\n\nEnabling the \"Add New Intent\" toggle will create 1 setting to allow you to register for a intent action. The toggle will switch back to off once a new setting is created so you will need to turn it back on to save more intent actions. You can also clear out the setting value to remove the setting in the next update.\n\nIf you are not receiving all intents then you will need to add categories that the intent expects. To do this you will need to add each category after the intent separated by a \",\" repeating until there are no more categories. For example an intent with 2 categories will be: \"intent,category1,category2\" as the setting value.\n\nYou must restart the application after making changes to these settings to take effect.</string>
<string name="sensor_description_light_sensor">The current level of illuminance</string>
<string name="sensor_description_location_accurate">Allow Home Assistant to send a notification to request an accurate location along with the application periodically requesting an accurate location. The Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant. The Minimum time between updates (in milliseconds) keeps the device from sending the accurate location too often. The Include in sensor update setting will make a location request with each sensor update.</string>
<string name="sensor_description_location_background">Update your location behind the scenes, periodically. The Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant.</string>
<string name="sensor_description_location_background">Update your location behind the scenes, periodically.\n\nThe Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant.\nThe Location Sent setting will allow you to change how detailed the location information is that is sent to your server.</string>
<string name="sensor_description_location_zone">Import existing Home Assistant zones as geofences for zone based tracking. The Minimum Accuracy setting will allow you to decide how accurate the device location (in meters) has to be in order to send to Home Assistant.</string>
<string name="sensor_description_lullaby">If a lullaby is playing</string>
<string name="sensor_description_media_session">The state will either be the playback state of the primary media session or it will be unavailable when you have no active sessions. Attributes will include data from all active sessions including total count.</string>
@ -599,6 +599,9 @@
<string name="sensor_setting_lastreboot_deadband_title">Deadband</string>
<string name="sensor_setting_lastupdate_add_new_intent_title">Add New Intent</string>
<string name="sensor_setting_lastupdate_intent_title" translatable="false">Intent %1$1s</string>
<string name="sensor_setting_location_send_as_title">Location Sent</string>
<string name="sensor_setting_location_send_as_exact_label">Exact</string>
<string name="sensor_setting_location_send_as_zone_only_label">Zone name only</string>
<string name="sensor_setting_location_ham_enabled_title">High accuracy mode (May drain battery fast)</string>
<string name="sensor_setting_location_ham_zone_bt_combined_title">High accuracy mode enabled only when in zone and BT device connected</string>
<string name="sensor_setting_location_ham_only_bt_dev_title">High accuracy mode only when connected to BT devices</string>