Entity state complication (#2574)

* Add entity state complication with hardcoded entity id

* Add config activity and save entity id in DB

* Update complications when entity state changes

* Allow all entity types for complication

* Update config layout

* remove unused file

* Upload database json

* Process review comments!

* Fix little bug

* Icon background and error catching

* Add automatic refresh and fix missed Dao inject

* Add icons for standard domains

91cd584b4b/src/common/const.ts (L66)

* Cleanup and further icon updates
This commit is contained in:
leroyboerefijn 2022-06-21 02:24:37 +02:00 committed by GitHub
parent fe377fcad8
commit 50d387a726
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1459 additions and 39 deletions

View file

@ -0,0 +1,705 @@
{
"formatVersion": 1,
"database": {
"version": 29,
"identityHash": "889b2ef9501971685255ec40632d95f9",
"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": {
"columnNames": [
"sensor_id",
"name"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"host"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sensors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `registered` INTEGER DEFAULT NULL, `state` TEXT NOT NULL, `last_sent_state` TEXT NOT 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`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"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": true
},
{
"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": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"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": {
"columnNames": [
"sensor_id",
"name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "button_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `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, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"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
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "camera_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "mediaplayctrls_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, `label` TEXT, `showSkip` INTEGER NOT NULL, `showSeek` INTEGER NOT NULL, `showVolume` INTEGER NOT NULL, `showSource` 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": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "showSkip",
"columnName": "showSkip",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showSeek",
"columnName": "showSeek",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showVolume",
"columnName": "showVolume",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showSource",
"columnName": "showSource",
"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": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "static_widget",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `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": "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": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "template_widgets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `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": "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": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"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)",
"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
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "qs_tiles",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `tileId` TEXT NOT NULL, `icon_id` INTEGER, `entityId` TEXT NOT NULL, `label` TEXT NOT NULL, `subtitle` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tileId",
"columnName": "tileId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconId",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "subtitle",
"columnName": "subtitle",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"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": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "entityStateComplications",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `entityId` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "entityId",
"columnName": "entityId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "settings",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websocketSetting` TEXT NOT NULL, `sensorUpdateFrequency` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "websocketSetting",
"columnName": "websocketSetting",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sensorUpdateFrequency",
"columnName": "sensorUpdateFrequency",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"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, '889b2ef9501971685255ec40632d95f9')"
]
}
}

View file

@ -39,6 +39,8 @@ import io.homeassistant.companion.android.database.settings.LocalNotificationSet
import io.homeassistant.companion.android.database.settings.LocalSensorSettingConverter
import io.homeassistant.companion.android.database.settings.Setting
import io.homeassistant.companion.android.database.settings.SettingsDao
import io.homeassistant.companion.android.database.wear.EntityStateComplications
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import io.homeassistant.companion.android.database.wear.Favorites
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
@ -69,14 +71,16 @@ import io.homeassistant.companion.android.common.R as commonR
NotificationItem::class,
TileEntity::class,
Favorites::class,
EntityStateComplications::class,
Setting::class
],
version = 28,
version = 29,
autoMigrations = [
AutoMigration(from = 24, to = 25),
AutoMigration(from = 25, to = 26),
AutoMigration(from = 26, to = 27),
AutoMigration(from = 27, to = 28, spec = AppDatabase.Companion.Migration27to28::class)
AutoMigration(from = 27, to = 28, spec = AppDatabase.Companion.Migration27to28::class),
AutoMigration(from = 28, to = 29)
]
)
@TypeConverters(
@ -97,6 +101,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun notificationDao(): NotificationDao
abstract fun tileDao(): TileDao
abstract fun favoritesDao(): FavoritesDao
abstract fun entityStateComplicationsDao(): EntityStateComplicationsDao
abstract fun settingsDao(): SettingsDao
companion object {

View file

@ -11,6 +11,7 @@ import io.homeassistant.companion.android.database.notification.NotificationDao
import io.homeassistant.companion.android.database.qs.TileDao
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.settings.SettingsDao
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
import io.homeassistant.companion.android.database.widget.CameraWidgetDao
@ -62,4 +63,7 @@ object DatabaseModule {
@Provides
fun provideSettingsDao(database: AppDatabase): SettingsDao = database.settingsDao()
@Provides
fun provideEntityStateComplicationsDao(database: AppDatabase): EntityStateComplicationsDao = database.entityStateComplicationsDao()
}

View file

@ -0,0 +1,17 @@
package io.homeassistant.companion.android.database.wear
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* Represents the configuration of an entity state complication
*/
@Entity(tableName = "entityStateComplications")
data class EntityStateComplications(
@PrimaryKey
@ColumnInfo(name = "id")
val id: Int,
@ColumnInfo(name = "entityId")
val entityId: String
)

View file

@ -0,0 +1,15 @@
package io.homeassistant.companion.android.database.wear
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface EntityStateComplicationsDao {
@Query("SELECT * FROM entityStateComplications WHERE id = :id")
suspend fun get(id: Int): EntityStateComplications?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun add(entityStateComplications: EntityStateComplications)
}

View file

@ -20,6 +20,7 @@
<string name="auth_error_message">The \'Username\' and \'Password\' fields must be completed</string>
<string name="auth_error">Authentication Error</string>
<string name="auth_request">Authentication Requested</string>
<string name="automation">Automation</string>
<string name="autoplay_video_summary">Autoplay Videos when dashboard is active. Enabling this setting may increase data usage unexpectedly, proceed with caution.</string>
<string name="autoplay_video">Autoplay Videos</string>
<string name="background_access_disabled">Home Assistant does not have access to run in the background. Without this permission the app will not be able to reliably send data back to your server. Click here to request permissions.</string>
@ -88,6 +89,7 @@
<string name="basic_sensor_name_wifi">WiFi Connection</string>
<string name="basic_sensor_sleep_events">Sleep Events</string>
<string name="basic_sensor_sleep_tracking">Sleep Tracking</string>
<string name="binary_sensor">Binary Sensor</string>
<string name="biometric_message">Unlock using your biometric or screenlock credential</string>
<string name="biometric_set_title">Confirm to continue</string>
<string name="biometric_title">Home Assistant is locked</string>
@ -102,9 +104,14 @@
<string name="cancel">Cancel</string>
<string name="changelog">View Full Change Log</string>
<string name="checking_with_home_assistant">Checking with Home Assistant</string>
<string name="choose_entity">Choose entity</string>
<string name="choose_server">Choose server</string>
<string name="clear_favorites">Clear Favorites</string>
<string name="color_temp">Color temperature: %1$d</string>
<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="config">Configuration</string>
<string name="configure_service_call">Configure Service Call</string>
<string name="configure_widget_label">Widget Label</string>
@ -137,6 +144,7 @@
<string name="developer_tools">Developer Tools</string>
<string name="device_name">Device Name</string>
<string name="device_registration">Device Registration</string>
<string name="device_tracker">Device tracker</string>
<string name="dialog_add_panel_shortcut_content">Create a shortcut to this item?</string>
<string name="dialog_add_panel_shortcut_title">Add Shortcut</string>
<string name="disable_all_sensors">Disable All %1$d Sensors</string>
@ -171,6 +179,7 @@
<string name="enable_sensor_missing_permission_nearby_devices">Requires \'Nearby devices\' permission</string>
<string name="enable_sensor_missing_permission_phone">Requires \'Phone\' permission</string>
<string name="enabled">Enabled</string>
<string name="entity">Entity</string>
<plurals name="entities_found">
<item quantity="one">%d entity found</item>
<item quantity="other">%d entities found</item>
@ -309,6 +318,7 @@
<string name="manual_title">What is your Home Assistant URL?</string>
<string name="map">Map</string>
<string name="maximum">Maximum</string>
<string name="media_player">Media player</string>
<string name="media_player_widget_desc">Control any media player and see current now playing image</string>
<string name="message_checking">Checking Wear Devices with App</string>
<string name="message_missing_all">The Wear app is missing on your watch, click the button below to install the app.\n\nNote: Currently the Wear OS app requires you to be enrolled in the beta for the phone app. If the button does not work then please join the beta: https://play.google.com/apps/testing/io.homeassistant.companion.android</string>
@ -370,6 +380,8 @@
<string name="password">Password</string>
<string name="permission_explanation_calls">In order to track incoming and outgoing call\'s occurrence we need access to your phone state. No phone numbers or other call details will be stored.</string>
<string name="permission_explanation">In order to use location tracking features or different connection urls based on WiFi SSID we need access to your location. If you want consistent background updates you will also need to allow background processing</string>
<string name="persistent_notification">Persistent notification</string>
<string name="person">Person</string>
<string name="pin_shortcut">Pin Shortcut</string>
<string name="pref_call_tracking_summary">Allow application to detect call occurrence and notify server about it.</string>
<string name="pref_call_tracking_title">Calls Tracking</string>
@ -419,6 +431,7 @@
<string name="security_vulnerably_understand">I Understand</string>
<string name="security_vulnerably_view">View Bulletin</string>
<string name="security">Security</string>
<string name="select">Select</string>
<string name="select_entity_to_display">Select Entity to display</string>
<string name="select_instance">Select the instance you would like to connect to:</string>
<string name="sensor_description_active_notification_count">Total count of active notifications that are visible to the user including silent, persistent and the Sensor Worker notifications.</string>
@ -657,6 +670,7 @@
<string name="store_request_successful">Request to install app on wear device sent successfully</string>
<string name="store_request_unsuccessful">Play Store Request Failed. Wear device(s) may not support Play Store, that is, the Wear device may be version 1.0.</string>
<string name="successful">Successful</string>
<string name="sun">Sun</string>
<string name="switches">Switches</string>
<string name="tag_reader_title">Processing Tag</string>
<string name="template_tile">Template tile</string>
@ -860,4 +874,7 @@
<string name="sensor_description_volume_dtmf">Volume level for DTMF tones on the device</string>
<string name="basic_sensor_name_high_accuracy_interval">High Accuracy Update Interval</string>
<string name="sensor_description_high_accuracy_interval">The update interval set for high accuracy mode</string>
<string name="update">Update</string>
<string name="weather">Weather</string>
<string name="zone">Zone</string>
</resources>

View file

@ -12,7 +12,7 @@ android {
defaultConfig {
applicationId = "io.homeassistant.companion.android"
minSdk = 25
minSdk = 26
targetSdk = 30
versionName = System.getenv("VERSION") ?: "LOCAL"
@ -92,7 +92,6 @@ dependencies {
implementation("com.google.android.material:material:1.6.0")
implementation("androidx.wear:wear:1.2.0")
implementation("com.google.android.support:wearable:2.9.0")
implementation("com.google.android.gms:play-services-wearable:17.1.0")
implementation("androidx.wear:wear-input:1.2.0-alpha02")
implementation("androidx.wear:wear-remote-interactions:1.0.0")
@ -120,4 +119,6 @@ dependencies {
implementation("com.google.guava:guava:31.1-android")
implementation("androidx.wear.tiles:tiles:1.0.1")
implementation("androidx.wear.watchface:watchface-complications-data-source-ktx:1.1.0-rc01")
}

View file

@ -53,6 +53,14 @@
<activity android:name=".onboarding.integration.MobileAppIntegrationActivity" />
<activity android:name=".onboarding.authentication.AuthenticationActivity" />
<activity android:name=".onboarding.manual_setup.ManualSetupActivity" />
<activity android:name=".complications.ComplicationConfigActivity"
android:exported="true">
<intent-filter>
<action android:name="io.homeassistant.companion.android.ENTITY_STATE_COMPLICATION_CONFIG" />
<category android:name="android.support.wearable.complications.category.PROVIDER_CONFIG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- To show confirmations and failures -->
<activity android:name="androidx.wear.activity.ConfirmationActivity" />
@ -90,6 +98,28 @@
<action android:name="io.homeassistant.companion.android.TILE_ACTION" />
</intent-filter>
</receiver>
<!-- Complications -->
<service android:name=".complications.EntityStateDataSourceService"
android:exported="true"
android:icon="@drawable/ic_lightbulb"
android:label="@string/complication_entity_state_label"
android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" />
</intent-filter>
<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT" />
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="900" />
<meta-data
android:name="android.support.wearable.complications.PROVIDER_CONFIG_ACTION"
android:value="io.homeassistant.companion.android.ENTITY_STATE_COMPLICATION_CONFIG" />
</service>
<receiver android:name=".complications.ComplicationReceiver" />
<service android:name=".phone.PhoneSettingsListener" android:exported="true">
<intent-filter>

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.content.Intent
import android.content.IntentFilter
import dagger.hilt.android.HiltAndroidApp
import io.homeassistant.companion.android.complications.ComplicationReceiver
import io.homeassistant.companion.android.sensors.SensorReceiver
@HiltAndroidApp
@ -24,5 +25,13 @@ open class HomeAssistantApplication : Application() {
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
)
// Update complications when the screen is on
val complicationReceiver = ComplicationReceiver()
val screenIntentFilter = IntentFilter()
screenIntentFilter.addAction(Intent.ACTION_SCREEN_ON)
registerReceiver(complicationReceiver, screenIntentFilter)
}
}

View file

@ -0,0 +1,54 @@
package io.homeassistant.companion.android.complications
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
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
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.complications.views.LoadConfigView
@AndroidEntryPoint
class ComplicationConfigActivity : ComponentActivity() {
private val complicationConfigViewModel by viewModels<ComplicationConfigViewModel>()
companion object {
private const val TAG = "EntityStateConfigActivity"
fun newInstance(context: Context): Intent {
return Intent(context, ComplicationConfigActivity::class.java)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
Log.i(TAG, "Config for id $id of type $type for component ${component?.className}")
setContent {
LoadConfigView(
complicationConfigViewModel
) {
setResult(Activity.RESULT_OK)
complicationConfigViewModel.selectedEntity?.let {
complicationConfigViewModel.addEntityStateComplication(
id,
it
)
}
finish()
}
}
}
}

View file

@ -0,0 +1,127 @@
package io.homeassistant.companion.android.complications
import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
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.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketState
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.database.wear.EntityStateComplications
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ComplicationConfigViewModel @Inject constructor(
application: Application,
private val integrationUseCase: IntegrationRepository,
private val webSocketUseCase: WebSocketRepository,
private val entityStateComplicationsDao: EntityStateComplicationsDao
) : AndroidViewModel(application) {
companion object {
const val TAG = "ComplicationConfigViewModel"
}
enum class LoadingState {
LOADING, READY, ERROR
}
val app = getApplication<HomeAssistantApplication>()
var entities = mutableStateMapOf<String, Entity<*>>()
private set
var entitiesByDomain = mutableStateMapOf<String, SnapshotStateList<Entity<*>>>()
private set
var entitiesByDomainOrder = mutableStateListOf<String>()
private set
var loadingState by mutableStateOf(LoadingState.LOADING)
private set
var selectedEntity: SimplifiedEntity? by mutableStateOf(null)
private set
init {
loadEntities()
}
private fun loadEntities() {
viewModelScope.launch {
if (!integrationUseCase.isRegistered()) {
loadingState = LoadingState.ERROR
return@launch
}
try {
// Load initial state
loadingState = LoadingState.LOADING
integrationUseCase.getEntities()?.forEach {
entities[it.entityId] = it
}
updateEntityDomains()
// Finished initial load, update state
val webSocketState = webSocketUseCase.getConnectionState()
if (webSocketState == WebSocketState.CLOSED_AUTH) {
loadingState = LoadingState.ERROR
return@launch
}
loadingState = if (webSocketState == WebSocketState.ACTIVE) {
LoadingState.READY
} else {
LoadingState.ERROR
}
// Listen for updates
viewModelScope.launch {
integrationUseCase.getEntityUpdates()?.collect {
entities[it.entityId] = it
updateEntityDomains()
}
}
} catch (e: Exception) {
Log.e(TAG, "Exception while loading entities", e)
loadingState = LoadingState.ERROR
}
}
}
private fun updateEntityDomains() {
val entitiesList = entities.values.toList().sortedBy { it.entityId }
val domainsList = entitiesList.map { it.domain }.distinct()
// Create a list with all discovered domains + their entities
domainsList.forEach { domain ->
val entitiesInDomain = mutableStateListOf<Entity<*>>()
entitiesInDomain.addAll(entitiesList.filter { it.domain == domain })
entitiesByDomain[domain]?.let {
it.clear()
it.addAll(entitiesInDomain)
} ?: run {
entitiesByDomain[domain] = entitiesInDomain
}
}
entitiesByDomainOrder.clear()
entitiesByDomainOrder.addAll(domainsList)
}
fun setEntity(entity: SimplifiedEntity) {
selectedEntity = entity
}
fun addEntityStateComplication(id: Int, entity: SimplifiedEntity) {
viewModelScope.launch {
entityStateComplicationsDao.add(EntityStateComplications(id, entity.entityId))
}
}
}

View file

@ -0,0 +1,96 @@
package io.homeassistant.companion.android.complications
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class ComplicationReceiver : BroadcastReceiver() {
@Inject
lateinit var integrationUseCase: IntegrationRepository
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
override fun onReceive(context: Context, intent: Intent) {
val result = goAsync()
try {
when (intent.action) {
UPDATE_COMPLICATION -> updateComplication(context, intent.getIntExtra(EXTRA_ID, -1))
Intent.ACTION_SCREEN_ON -> onScreenOn(context)
}
} finally {
result.finish()
}
}
private fun updateComplication(context: Context, id: Int) {
scope.launch {
// Request an update for the complication that has just been toggled.
ComplicationDataSourceUpdateRequester
.create(
context = context,
complicationDataSourceComponent = ComponentName(context, EntityStateDataSourceService::class.java)
)
.requestUpdate(id)
}
}
private fun updateAllComplications(context: Context) {
ComplicationDataSourceUpdateRequester
.create(
context = context,
complicationDataSourceComponent = ComponentName(context, EntityStateDataSourceService::class.java)
)
.requestUpdateAll()
}
private fun onScreenOn(context: Context) {
scope.launch {
if (!integrationUseCase.isRegistered()) {
return@launch
}
updateAllComplications(context)
}
}
companion object {
private const val TAG = "ComplicationReceiver"
const val UPDATE_COMPLICATION = "update_complication"
private const val EXTRA_ID = "complication_instance_id"
/**
* Returns a pending intent, suitable for use as a tap intent, that causes a complication to be
* toggled and updated.
*/
fun getComplicationToggleIntent(
context: Context,
complicationInstanceId: Int
): PendingIntent {
val intent = Intent(context, ComplicationReceiver::class.java).apply {
action = UPDATE_COMPLICATION
putExtra(EXTRA_ID, complicationInstanceId)
}
// Pass complicationId as the requestCode to ensure that different complications get
// different intents.
return PendingIntent.getBroadcast(
context,
complicationInstanceId,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}
}

View file

@ -0,0 +1,92 @@
package io.homeassistant.companion.android.complications
import android.graphics.Color
import android.graphics.drawable.Icon
import android.util.Log
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.MonochromaticImage
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
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.IntegrationRepository
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import io.homeassistant.companion.android.util.getIcon
import javax.inject.Inject
@AndroidEntryPoint
class EntityStateDataSourceService : SuspendingComplicationDataSourceService() {
@Inject
lateinit var integrationUseCase: IntegrationRepository
@Inject
lateinit var entityStateComplicationsDao: EntityStateComplicationsDao
companion object {
const val TAG = "EntityStateDataSourceService"
}
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
if (request.complicationType != ComplicationType.SHORT_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 entity = try {
integrationUseCase.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()
} catch (t: Throwable) {
Log.e(TAG, "Unable to get entity state for $entityId: ${t.message}")
return null
}
val attributes = entity.attributes as Map<*, *>
val icon = getIcon(entity, entity.domain, 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()
}
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(),
)
.setTitle(PlainComplicationText.Builder(getText(R.string.entity)).build())
.build()
}

View file

@ -0,0 +1,139 @@
package io.homeassistant.companion.android.complications.views
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
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.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.Text
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.views.ChooseEntityView
import io.homeassistant.companion.android.views.ListHeader
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,
onAcceptClicked: () -> Unit
) {
WearAppTheme {
val swipeDismissableNavController = rememberSwipeDismissableNavController()
SwipeDismissableNavHost(
navController = swipeDismissableNavController,
startDestination = SCREEN_MAIN
) {
composable(SCREEN_MAIN) {
MainConfigView(
entity = complicationConfigViewModel.selectedEntity,
loadingState = complicationConfigViewModel.loadingState,
onChooseEntityClicked = {
swipeDismissableNavController.navigate(SCREEN_CHOOSE_ENTITY)
},
onAcceptClicked = onAcceptClicked
)
}
composable(SCREEN_CHOOSE_ENTITY) {
val app = complicationConfigViewModel.getApplication<HomeAssistantApplication>()
ChooseEntityView(
entitiesByDomainOrder = complicationConfigViewModel.entitiesByDomainOrder,
entitiesByDomain = complicationConfigViewModel.entitiesByDomain,
onNoneClicked = {},
onEntitySelected = { entity ->
complicationConfigViewModel.setEntity(entity)
swipeDismissableNavController.navigateUp()
},
allowNone = false
)
}
}
}
}
@Composable
fun MainConfigView(
entity: SimplifiedEntity?,
loadingState: ComplicationConfigViewModel.LoadingState,
onChooseEntityClicked: () -> Unit,
onAcceptClicked: () -> Unit
) {
ThemeLazyColumn {
item {
ListHeader(id = R.string.complication_entity_state_label)
}
if (loadingState != ComplicationConfigViewModel.LoadingState.ERROR) {
val loaded = loadingState == ComplicationConfigViewModel.LoadingState.READY
item {
val iconBitmap = getIcon(
entity?.icon,
entity?.domain ?: "light",
LocalContext.current
)
Chip(
modifier = Modifier.fillMaxWidth(),
icon = {
Image(
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},
colors = ChipDefaults.secondaryChipColors(),
label = {
Text(
text = stringResource(id = R.string.choose_entity)
)
},
secondaryLabel = {
Text(
if (loaded)
entity?.friendlyName ?: ""
else
stringResource(R.string.loading)
)
},
enabled = loaded,
onClick = onChooseEntityClicked
)
}
item {
Button(
modifier = Modifier.padding(top = 8.dp),
onClick = { onAcceptClicked() },
colors = ButtonDefaults.primaryButtonColors(),
enabled = loaded && entity != null
) {
Image(
CommunityMaterial.Icon.cmd_check
)
}
}
} else {
item {
Text(text = stringResource(R.string.error_connection_failed))
}
}
}
}

View file

@ -36,6 +36,8 @@ import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.util.getColorTemperature
import io.homeassistant.companion.android.util.onEntityClickedFeedback
import io.homeassistant.companion.android.util.onEntityFeedback
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import java.text.DateFormat
@Composable

View file

@ -16,6 +16,10 @@ import io.homeassistant.companion.android.util.playPreviewEntityScene2
import io.homeassistant.companion.android.util.playPreviewEntityScene3
import io.homeassistant.companion.android.util.previewEntity1
import io.homeassistant.companion.android.util.previewEntity2
import io.homeassistant.companion.android.views.ExpandableListHeader
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.views.rememberExpandedStates
import io.homeassistant.companion.android.common.R as commonR
@Composable

View file

@ -53,7 +53,7 @@ fun EntityUi(
.fillMaxWidth(),
appIcon = {
Image(
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},
@ -90,7 +90,7 @@ fun EntityUi(
.fillMaxWidth(),
icon = {
Image(
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},

View file

@ -27,6 +27,8 @@ import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.tiles.ShortcutsTile
import io.homeassistant.companion.android.tiles.TemplateTile
import io.homeassistant.companion.android.views.ChooseEntityView
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.common.R as commonR
private const val ARG_SCREEN_SENSOR_MANAGER_ID = "sensorManagerId"
@ -199,13 +201,14 @@ fun LoadHomePage(
}
composable(SCREEN_SELECT_TILE_SHORTCUT) {
ChooseEntityView(
mainViewModel,
{
entitiesByDomainOrder = mainViewModel.entitiesByDomainOrder,
entitiesByDomain = mainViewModel.entitiesByDomain,
onNoneClicked = {
mainViewModel.clearTileShortcut(shortcutEntitySelectionIndex)
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
swipeDismissableNavController.navigateUp()
},
{ entity ->
onEntitySelected = { entity ->
mainViewModel.setTileShortcut(shortcutEntitySelectionIndex, entity)
TileService.getUpdater(context).requestUpdate(ShortcutsTile::class.java)
swipeDismissableNavController.navigateUp()

View file

@ -35,6 +35,9 @@ 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.onEntityClickedFeedback
import io.homeassistant.companion.android.views.ExpandableListHeader
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable
@ -86,7 +89,7 @@ fun MainView(
.fillMaxWidth(),
icon = {
Image(
asset = CommunityMaterial.Icon.cmd_cellphone,
asset = CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},

View file

@ -17,6 +17,7 @@ import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.IntervalToString
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.common.R as R
@Composable

View file

@ -9,6 +9,8 @@ import androidx.wear.compose.material.rememberScalingLazyListState
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.database.sensor.Sensor
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
@Composable
fun SensorManagerUi(

View file

@ -18,6 +18,8 @@ import androidx.wear.compose.material.rememberScalingLazyListState
import io.homeassistant.companion.android.common.sensors.SensorManager
import io.homeassistant.companion.android.sensors.SensorReceiver
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable

View file

@ -22,6 +22,10 @@ import io.homeassistant.companion.android.home.MainViewModel
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.views.ExpandableListHeader
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.views.rememberExpandedStates
import io.homeassistant.companion.android.common.R as commonR
@Composable
@ -99,7 +103,7 @@ private fun FavoriteToggleChip(
.fillMaxWidth(),
appIcon = {
Image(
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(wearColorPalette.onSurface)
)
},

View file

@ -24,6 +24,8 @@ 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.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable
@ -75,7 +77,7 @@ fun SetTileShortcutsView(
.fillMaxWidth(),
icon = {
Image(
iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(Color.White)
)
},

View file

@ -23,6 +23,8 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.previewFavoritesList
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
import io.homeassistant.companion.android.common.R as commonR
@Composable

View file

@ -15,6 +15,8 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.theme.wearColorPalette
import io.homeassistant.companion.android.util.IntervalToString
import io.homeassistant.companion.android.views.ListHeader
import io.homeassistant.companion.android.views.ThemeLazyColumn
@Composable
fun TemplateTileSettingsView(

View file

@ -109,7 +109,7 @@ class ShortcutsTile : TileService() {
entity.icon,
entity.domain,
this@ShortcutsTile
) ?: CommunityMaterial.Icon.cmd_cellphone
) ?: CommunityMaterial.Icon.cmd_bookmark
val iconBitmap = IconicsDrawable(this@ShortcutsTile, iconIIcon).apply {
colorInt = Color.WHITE
sizeDp = iconSize.roundToInt()

View file

@ -8,9 +8,29 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.HomePresenterImpl
import java.util.Calendar
import io.homeassistant.companion.android.common.R as commonR
fun stringForDomain(domain: String, context: Context): String? =
(
HomePresenterImpl.domainsWithNames + mapOf(
"automation" to commonR.string.automation,
"binary_sensor" to commonR.string.binary_sensor,
"device_tracker" to commonR.string.device_tracker,
"input_number" to commonR.string.domain_input_number,
"media_player" to commonR.string.media_player,
"persistent_notification" to commonR.string.persistent_notification,
"person" to commonR.string.person,
"select" to commonR.string.select,
"sensor" to commonR.string.sensor,
"sun" to commonR.string.sun,
"update" to commonR.string.update,
"weather" to commonR.string.weather,
"zone" to commonR.string.zone
)
)[domain]?.let { context.getString(it) }
fun getIcon(icon: String?, domain: String, context: Context): IIcon? {
val simpleEntity = Entity(
"",
@ -40,13 +60,27 @@ fun getIcon(entity: Entity<Map<String, Any>>?, domain: String, context: Context)
else
entity?.attributes?.get("state") as String?
when (domain) {
"alert" -> CommunityMaterial.Icon.cmd_alert
"air_quality" -> CommunityMaterial.Icon.cmd_air_filter
"automation" -> CommunityMaterial.Icon3.cmd_robot
"button" -> when (entity?.attributes?.get("device_class")) {
"restart" -> CommunityMaterial.Icon3.cmd_restart
"update" -> CommunityMaterial.Icon3.cmd_package_up
else -> CommunityMaterial.Icon2.cmd_gesture_tap_button
}
"calendar" -> CommunityMaterial.Icon.cmd_calendar
"camera" -> CommunityMaterial.Icon3.cmd_video
"climate" -> CommunityMaterial.Icon3.cmd_thermostat
"configurator" -> CommunityMaterial.Icon.cmd_cog
"conversation" -> CommunityMaterial.Icon3.cmd_text_to_speech
"cover" -> coverIcon(compareState, entity)
"counter" -> CommunityMaterial.Icon.cmd_counter
"fan" -> CommunityMaterial.Icon2.cmd_fan
"google_assistant" -> CommunityMaterial.Icon2.cmd_google_assistant
"group" -> CommunityMaterial.Icon2.cmd_google_circles_communities
"homeassistant" -> CommunityMaterial.Icon2.cmd_home_assistant
"homekit" -> CommunityMaterial.Icon2.cmd_home_automation
"image_processing" -> CommunityMaterial.Icon2.cmd_image_filter_frames
"input_boolean" -> if (entity?.entityId?.isNotBlank() == true) {
if (compareState == "on")
CommunityMaterial.Icon.cmd_check_circle_outline
@ -56,6 +90,14 @@ fun getIcon(entity: Entity<Map<String, Any>>?, domain: String, context: Context)
CommunityMaterial.Icon2.cmd_light_switch
}
"input_button" -> CommunityMaterial.Icon2.cmd_gesture_tap_button
"input_datetime" -> if (entity?.attributes?.get("has_date") == false)
CommunityMaterial.Icon.cmd_clock
else if (entity?.attributes?.get("has_time") == false)
CommunityMaterial.Icon.cmd_calendar
else
CommunityMaterial.Icon.cmd_calendar_clock
"input_select" -> CommunityMaterial.Icon2.cmd_format_list_bulleted
"input_text" -> CommunityMaterial.Icon2.cmd_form_textbox
"light" -> CommunityMaterial.Icon2.cmd_lightbulb
"lock" -> when (compareState) {
"unlocked" -> CommunityMaterial.Icon2.cmd_lock_open
@ -63,8 +105,24 @@ fun getIcon(entity: Entity<Map<String, Any>>?, domain: String, context: Context)
"locking", "unlocking" -> CommunityMaterial.Icon2.cmd_lock_clock
else -> CommunityMaterial.Icon2.cmd_lock
}
"script" -> CommunityMaterial.Icon3.cmd_script_text_outline // Different from frontend: outline version
"mailbox" -> CommunityMaterial.Icon3.cmd_mailbox
"notify" -> CommunityMaterial.Icon.cmd_comment_alert
"number" -> CommunityMaterial.Icon3.cmd_ray_vertex
"persistent_notification" -> CommunityMaterial.Icon.cmd_bell
"person" -> CommunityMaterial.Icon.cmd_account
"plant" -> CommunityMaterial.Icon2.cmd_flower
"proximity" -> CommunityMaterial.Icon.cmd_apple_safari
"remote" -> CommunityMaterial.Icon3.cmd_remote
"scene" -> CommunityMaterial.Icon3.cmd_palette_outline // Different from frontend: outline version
"script" -> CommunityMaterial.Icon3.cmd_script_text_outline // Different from frontend: outline version
"select" -> CommunityMaterial.Icon2.cmd_format_list_bulleted
"sensor" -> CommunityMaterial.Icon.cmd_eye
"siren" -> CommunityMaterial.Icon.cmd_bullhorn
"simple_alarm" -> CommunityMaterial.Icon.cmd_bell
"sun" -> if (compareState == "above_horizon")
CommunityMaterial.Icon3.cmd_white_balance_sunny
else
CommunityMaterial.Icon3.cmd_weather_night
"switch" -> if (entity?.entityId?.isNotBlank() == true) {
when (entity.attributes["device_class"]) {
"outlet" -> if (compareState == "on") CommunityMaterial.Icon3.cmd_power_plug else CommunityMaterial.Icon3.cmd_power_plug_off
@ -74,7 +132,13 @@ fun getIcon(entity: Entity<Map<String, Any>>?, domain: String, context: Context)
} else { // For SimplifiedEntity without state, use a more generic icon
CommunityMaterial.Icon2.cmd_light_switch
}
else -> CommunityMaterial.Icon.cmd_cellphone
"timer" -> CommunityMaterial.Icon3.cmd_timer_outline
"updater" -> CommunityMaterial.Icon.cmd_cloud_upload
"vacuum" -> CommunityMaterial.Icon3.cmd_robot_vacuum
"water_heater" -> CommunityMaterial.Icon3.cmd_thermometer
"weather" -> CommunityMaterial.Icon3.cmd_weather_cloudy
"zone" -> CommunityMaterial.Icon3.cmd_map_marker_radius
else -> CommunityMaterial.Icon.cmd_bookmark
}
}
}

View file

@ -1,13 +1,16 @@
package io.homeassistant.companion.android.home.views
package io.homeassistant.companion.android.views
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
@ -19,44 +22,50 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.common.data.integration.domain
import io.homeassistant.companion.android.data.SimplifiedEntity
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.stringForDomain
import java.util.Locale
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun ChooseEntityView(
mainViewModel: MainViewModel,
entitiesByDomainOrder: SnapshotStateList<String>,
entitiesByDomain: SnapshotStateMap<String, SnapshotStateList<Entity<*>>>,
onNoneClicked: () -> Unit,
onEntitySelected: (entity: SimplifiedEntity) -> Unit
onEntitySelected: (entity: SimplifiedEntity) -> Unit,
allowNone: Boolean = true
) {
// Remember expanded state of each header
val expandedStates = rememberExpandedStates(mainViewModel.supportedDomains())
val expandedStates = rememberExpandedStates(entitiesByDomainOrder)
WearAppTheme {
ThemeLazyColumn {
item {
ListHeader(id = commonR.string.shortcuts_choose)
ListHeader(id = commonR.string.choose_entity)
}
item {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
icon = { Image(asset = CommunityMaterial.Icon.cmd_delete) },
label = { Text(stringResource(id = commonR.string.none)) },
onClick = onNoneClicked,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
if (allowNone) {
item {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
icon = { Image(asset = CommunityMaterial.Icon.cmd_delete) },
label = { Text(stringResource(id = commonR.string.none)) },
onClick = onNoneClicked,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
)
}
}
for (domain in mainViewModel.entitiesByDomainOrder) {
val entities = mainViewModel.entitiesByDomain[domain]
for (domain in entitiesByDomainOrder) {
val entities = entitiesByDomain[domain]
if (!entities.isNullOrEmpty()) {
item {
ExpandableListHeader(
string = mainViewModel.stringForDomain(domain)!!,
string = stringForDomain(domain, LocalContext.current)
?: domain.replace('_', ' ').capitalize(Locale.getDefault()),
key = domain,
expandedStates = expandedStates
)
@ -91,7 +100,7 @@ private fun ChooseEntityChip(
.fillMaxWidth(),
icon = {
Image(
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone,
asset = iconBitmap ?: CommunityMaterial.Icon.cmd_bookmark,
colorFilter = ColorFilter.tint(Color.White)
)
},

View file

@ -1,4 +1,4 @@
package io.homeassistant.companion.android.home.views
package io.homeassistant.companion.android.views
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package io.homeassistant.companion.android.home.views
package io.homeassistant.companion.android.views
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package io.homeassistant.companion.android.home.views
package io.homeassistant.companion.android.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF03A9F4"
android:pathData="M12,2A7,7 0,0 0,5 9C5,11.38 6.19,13.47 8,14.74V17A1,1 0,0 0,9 18H15A1,1 0,0 0,16 17V14.74C17.81,13.47 19,11.38 19,9A7,7 0,0 0,12 2M9,21A1,1 0,0 0,10 22H14A1,1 0,0 0,15 21V20H9V21Z"/>
</vector>