diff --git a/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt b/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt index d2a6f3d3fcb..6e5c95f2b81 100644 --- a/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt +++ b/app/src/full/kotlin/io/homeassistant/companion/android/sensors/LocationSensorManager.kt @@ -936,19 +936,31 @@ class LocationSensorManager : val updateLocationAs: String = getSendLocationAsSetting(serverId) if (updateLocationAs == SEND_LOCATION_AS_ZONE_ONLY) { val zones = getZones(serverId) - val locationZone = zones + val inZones = zones .filter { - val passive = it.attributes["passive"] as? Boolean val radius = it.attributes["radius"] as? Number - return@filter passive == false && radius != null && it.containsWithAccuracy(location) - } - .minByOrNull { (it.attributes["radius"] as? Number ?: Int.MAX_VALUE).toFloat() } + return@filter radius != null && it.containsWithAccuracy(location) + }.sortedBy { (it.attributes["radius"] as? Number ?: Int.MAX_VALUE).toFloat() } + val locationZone = inZones.firstOrNull { it.attributes["passive"] as? Boolean == false } val locationName = locationZone?.entityId?.split(".")?.getOrNull(1) ?: ZONE_NAME_NOT_HOME + // Send both `location_name` (deprecated) and `in_zones` (its replacement, per + // https://github.com/home-assistant/architecture/discussions/1387) so the payload works + // against both pre- and post-deprecation Core servers during the rollout window. updateLocation = UpdateLocation( gps = null, gpsAccuracy = null, locationName = locationName, + inZones = if (serverManager(latestContext).getServer(serverId)?.version?.isAtLeast( + 2026, + 6, + 0, + ) == true + ) { + inZones.map { it.entityId } + } else { + null + }, speed = null, altitude = null, course = null, @@ -960,6 +972,7 @@ class LocationSensorManager : gps = listOf(location.latitude, location.longitude), gpsAccuracy = accuracy, locationName = null, + inZones = null, speed = location.speed.toInt(), altitude = location.altitude.toInt(), course = location.bearing.toInt(), @@ -1358,6 +1371,7 @@ class LocationSensorManager : Manifest.permission.BLUETOOTH_CONNECT, ) } + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) -> { arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, @@ -1367,6 +1381,7 @@ class LocationSensorManager : Manifest.permission.BLUETOOTH, ) } + else -> { arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, @@ -1443,6 +1458,7 @@ class LocationSensorManager : latitude = if (updateLocation != null) updateLocation.gps?.getOrNull(0) else location.latitude, longitude = if (updateLocation != null) updateLocation.gps?.getOrNull(1) else location.longitude, locationName = updateLocation?.locationName, + inZones = updateLocation?.inZones.orEmpty(), accuracy = updateLocation?.gpsAccuracy ?: location.accuracy.toInt(), data = updateLocation?.toString(), serverId = serverId, diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/settings/developer/location/views/LocationTrackingView.kt b/app/src/main/kotlin/io/homeassistant/companion/android/settings/developer/location/views/LocationTrackingView.kt index fe3717c1b22..47a303c78c5 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/settings/developer/location/views/LocationTrackingView.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/settings/developer/location/views/LocationTrackingView.kt @@ -224,7 +224,11 @@ fun LocationTrackingHistoryRow(item: LocationHistoryItem?, servers: List Column { ReadOnlyRow( primaryText = stringResource(commonR.string.location), - secondaryText = (item?.locationName ?: "${item?.latitude}, ${item?.longitude}"), + secondaryText = if (item?.inZones?.isNotEmpty() == true) { + item.inZones.joinToString() + } else { + item?.locationName ?: "${item?.latitude}, ${item?.longitude}" + }, ) ReadOnlyRow( primaryText = stringResource(commonR.string.accuracy), diff --git a/common/schemas/io.homeassistant.companion.android.database.AppDatabase/52.json b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/52.json new file mode 100644 index 00000000000..f7884f525ea --- /dev/null +++ b/common/schemas/io.homeassistant.companion.android.database.AppDatabase/52.json @@ -0,0 +1,1158 @@ +{ + "formatVersion": 1, + "database": { + "version": 52, + "identityHash": "c6c1ea610cb50bc14cffeac69ce49216", + "entities": [ + { + "tableName": "sensor_attributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + } + }, + { + "tableName": "authentication_list", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`host` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`host`))", + "fields": [ + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "host" + ] + } + }, + { + "tableName": "sensors", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT DEFAULT NULL, `last_sent_icon` TEXT DEFAULT NULL, `state_type` TEXT NOT NULL, `type` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `device_class` TEXT, `unit_of_measurement` TEXT, `state_class` TEXT, `entity_category` TEXT, `core_registration` TEXT, `app_registration` TEXT, PRIMARY KEY(`id`, `server_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registered", + "columnName": "registered", + "affinity": "INTEGER", + "defaultValue": "NULL" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSentState", + "columnName": "last_sent_state", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "lastSentIcon", + "columnName": "last_sent_icon", + "affinity": "TEXT", + "defaultValue": "NULL" + }, + { + "fieldPath": "stateType", + "columnName": "state_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceClass", + "columnName": "device_class", + "affinity": "TEXT" + }, + { + "fieldPath": "unitOfMeasurement", + "columnName": "unit_of_measurement", + "affinity": "TEXT" + }, + { + "fieldPath": "stateClass", + "columnName": "state_class", + "affinity": "TEXT" + }, + { + "fieldPath": "entityCategory", + "columnName": "entity_category", + "affinity": "TEXT" + }, + { + "fieldPath": "coreRegistration", + "columnName": "core_registration", + "affinity": "TEXT" + }, + { + "fieldPath": "appRegistration", + "columnName": "app_registration", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "server_id" + ] + } + }, + { + "tableName": "sensor_settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sensor_id` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, `value_type` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `entries` TEXT NOT NULL, PRIMARY KEY(`sensor_id`, `name`))", + "fields": [ + { + "fieldPath": "sensorId", + "columnName": "sensor_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueType", + "columnName": "value_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entries", + "columnName": "entries", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sensor_id", + "name" + ] + } + }, + { + "tableName": "button_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT NOT NULL, `domain` TEXT NOT NULL, `service` TEXT NOT NULL, `service_data` TEXT NOT NULL, `label` TEXT, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `require_authentication` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "service", + "columnName": "service", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceData", + "columnName": "service_data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT" + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "requireAuthentication", + "columnName": "require_authentication", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "camera_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "media_player_controls_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `label` TEXT, `show_skip` INTEGER NOT NULL, `show_seek` INTEGER NOT NULL, `show_volume` INTEGER NOT NULL, `show_source` INTEGER NOT NULL DEFAULT false, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT" + }, + { + "fieldPath": "showSkip", + "columnName": "show_skip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSeek", + "columnName": "show_seek", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showVolume", + "columnName": "show_volume", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showSource", + "columnName": "show_source", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "static_widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `attribute_ids` TEXT, `label` TEXT, `text_size` REAL NOT NULL, `state_separator` TEXT NOT NULL, `attribute_separator` TEXT NOT NULL, `tap_action` TEXT NOT NULL DEFAULT 'REFRESH', `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeIds", + "columnName": "attribute_ids", + "affinity": "TEXT" + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT" + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "stateSeparator", + "columnName": "state_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attributeSeparator", + "columnName": "attribute_separator", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tapAction", + "columnName": "tap_action", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'REFRESH'" + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "todo_widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `entity_id` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, `show_completed` INTEGER NOT NULL DEFAULT true, `latest_update_data` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "showCompleted", + "columnName": "show_completed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "true" + }, + { + "fieldPath": "latestUpdateData", + "columnName": "latest_update_data", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "template_widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `template` TEXT NOT NULL, `text_size` REAL NOT NULL DEFAULT 12.0, `last_update` TEXT NOT NULL, `background_type` TEXT NOT NULL DEFAULT 'DAYNIGHT', `text_color` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "template", + "columnName": "template", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textSize", + "columnName": "text_size", + "affinity": "REAL", + "notNull": true, + "defaultValue": "12.0" + }, + { + "fieldPath": "lastUpdate", + "columnName": "last_update", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "backgroundType", + "columnName": "background_type", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'DAYNIGHT'" + }, + { + "fieldPath": "textColor", + "columnName": "text_color", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "notification_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `message` TEXT NOT NULL, `data` TEXT NOT NULL, `source` TEXT NOT NULL, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "location_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created` INTEGER NOT NULL, `trigger` TEXT NOT NULL, `result` TEXT NOT NULL, `latitude` REAL, `longitude` REAL, `location_name` TEXT, `in_zones` TEXT NOT NULL DEFAULT '[]', `accuracy` INTEGER, `data` TEXT, `server_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trigger", + "columnName": "trigger", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL" + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL" + }, + { + "fieldPath": "locationName", + "columnName": "location_name", + "affinity": "TEXT" + }, + { + "fieldPath": "inZones", + "columnName": "in_zones", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "INTEGER" + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT" + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "qs_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tile_id` TEXT NOT NULL, `added` INTEGER NOT NULL DEFAULT 1, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_name` TEXT, `entity_id` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT, `should_vibrate` INTEGER NOT NULL DEFAULT 0, `auth_required` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tileId", + "columnName": "tile_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "serverId", + "columnName": "server_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "iconName", + "columnName": "icon_name", + "affinity": "TEXT" + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subtitle", + "columnName": "subtitle", + "affinity": "TEXT" + }, + { + "fieldPath": "shouldVibrate", + "columnName": "should_vibrate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "authRequired", + "columnName": "auth_required", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "favorite_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `friendly_name` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "friendlyName", + "columnName": "friendly_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "camera_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT, `refresh_interval` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT" + }, + { + "fieldPath": "refreshInterval", + "columnName": "refresh_interval", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "thermostat_tiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT, `refresh_interval` INTEGER, `target_temperature` REAL, `show_entity_name` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT" + }, + { + "fieldPath": "refreshInterval", + "columnName": "refresh_interval", + "affinity": "INTEGER" + }, + { + "fieldPath": "targetTemperature", + "columnName": "target_temperature", + "affinity": "REAL" + }, + { + "fieldPath": "showEntityName", + "columnName": "show_entity_name", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "entity_state_complications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entity_id` TEXT NOT NULL, `show_title` INTEGER NOT NULL DEFAULT 1, `show_unit` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "entityId", + "columnName": "entity_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "showTitle", + "columnName": "show_title", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "showUnit", + "columnName": "show_unit", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "servers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `_name` TEXT NOT NULL, `name_override` TEXT, `_version` TEXT, `device_registry_id` TEXT, `list_order` INTEGER NOT NULL, `device_name` TEXT, `external_url` TEXT NOT NULL, `internal_url` TEXT, `cloud_url` TEXT, `webhook_id` TEXT, `secret` TEXT, `cloudhook_url` TEXT, `use_cloud` INTEGER NOT NULL, `internal_ssids` TEXT NOT NULL, `internal_ethernet` INTEGER, `internal_vpn` INTEGER, `prioritize_internal` INTEGER NOT NULL, `allow_insecure_connection` INTEGER, `access_token` TEXT, `refresh_token` TEXT, `token_expiration` INTEGER, `token_type` TEXT, `install_id` TEXT, `user_id` TEXT, `user_name` TEXT, `user_is_owner` INTEGER, `user_is_admin` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "_name", + "columnName": "_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nameOverride", + "columnName": "name_override", + "affinity": "TEXT" + }, + { + "fieldPath": "_version", + "columnName": "_version", + "affinity": "TEXT" + }, + { + "fieldPath": "deviceRegistryId", + "columnName": "device_registry_id", + "affinity": "TEXT" + }, + { + "fieldPath": "listOrder", + "columnName": "list_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceName", + "columnName": "device_name", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.externalUrl", + "columnName": "external_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.internalUrl", + "columnName": "internal_url", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.cloudUrl", + "columnName": "cloud_url", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.webhookId", + "columnName": "webhook_id", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.secret", + "columnName": "secret", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.cloudhookUrl", + "columnName": "cloudhook_url", + "affinity": "TEXT" + }, + { + "fieldPath": "connection.useCloud", + "columnName": "use_cloud", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connection.internalSsids", + "columnName": "internal_ssids", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "connection.internalEthernet", + "columnName": "internal_ethernet", + "affinity": "INTEGER" + }, + { + "fieldPath": "connection.internalVpn", + "columnName": "internal_vpn", + "affinity": "INTEGER" + }, + { + "fieldPath": "connection.prioritizeInternal", + "columnName": "prioritize_internal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connection.allowInsecureConnection", + "columnName": "allow_insecure_connection", + "affinity": "INTEGER" + }, + { + "fieldPath": "session.accessToken", + "columnName": "access_token", + "affinity": "TEXT" + }, + { + "fieldPath": "session.refreshToken", + "columnName": "refresh_token", + "affinity": "TEXT" + }, + { + "fieldPath": "session.tokenExpiration", + "columnName": "token_expiration", + "affinity": "INTEGER" + }, + { + "fieldPath": "session.tokenType", + "columnName": "token_type", + "affinity": "TEXT" + }, + { + "fieldPath": "session.installId", + "columnName": "install_id", + "affinity": "TEXT" + }, + { + "fieldPath": "user.id", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "user.name", + "columnName": "user_name", + "affinity": "TEXT" + }, + { + "fieldPath": "user.isOwner", + "columnName": "user_is_owner", + "affinity": "INTEGER" + }, + { + "fieldPath": "user.isAdmin", + "columnName": "user_is_admin", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocket_setting` TEXT NOT NULL, `sensor_update_frequency` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "websocketSetting", + "columnName": "websocket_setting", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensorUpdateFrequency", + "columnName": "sensor_update_frequency", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c6c1ea610cb50bc14cffeac69ce49216')" + ] + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/UpdateLocation.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/UpdateLocation.kt index a85c106e4cb..ce625e4afd2 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/UpdateLocation.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/UpdateLocation.kt @@ -3,7 +3,15 @@ package io.homeassistant.companion.android.common.data.integration data class UpdateLocation( val gps: List?, val gpsAccuracy: Int?, + /** + * Deprecated in core 2026.6.0+; use [inZones] instead. Kept for backwards compatibility. + */ val locationName: String?, + /** + * Full zone entity IDs (e.g. `zone.home`) the device is currently in. Replaces + * [locationName] in core 2026.6.0+, should be `null` when using core <2026.6.0. + */ + val inZones: List?, val speed: Int?, val altitude: Int?, val course: Int?, diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt index ea76a6d43c0..66fcc42e449 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/IntegrationRepositoryImpl.kt @@ -714,13 +714,14 @@ class IntegrationRepositoryImpl @AssistedInject constructor( private fun createUpdateLocation(updateLocation: UpdateLocation): UpdateLocationIntegrationRequest { return UpdateLocationIntegrationRequest( UpdateLocationRequest( - updateLocation.gps, - updateLocation.gpsAccuracy, - updateLocation.locationName, - updateLocation.speed, - updateLocation.altitude, - updateLocation.course, - updateLocation.verticalAccuracy, + gps = updateLocation.gps, + gpsAccuracy = updateLocation.gpsAccuracy, + locationName = updateLocation.locationName, + inZones = updateLocation.inZones, + speed = updateLocation.speed, + altitude = updateLocation.altitude, + course = updateLocation.course, + verticalAccuracy = updateLocation.verticalAccuracy, ), ) } diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequest.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequest.kt index abf12a0c32c..6b150fea56d 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequest.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequest.kt @@ -21,6 +21,7 @@ data class UpdateLocationRequest( val gps: List? = null, val gpsAccuracy: Int? = null, val locationName: String? = null, + val inZones: List? = null, val speed: Int? = null, val altitude: Int? = null, val course: Int? = null, @@ -37,6 +38,7 @@ private object UpdateLocationRequestSerializer : KSerializer? = null var gpsAccuracy: Int? = null var locationName: String? = null + var inZones: List? = null var speed: Int? = null var altitude: Int? = null var course: Int? = null @@ -74,10 +80,11 @@ private object UpdateLocationRequestSerializer : KSerializer gps = decodeSerializableElement(descriptor, index, ListSerializer(Double.serializer())) 1 -> gpsAccuracy = decodeIntElement(descriptor, index) 2 -> locationName = decodeStringElement(descriptor, index) - 3 -> speed = decodeIntElement(descriptor, index) - 4 -> altitude = decodeIntElement(descriptor, index) - 5 -> course = decodeIntElement(descriptor, index) - 6 -> verticalAccuracy = decodeIntElement(descriptor, index) + 3 -> inZones = decodeSerializableElement(descriptor, index, ListSerializer(String.serializer())) + 4 -> speed = decodeIntElement(descriptor, index) + 5 -> altitude = decodeIntElement(descriptor, index) + 6 -> course = decodeIntElement(descriptor, index) + 7 -> verticalAccuracy = decodeIntElement(descriptor, index) CompositeDecoder.DECODE_DONE -> break } } @@ -85,6 +92,7 @@ private object UpdateLocationRequestSerializer : KSerializer = emptyList(), val accuracy: Int?, val data: String?, @ColumnInfo(name = "server_id") @@ -24,17 +35,42 @@ data class LocationHistoryItem( ) { fun toSharingString(serverName: String?): String { val createdString = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(created) + val zonesLine = if (inZones.isNotEmpty()) "In zones: ${inZones.joinToString()}\n" else "" return "Created: $createdString\n" + "Trigger: $trigger\n" + "Result: $result\n" + "Data: $data\n\n" + "Location: ${locationName ?: "$latitude, $longitude"}\n" + + zonesLine + "Accuracy: $accuracy\n" + "Server: ${if (serverId != null) serverName?.ifBlank { serverId } ?: serverId else "null"}" } } +/** + * Room [TypeConverter] for serializing the `in_zones` column (a list of zone entity IDs) to/from + * JSON strings. + */ +class LocationHistoryInZonesConverter { + @TypeConverter + fun fromStringToList(value: String): List { + if (value.isBlank()) return emptyList() + return try { + kotlinJsonMapper.decodeFromString(value) + } catch (_: SerializationException) { + emptyList() + } + } + + @TypeConverter + fun fromListToString(value: List): String = try { + kotlinJsonMapper.encodeToString(value) + } catch (_: SerializationException) { + "" + } +} + enum class LocationHistoryItemTrigger { FLP_BACKGROUND, FLP_FOREGROUND, diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequestTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequestTest.kt index ea58f4a7721..677529cb095 100644 --- a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequestTest.kt +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/impl/entities/UpdateLocationRequestTest.kt @@ -8,12 +8,13 @@ class UpdateLocationRequestTest { @Test fun `Given a valid UpdateLocationRequest when serializing it then it creates a valid JSON`() { assertEquals( - """{"gps":[1.0,2.0],"gps_accuracy":0,"location_name":"Test Location","speed":1,"altitude":2,"course":3,"vertical_accuracy":4}""", + """{"gps":[1.0,2.0],"gps_accuracy":0,"location_name":"Test Location","in_zones":["home","work"],"speed":1,"altitude":2,"course":3,"vertical_accuracy":4}""", kotlinJsonMapper.encodeToString( UpdateLocationRequest( gps = listOf(1.0, 2.0), gpsAccuracy = 0, locationName = "Test Location", + inZones = listOf("home", "work"), speed = 1, altitude = 2, course = 3, @@ -36,12 +37,13 @@ class UpdateLocationRequestTest { gps = listOf(1.0, 2.0), gpsAccuracy = 0, locationName = "Test Location", + inZones = listOf("home", "work"), speed = 1, altitude = 2, course = 3, verticalAccuracy = 4, ), - kotlinJsonMapper.decodeFromString("""{"gps":[1.0,2.0],"gps_accuracy":0,"location_name":"Test Location","speed":1,"altitude":2,"course":3,"vertical_accuracy":4}"""), + kotlinJsonMapper.decodeFromString("""{"gps":[1.0,2.0],"gps_accuracy":0,"location_name":"Test Location","in_zones":["home","work"],"speed":1,"altitude":2,"course":3,"vertical_accuracy":4}"""), ) assertEquals( UpdateLocationRequest(),