Entity state complication improvements (more flexibility) (#3465)

* Add ComplicationType.LONG_TEXT support, friendly state

 - Makes it possible for watch faces to request a long text complication
 - Use the friendly state to support translated states and dates

* Add show title option

 - Allows hiding the title of a complication in case it doesn't look right

* Implement/fix reading state from database

 - When a entity ID is provided in the configuration request, load data for that complication from the database to allow easy reconfiguration
 - Provide more appropriate error messages when complication isn't configured / the entity doesn't exist

* ktlint
This commit is contained in:
Joris Pelgröm 2023-04-08 22:33:16 +02:00 committed by GitHub
parent 95a322fc0b
commit 64ee62b8fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1222 additions and 53 deletions

View File

@ -163,7 +163,6 @@ dependencies {
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.fragment:fragment-ktx:1.5.6")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.5")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.picasso:picasso:2.8")

View File

@ -68,7 +68,7 @@ dependencies {
api("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
api("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-jackson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")

View File

@ -0,0 +1,994 @@
{
"formatVersion": 1,
"database": {
"version": 40,
"identityHash": "9ec60cb96f3febd24dc5713f23f65e50",
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false,
"defaultValue": "NULL"
},
{
"fieldPath": "state",
"columnName": "state",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSentState",
"columnName": "last_sent_state",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "NULL"
},
{
"fieldPath": "lastSentIcon",
"columnName": "last_sent_icon",
"affinity": "TEXT",
"notNull": false,
"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",
"notNull": false
},
{
"fieldPath": "unitOfMeasurement",
"columnName": "unit_of_measurement",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "stateClass",
"columnName": "state_class",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "entityCategory",
"columnName": "entity_category",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coreRegistration",
"columnName": "core_registration",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "appRegistration",
"columnName": "app_registration",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id",
"server_id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "button_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `server_id` INTEGER NOT NULL DEFAULT 0, `icon_id` INTEGER 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": "iconId",
"columnName": "icon_id",
"affinity": "INTEGER",
"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",
"notNull": false
},
{
"fieldPath": "backgroundType",
"columnName": "background_type",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'DAYNIGHT'"
},
{
"fieldPath": "textColor",
"columnName": "text_color",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "requireAuthentication",
"columnName": "require_authentication",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, 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
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
},
{
"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",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, `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",
"notNull": false
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"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": "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",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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_id` INTEGER, `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": "iconId",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "entityId",
"columnName": "entity_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subtitle",
"columnName": "subtitle",
"affinity": "TEXT",
"notNull": false
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
},
{
"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",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, 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"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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, `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, `prioritize_internal` INTEGER NOT NULL, `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",
"notNull": false
},
{
"fieldPath": "_version",
"columnName": "_version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "listOrder",
"columnName": "list_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "deviceName",
"columnName": "device_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.externalUrl",
"columnName": "external_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "connection.internalUrl",
"columnName": "internal_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.cloudUrl",
"columnName": "cloud_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.webhookId",
"columnName": "webhook_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.secret",
"columnName": "secret",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.cloudhookUrl",
"columnName": "cloudhook_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "connection.useCloud",
"columnName": "use_cloud",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "connection.internalSsids",
"columnName": "internal_ssids",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "connection.prioritizeInternal",
"columnName": "prioritize_internal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "session.accessToken",
"columnName": "access_token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "session.refreshToken",
"columnName": "refresh_token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "session.tokenExpiration",
"columnName": "token_expiration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "session.tokenType",
"columnName": "token_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "session.installId",
"columnName": "install_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.id",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.name",
"columnName": "user_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "user.isOwner",
"columnName": "user_is_owner",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "user.isAdmin",
"columnName": "user_is_admin",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"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"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"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, '9ec60cb96f3febd24dc5713f23f65e50')"
]
}
}

View File

@ -87,7 +87,7 @@ import io.homeassistant.companion.android.common.R as commonR
Server::class,
Setting::class
],
version = 39,
version = 40,
autoMigrations = [
AutoMigration(from = 24, to = 25),
AutoMigration(from = 25, to = 26),
@ -103,7 +103,8 @@ import io.homeassistant.companion.android.common.R as commonR
AutoMigration(from = 35, to = 36),
AutoMigration(from = 36, to = 37, spec = AppDatabase.Companion.Migration36to37::class),
AutoMigration(from = 37, to = 38, spec = AppDatabase.Companion.Migration37to38::class),
AutoMigration(from = 38, to = 39)
AutoMigration(from = 38, to = 39),
AutoMigration(from = 39, to = 40)
]
)
@TypeConverters(

View File

@ -13,5 +13,7 @@ data class EntityStateComplications(
@ColumnInfo(name = "id")
val id: Int,
@ColumnInfo(name = "entity_id")
val entityId: String
val entityId: String,
@ColumnInfo(name = "show_title", defaultValue = "1")
val showTitle: Boolean
)

View File

@ -125,7 +125,7 @@
<string name="complication_entity_invalid">Invalid entity</string>
<string name="complication_entity_state_content_description">Entity state</string>
<string name="complication_entity_state_label">Entity state</string>
<string name="complication_entity_state_preview">preview</string>
<string name="complication_entity_state_preview">Preview</string>
<string name="config">Configuration</string>
<string name="configure_service_call">Configure Service Call</string>
<string name="configure_widget_label">Widget Label</string>
@ -721,6 +721,7 @@
<string name="show">Show</string>
<string name="show_share_logs_summary">Sharing logs with the Home Assistant team will help to solve issues. Please share the logs only if you have been asked to do so by a Home Assistant developer</string>
<string name="show_share_logs">Show and Share Logs</string>
<string name="show_entity_title">Show entity name</string>
<string name="sign_in_on_phone">Sign in on phone</string>
<string name="slider_decreased">%1$s decreased</string>
<string name="slider_increased">%1$s increased</string>

View File

@ -96,6 +96,7 @@ dependencies {
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.wear:wear:1.2.0")
implementation("androidx.core:core-ktx:1.10.0")
implementation("com.google.android.gms:play-services-wearable:18.0.0")
implementation("androidx.wear:wear-input:1.2.0-alpha02")
implementation("androidx.wear:wear-remote-interactions:1.0.0")

View File

@ -154,7 +154,7 @@
<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT" />
android:value="SHORT_TEXT,LONG_TEXT" />
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="900" />

View File

@ -9,6 +9,7 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.core.content.IntentCompat
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_COMPLICATION_ID
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_COMPLICATION_TYPE
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_DATA_SOURCE_COMPONENT
@ -33,9 +34,11 @@ class ComplicationConfigActivity : ComponentActivity() {
val id = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_ID, -1)
val type = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_TYPE, -1)
val component = intent.getParcelableExtra<ComponentName>(EXTRA_CONFIG_DATA_SOURCE_COMPONENT)
val component = IntentCompat.getParcelableExtra(intent, EXTRA_CONFIG_DATA_SOURCE_COMPONENT, ComponentName::class.java)
Log.i(TAG, "Config for id $id of type $type for component ${component?.className}")
complicationConfigViewModel.setDataFromIntent(id)
setContent {
LoadConfigView(
complicationConfigViewModel

View File

@ -15,6 +15,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.integration.friendlyName
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
import io.homeassistant.companion.android.data.SimplifiedEntity
@ -29,8 +30,8 @@ import javax.inject.Inject
@HiltViewModel
class ComplicationConfigViewModel @Inject constructor(
application: Application,
favoritesDao: FavoritesDao,
private val serverManager: ServerManager,
private val favoritesDao: FavoritesDao,
private val entityStateComplicationsDao: EntityStateComplicationsDao
) : AndroidViewModel(application) {
companion object {
@ -55,11 +56,28 @@ class ComplicationConfigViewModel @Inject constructor(
private set
var selectedEntity: SimplifiedEntity? by mutableStateOf(null)
private set
var entityShowTitle by mutableStateOf(true)
private set
init {
loadEntities()
}
fun setDataFromIntent(id: Int) {
viewModelScope.launch {
if (!serverManager.isRegistered() || id <= 0) return@launch
val stored = entityStateComplicationsDao.get(id)
stored?.let {
selectedEntity = SimplifiedEntity(entityId = it.entityId)
entityShowTitle = it.showTitle
if (loadingState == LoadingState.READY) {
updateSelectedEntity()
}
}
}
}
private fun loadEntities() {
viewModelScope.launch {
if (!serverManager.isRegistered()) {
@ -73,6 +91,7 @@ class ComplicationConfigViewModel @Inject constructor(
entities[it.entityId] = it
}
updateEntityDomains()
updateSelectedEntity()
// Finished initial load, update state
val webSocketState = serverManager.webSocketRepository().getConnectionState()
@ -111,13 +130,32 @@ class ComplicationConfigViewModel @Inject constructor(
entitiesByDomainOrder.addAll(domainsList)
}
private fun updateSelectedEntity() {
if (selectedEntity == null) return
val fullEntity = entities[selectedEntity!!.entityId]
selectedEntity = if (fullEntity == null) {
null // Clear invalid value
} else {
SimplifiedEntity(
entityId = fullEntity.entityId,
friendlyName = fullEntity.friendlyName,
icon = (fullEntity.attributes as? Map<*, *>)?.get("icon") as? String ?: ""
)
}
}
fun setEntity(entity: SimplifiedEntity) {
selectedEntity = entity
}
fun setShowTitle(show: Boolean) {
entityShowTitle = show
}
fun addEntityStateComplication(id: Int, entity: SimplifiedEntity) {
viewModelScope.launch {
entityStateComplicationsDao.add(EntityStateComplications(id, entity.entityId))
entityStateComplicationsDao.add(EntityStateComplications(id, entity.entityId, entityShowTitle))
}
}

View File

@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.EXTRA_CONFIG_COMPLICATION_ID
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.servers.ServerManager
@ -92,6 +93,20 @@ class ComplicationReceiver : BroadcastReceiver() {
)
}
fun getComplicationConfigureIntent(
context: Context,
complicationInstanceId: Int
): PendingIntent {
return PendingIntent.getActivity(
context,
complicationInstanceId,
Intent(context, ComplicationConfigActivity::class.java).apply {
putExtra(EXTRA_CONFIG_COMPLICATION_ID, complicationInstanceId)
},
PendingIntent.FLAG_IMMUTABLE
)
}
fun getAssistIntent(context: Context): PendingIntent {
return PendingIntent.getActivity(
context,

View File

@ -3,8 +3,10 @@ package io.homeassistant.companion.android.complications
import android.graphics.Color
import android.graphics.drawable.Icon
import android.util.Log
import androidx.annotation.StringRes
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.LongTextComplicationData
import androidx.wear.watchface.complications.data.MonochromaticImage
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
@ -15,9 +17,12 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import com.mikepenz.iconics.utils.colorInt
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.integration.friendlyName
import io.homeassistant.companion.android.common.data.integration.friendlyState
import io.homeassistant.companion.android.common.data.integration.getIcon
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import retrofit2.HttpException
import javax.inject.Inject
@AndroidEntryPoint
@ -34,60 +39,131 @@ class EntityStateDataSourceService : SuspendingComplicationDataSourceService() {
}
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
if (request.complicationType != ComplicationType.SHORT_TEXT) {
if (request.complicationType != ComplicationType.SHORT_TEXT && request.complicationType != ComplicationType.LONG_TEXT) {
return null
}
val id = request.complicationInstanceId
val entityId = entityStateComplicationsDao.get(id)?.entityId
?: return ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(getText(R.string.complication_entity_invalid)).build(),
contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description))
.build()
).build()
val settings = entityStateComplicationsDao.get(request.complicationInstanceId)
val entityId = settings?.entityId
?: return getErrorComplication(request, R.string.complication_entity_invalid, true)
val entity = try {
serverManager.integrationRepository().getEntity(entityId)
?: return ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(getText(R.string.state_unknown)).build(),
contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description))
.build()
).build()
?: return getErrorComplication(request, R.string.state_unknown)
} catch (t: Throwable) {
Log.e(TAG, "Unable to get entity state for $entityId: ${t.message}")
return null
return if (t is HttpException && t.code() == 404) {
getErrorComplication(request, R.string.complication_entity_invalid)
} else {
null
}
}
val attributes = entity.attributes as Map<*, *>
val icon = entity.getIcon(applicationContext) ?: CommunityMaterial.Icon.cmd_bookmark
val iconBitmap = IconicsDrawable(this, icon).apply {
colorInt = Color.WHITE
}.toBitmap()
return ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(entity.state).build(),
contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description))
.build()
)
.setTapAction(ComplicationReceiver.getComplicationToggleIntent(this, request.complicationInstanceId))
.setMonochromaticImage(MonochromaticImage.Builder(Icon.createWithBitmap(iconBitmap)).build())
.setTitle(PlainComplicationText.Builder(attributes["friendly_name"] as String? ?: entity.entityId).build())
.build()
val title = if (settings.showTitle) {
PlainComplicationText.Builder(entity.friendlyName).build()
} else {
null
}
val text = PlainComplicationText.Builder(entity.friendlyState(this)).build()
val contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description)).build()
val monochromaticImage = MonochromaticImage.Builder(Icon.createWithBitmap(iconBitmap)).build()
val tapAction = ComplicationReceiver.getComplicationToggleIntent(this, request.complicationInstanceId)
return when (request.complicationType) {
ComplicationType.SHORT_TEXT -> {
ShortTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
)
.setTitle(title)
.setTapAction(tapAction)
.setMonochromaticImage(monochromaticImage)
.build()
}
ComplicationType.LONG_TEXT -> {
LongTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
)
.setTitle(title)
.setTapAction(tapAction)
.setMonochromaticImage(monochromaticImage)
.build()
}
else -> null // Already handled at the start of the function
}
}
override fun getPreviewData(type: ComplicationType): ComplicationData =
ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(getText(R.string.complication_entity_state_preview)).build(),
contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description)).build()
)
.setMonochromaticImage(
MonochromaticImage.Builder(
Icon.createWithResource(
this,
io.homeassistant.companion.android.R.drawable.ic_lightbulb
)
).build()
override fun getPreviewData(type: ComplicationType): ComplicationData? {
val text = PlainComplicationText.Builder(getText(R.string.complication_entity_state_preview)).build()
val contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description)).build()
val title = PlainComplicationText.Builder(getText(R.string.entity)).build()
val monochromaticImage = MonochromaticImage.Builder(
Icon.createWithResource(
this,
io.homeassistant.companion.android.R.drawable.ic_lightbulb
)
.setTitle(PlainComplicationText.Builder(getText(R.string.entity)).build())
.build()
).build()
return when (type) {
ComplicationType.SHORT_TEXT -> {
ShortTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
)
.setTitle(title)
.setMonochromaticImage(monochromaticImage)
.build()
}
ComplicationType.LONG_TEXT -> {
LongTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
)
.setTitle(title)
.setMonochromaticImage(monochromaticImage)
.build()
}
else -> {
Log.w(TAG, "Preview for unsupported complication type $type requested")
null
}
}
}
/**
* Get a simple complication for errors with [textRes] in the text slot.
*
* @param setTapAction If tapping the complication should open configuration
*/
private fun getErrorComplication(
request: ComplicationRequest,
@StringRes textRes: Int,
setTapAction: Boolean = false
): ComplicationData {
val text = PlainComplicationText.Builder(
if (setTapAction) { "+" } else { getText(textRes) }
).build()
val contentDescription = PlainComplicationText.Builder(getText(R.string.complication_entity_state_content_description)).build()
val tapAction = if (setTapAction) {
ComplicationReceiver.getComplicationConfigureIntent(this, request.complicationInstanceId)
} else {
null
}
return if (request.complicationType == ComplicationType.SHORT_TEXT) {
ShortTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
).setTapAction(tapAction).build()
} else {
LongTextComplicationData.Builder(
text = text,
contentDescription = contentDescription
).setTapAction(tapAction).build()
}
}
}

View File

@ -7,25 +7,29 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.ButtonDefaults
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.Icon
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.HomeAssistantApplication
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.complications.ComplicationConfigViewModel
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.simplifiedEntity
import io.homeassistant.companion.android.views.ChooseEntityView
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
@ -33,7 +37,6 @@ import io.homeassistant.companion.android.views.ThemeLazyColumn
private const val SCREEN_MAIN = "main"
private const val SCREEN_CHOOSE_ENTITY = "choose_entity"
@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun LoadConfigView(
complicationConfigViewModel: ComplicationConfigViewModel,
@ -48,15 +51,16 @@ fun LoadConfigView(
composable(SCREEN_MAIN) {
MainConfigView(
entity = complicationConfigViewModel.selectedEntity,
showTitle = complicationConfigViewModel.entityShowTitle,
loadingState = complicationConfigViewModel.loadingState,
onChooseEntityClicked = {
swipeDismissableNavController.navigate(SCREEN_CHOOSE_ENTITY)
},
onShowTitleClicked = complicationConfigViewModel::setShowTitle,
onAcceptClicked = onAcceptClicked
)
}
composable(SCREEN_CHOOSE_ENTITY) {
val app = complicationConfigViewModel.getApplication<HomeAssistantApplication>()
ChooseEntityView(
entitiesByDomainOrder = complicationConfigViewModel.entitiesByDomainOrder,
entitiesByDomain = complicationConfigViewModel.entitiesByDomain,
@ -76,8 +80,10 @@ fun LoadConfigView(
@Composable
fun MainConfigView(
entity: SimplifiedEntity?,
showTitle: Boolean,
loadingState: ComplicationConfigViewModel.LoadingState,
onChooseEntityClicked: () -> Unit,
onShowTitleClicked: (Boolean) -> Unit,
onAcceptClicked: () -> Unit
) {
ThemeLazyColumn {
@ -119,6 +125,26 @@ fun MainConfigView(
onClick = onChooseEntityClicked
)
}
item {
val isChecked = !loaded || showTitle
ToggleChip(
checked = isChecked,
onCheckedChange = onShowTitleClicked,
label = { Text(stringResource(R.string.show_entity_title)) },
toggleControl = {
Icon(
imageVector = ToggleChipDefaults.switchIcon(isChecked),
contentDescription = if (isChecked) {
stringResource(R.string.enabled)
} else {
stringResource(R.string.disabled)
}
)
},
modifier = Modifier.fillMaxWidth(),
enabled = loaded && entity != null
)
}
item {
Button(
@ -139,3 +165,16 @@ fun MainConfigView(
}
}
}
@Preview(device = Devices.WEAR_OS_LARGE_ROUND)
@Composable
fun PreviewMainConfigView() {
MainConfigView(
entity = simplifiedEntity,
showTitle = true,
loadingState = ComplicationConfigViewModel.LoadingState.READY,
onChooseEntityClicked = {},
onShowTitleClicked = {},
onAcceptClicked = {}
)
}