Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New thermostat tile for Wear OS #4959

Merged
merged 58 commits into from
Feb 8, 2025
Merged
Changes from 7 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
113d10e
Add thermostat tile to Wear OS app.
Martreides Jan 5, 2025
e80d61b
Add database schema.
Martreides Jan 5, 2025
d4bfe9b
Remove debug logging.
Martreides Jan 5, 2025
b06a4cc
Merge branch 'master' into develop-thermostat-tile
Martreides Jan 5, 2025
f114b3a
Minor changes for ktlint.
Martreides Jan 5, 2025
a416b6a
More minor changes for ktlint.
Martreides Jan 6, 2025
96c6381
Changed import order in AppDatabase.
Martreides Jan 6, 2025
f7fc5d8
Changed layout of the tile. Now includes the state (Idle/Heating/Cool…
Martreides Jan 6, 2025
87512f0
Add temperature unit to the tile.
Martreides Jan 7, 2025
ea07379
Add handling of "Off" state.
Martreides Jan 7, 2025
b1164ac
Add setting to toggle showing of name on tile.
Martreides Jan 7, 2025
67c053f
Aligned name on tile setting of the thermostat tile to the shortcuts …
Martreides Jan 8, 2025
be58175
Revert back to SwitchButton for name on tile.
Martreides Jan 8, 2025
e94bbfa
Change preview to realistic image.
Martreides Jan 8, 2025
52493ce
Changed retrieving of targetTemperature.
Martreides Jan 8, 2025
728e6c5
Changed retrieving of friendlyName.
Martreides Jan 8, 2025
a4264da
Change location of retrieving information only needed when the tile i…
Martreides Jan 8, 2025
31467e1
Changed format of temperature when entity is off.
Martreides Jan 8, 2025
4cdc938
Use friendlyState for the state shown on the tile.
Martreides Jan 8, 2025
d84ce37
Use constant for temp up and down action, and combine getTempUpButton…
Martreides Jan 8, 2025
9039b70
Make hvac_action translatable.
Martreides Jan 8, 2025
e3170ac
Add missing import.
Martreides Jan 8, 2025
ea691a8
Update layout of the tile.
Martreides Jan 8, 2025
d4b474b
Compressed preview image.
Martreides Jan 9, 2025
682497c
Move hapticClick up in the code to prevent delay.
Martreides Jan 9, 2025
c644513
Disable buttons if entity is off.
Martreides Jan 9, 2025
e395ce5
Add graceful handling of not being able to fetch entity.
Martreides Jan 10, 2025
01215ad
Merge branch 'master' into develop-thermostat-tile
Martreides Jan 10, 2025
bebbd5f
Fixed "select entity" message not being shown.
Martreides Jan 11, 2025
db9bfe0
use state strings for thermostat tile.
Martreides Jan 13, 2025
7d65b20
Update preview for the select thermostat view.
Martreides Jan 13, 2025
09d9905
Change icon to domain default.
Martreides Jan 13, 2025
273e42b
Use TAP_ACTION_UP in setting the updated temperature.
Martreides Jan 13, 2025
4b20946
Capitalize hvacAction.
Martreides Jan 13, 2025
7e8a7aa
Wrap code to show entity name on tile in if statement.
Martreides Jan 13, 2025
7d0f284
Move logged in check up in the code.
Martreides Jan 13, 2025
1469042
Move logged in check up in the code - Fix.
Martreides Jan 13, 2025
64ca70e
Remove unnecessary safe calls.
Martreides Jan 17, 2025
35d6fd5
Pass entity to timeline function instead of retrieving it again.
Martreides Jan 22, 2025
cead805
Handle situation where attribute stepSize is not set.
Martreides Jan 22, 2025
8cc2ddd
Fix indentation.
Martreides Jan 22, 2025
73ad565
Fix empty line.
Martreides Jan 22, 2025
6199198
Changed handling of off/unavailable state
Martreides Jan 22, 2025
f08fe68
Indentation.
Martreides Jan 22, 2025
c249c08
Updated example image.
Martreides Jan 22, 2025
f1a3ed7
Change logic to get entity domain.
Martreides Feb 4, 2025
313f878
Direct use of entity.entityId.
Martreides Feb 4, 2025
7803458
Clean up updatedTargetTemp.
Martreides Feb 4, 2025
3d0e3e9
Change logic for unavailable entity.
Martreides Feb 4, 2025
d2a2446
Remove empty line.
Martreides Feb 4, 2025
6a5d959
Merge branch 'master' into develop-thermostat-tile
Martreides Feb 4, 2025
f475379
Remove suspend from timeline functoin declaration.
Martreides Feb 7, 2025
f6ab32d
Improve imports.
Martreides Feb 7, 2025
140f921
Merge branch 'master' into develop-thermostat-tile
Martreides Feb 7, 2025
c464e74
Change order of imports.
Martreides Feb 7, 2025
4d701c7
Change order of imports, again.
Martreides Feb 7, 2025
e170b6f
Change order of imports, again.
Martreides Feb 7, 2025
9b7f075
Another import change.
Martreides Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,177 changes: 1,177 additions & 0 deletions common/schemas/io.homeassistant.companion.android.database.AppDatabase/49.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -61,6 +61,8 @@ import io.homeassistant.companion.android.database.wear.FavoriteCaches
import io.homeassistant.companion.android.database.wear.FavoriteCachesDao
import io.homeassistant.companion.android.database.wear.Favorites
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.wear.ThermostatTile
import io.homeassistant.companion.android.database.wear.ThermostatTileDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetEntity
import io.homeassistant.companion.android.database.widget.CameraWidgetDao
@@ -93,11 +95,12 @@ import kotlinx.coroutines.runBlocking
Favorites::class,
FavoriteCaches::class,
CameraTile::class,
ThermostatTile::class,
EntityStateComplications::class,
Server::class,
Setting::class
],
version = 48,
version = 49,
autoMigrations = [
AutoMigration(from = 24, to = 25),
AutoMigration(from = 25, to = 26),
@@ -121,7 +124,8 @@ import kotlinx.coroutines.runBlocking
AutoMigration(from = 44, to = 45),
AutoMigration(from = 45, to = 46),
AutoMigration(from = 46, to = 47),
AutoMigration(from = 47, to = 48)
AutoMigration(from = 47, to = 48),
AutoMigration(from = 48, to = 49)
]
)
@TypeConverters(
@@ -146,6 +150,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun favoritesDao(): FavoritesDao
abstract fun favoriteCachesDao(): FavoriteCachesDao
abstract fun cameraTileDao(): CameraTileDao
abstract fun thermostatTileDao(): ThermostatTileDao
abstract fun entityStateComplicationsDao(): EntityStateComplicationsDao
abstract fun serverDao(): ServerDao
abstract fun settingsDao(): SettingsDao
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import io.homeassistant.companion.android.database.wear.CameraTileDao
import io.homeassistant.companion.android.database.wear.EntityStateComplicationsDao
import io.homeassistant.companion.android.database.wear.FavoriteCachesDao
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.wear.ThermostatTileDao
import io.homeassistant.companion.android.database.widget.ButtonWidgetDao
import io.homeassistant.companion.android.database.widget.CameraWidgetDao
import io.homeassistant.companion.android.database.widget.MediaPlayerControlsWidgetDao
@@ -80,6 +81,9 @@ object DatabaseModule {
@Provides
fun provideCameraTileDao(database: AppDatabase): CameraTileDao = database.cameraTileDao()

@Provides
fun provideThermostatTileDao(database: AppDatabase): ThermostatTileDao = database.thermostatTileDao()

@Provides
fun provideEntityStateComplicationsDao(database: AppDatabase): EntityStateComplicationsDao = database.entityStateComplicationsDao()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.homeassistant.companion.android.database.wear

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

/**
* Represents the configuration of a thermostat tile.
* If the tile was added but not configured, everything except the tile ID will be `null`.
*/
@Entity(tableName = "thermostat_tiles")
data class ThermostatTile(
/** The system's tile ID */
@PrimaryKey
@ColumnInfo(name = "id")
val id: Int,
/** The climate entity ID */
@ColumnInfo(name = "entity_id")
val entityId: String? = null,
/** The refresh interval of this tile, in seconds */
@ColumnInfo(name = "refresh_interval")
val refreshInterval: Long? = null,
/** The target temperature to allow quick repeated changes */
@ColumnInfo(name = "target_temperature")
val targetTemperature: Float? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.homeassistant.companion.android.database.wear

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface ThermostatTileDao {

@Query("SELECT * FROM thermostat_tiles WHERE id = :id")
suspend fun get(id: Int): ThermostatTile?

@Query("SELECT * FROM thermostat_tiles ORDER BY id ASC")
fun getAllFlow(): Flow<List<ThermostatTile>>

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun add(tile: ThermostatTile)

@Query("DELETE FROM thermostat_tiles where id = :id")
fun delete(id: Int)
}
8 changes: 8 additions & 0 deletions common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1355,4 +1355,12 @@
<string name="domain_remote">Remote</string>
<string name="domain_siren">Siren</string>
<string name="domain_humidifier">Humidifier</string>
<string name="thermostat_tiles">Thermostat tiles</string>
<string name="thermostat">Thermostat</string>
<string name="thermostat_tile_desc">See and change the thermostat temperature.</string>
<string name="thermostat_tile_no_tiles_yet">There are no thermostat tiles added yet - add one from the watch face to set it up</string>
<string name="thermostat_tile_no_entity_yet">Edit the tile settings and select a thermostat to show</string>
<string name="thermostat_tile_n">Thermostat tile #%d</string>
<string name="thermostat_tile_log_in">Log in to add a thermostat tile</string>
<string name="thermostat_tile">Thermostat tile</string>
</resources>
26 changes: 26 additions & 0 deletions wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -104,6 +104,11 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.google.android.clockwork.tiles.category.PROVIDER_CONFIG" />
</intent-filter>
<intent-filter>
<action android:name="ConfigThermostatTile" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.google.android.clockwork.tiles.category.PROVIDER_CONFIG" />
</intent-filter>
</activity>

<!-- To show confirmations and failures -->
@@ -209,6 +214,27 @@
android:name="com.google.android.clockwork.tiles.PROVIDER_CONFIG_ACTION"
android:value="ConfigCameraTile" />
</service>
<service
android:name=".tiles.ThermostatTile"
android:label="@string/thermostat"
android:description="@string/thermostat_tile_desc"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER"
android:exported="true">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>

<meta-data android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/thermostat_tile_example" />

<meta-data
android:name="com.google.android.clockwork.tiles.MULTI_INSTANCES_SUPPORTED"
android:value="true" /> <!-- This is supported starting from Wear OS 3 -->

<meta-data
android:name="com.google.android.clockwork.tiles.PROVIDER_CONFIG_ACTION"
android:value="ConfigThermostatTile" />
</service>
<receiver android:name=".tiles.TileActionReceiver"
android:exported="false">
<intent-filter>
Original file line number Diff line number Diff line change
@@ -33,6 +33,8 @@ import io.homeassistant.companion.android.database.wear.CameraTileDao
import io.homeassistant.companion.android.database.wear.FavoriteCaches
import io.homeassistant.companion.android.database.wear.FavoriteCachesDao
import io.homeassistant.companion.android.database.wear.FavoritesDao
import io.homeassistant.companion.android.database.wear.ThermostatTile
import io.homeassistant.companion.android.database.wear.ThermostatTileDao
import io.homeassistant.companion.android.database.wear.getAll
import io.homeassistant.companion.android.database.wear.getAllFlow
import io.homeassistant.companion.android.sensors.SensorReceiver
@@ -53,6 +55,7 @@ class MainViewModel @Inject constructor(
private val favoriteCachesDao: FavoriteCachesDao,
private val sensorsDao: SensorDao,
private val cameraTileDao: CameraTileDao,
private val thermostatTileDao: ThermostatTileDao,
application: Application
) : AndroidViewModel(application) {

@@ -99,6 +102,10 @@ class MainViewModel @Inject constructor(
var cameraEntitiesMap = mutableStateMapOf<String, SnapshotStateList<Entity<*>>>()
private set

val thermostatTiles = thermostatTileDao.getAllFlow().collectAsState()
var climateEntitiesMap = mutableStateMapOf<String, SnapshotStateList<Entity<*>>>()
private set

var areas = mutableListOf<AreaRegistryResponse>()
private set

@@ -242,9 +249,11 @@ class MainViewModel @Inject constructor(
entities.clear()
it.forEach { state -> updateEntityStates(state) }

// Special list: camera entities
// Special lists: camera entities and climate entities
val cameraEntities = it.filter { entity -> entity.domain == "camera" }
cameraEntitiesMap["camera"] = mutableStateListOf<Entity<*>>().apply { addAll(cameraEntities) }
val climateEntities = it.filter { entity -> entity.domain == "climate" }
climateEntitiesMap["climate"] = mutableStateListOf<Entity<*>>().apply { addAll(climateEntities) }
}
if (!isFavoritesOnly) {
updateEntityDomains()
@@ -448,6 +457,18 @@ class MainViewModel @Inject constructor(
cameraTileDao.add(updated)
}

fun setThermostatTileEntity(tileId: Int, entityId: String) = viewModelScope.launch {
val current = thermostatTileDao.get(tileId)
val updated = current?.copy(entityId = entityId) ?: ThermostatTile(id = tileId, entityId = entityId)
thermostatTileDao.add(updated)
}

fun setThermostatTileRefreshInterval(tileId: Int, interval: Long) = viewModelScope.launch {
val current = thermostatTileDao.get(tileId)
val updated = current?.copy(refreshInterval = interval) ?: ThermostatTile(id = tileId, refreshInterval = interval)
thermostatTileDao.add(updated)
}

fun setTileShortcut(tileId: Int?, index: Int, entity: SimplifiedEntity) {
viewModelScope.launch {
val shortcutEntities = shortcutEntitiesMap[tileId]!!
Original file line number Diff line number Diff line change
@@ -22,10 +22,12 @@ import io.homeassistant.companion.android.theme.WearAppTheme
import io.homeassistant.companion.android.tiles.CameraTile
import io.homeassistant.companion.android.tiles.ShortcutsTile
import io.homeassistant.companion.android.tiles.TemplateTile
import io.homeassistant.companion.android.tiles.ThermostatTile
import io.homeassistant.companion.android.views.ChooseEntityView

private const val ARG_SCREEN_SENSOR_MANAGER_ID = "sensorManagerId"
private const val ARG_SCREEN_CAMERA_TILE_ID = "cameraTileId"
private const val ARG_SCREEN_THERMOSTAT_TILE_ID = "thermostatTileId"
private const val ARG_SCREEN_SHORTCUTS_TILE_ID = "shortcutsTileId"
private const val ARG_SCREEN_SHORTCUTS_TILE_ENTITY_INDEX = "shortcutsTileEntityIndex"
private const val ARG_SCREEN_TEMPLATE_TILE_ID = "templateTileId"
@@ -42,6 +44,11 @@ private const val SCREEN_SELECT_CAMERA_TILE = "select_camera_tile"
private const val SCREEN_SET_CAMERA_TILE = "set_camera_tile"
private const val SCREEN_SET_CAMERA_TILE_ENTITY = "entity"
private const val SCREEN_SET_CAMERA_TILE_REFRESH_INTERVAL = "refresh_interval"
private const val ROUTE_THERMOSTAT_TILE = "thermostat_tile"
private const val SCREEN_SELECT_THERMOSTAT_TILE = "select_thermostat_tile"
private const val SCREEN_SET_THERMOSTAT_TILE = "set_thermostat_tile"
private const val SCREEN_SET_THERMOSTAT_TILE_ENTITY = "entity"
private const val SCREEN_SET_THERMOSTAT_TILE_REFRESH_INTERVAL = "refresh_interval"
private const val ROUTE_SHORTCUTS_TILE = "shortcuts_tile"
private const val ROUTE_TEMPLATE_TILE = "template_tile"
private const val SCREEN_SELECT_SHORTCUTS_TILE = "select_shortcuts_tile"
@@ -53,6 +60,7 @@ private const val SCREEN_SET_TILE_TEMPLATE_REFRESH_INTERVAL = "set_tile_template

const val DEEPLINK_SENSOR_MANAGER = "ha_wear://$SCREEN_SINGLE_SENSOR_MANAGER"
const val DEEPLINK_PREFIX_SET_CAMERA_TILE = "ha_wear://$SCREEN_SET_CAMERA_TILE"
const val DEEPLINK_PREFIX_SET_THERMOSTAT_TILE = "ha_wear://$SCREEN_SET_THERMOSTAT_TILE"
const val DEEPLINK_PREFIX_SET_SHORTCUT_TILE = "ha_wear://$SCREEN_SET_SHORTCUTS_TILE"
const val DEEPLINK_PREFIX_SET_TEMPLATE_TILE = "ha_wear://$SCREEN_SET_TILE_TEMPLATE"

@@ -177,6 +185,9 @@ fun LoadHomePage(
mainViewModel.loadTemplateTiles()
swipeDismissableNavController.navigate("$ROUTE_TEMPLATE_TILE/$SCREEN_SELECT_TEMPLATE_TILE")
},
onClickThermostatTiles = {
swipeDismissableNavController.navigate("$ROUTE_THERMOSTAT_TILE/$SCREEN_SELECT_THERMOSTAT_TILE")
},
onAssistantAppAllowed = mainViewModel::setAssistantApp,
onClickNotifications = {
notificationLaunch.launch(
@@ -278,6 +289,85 @@ fun LoadHomePage(
swipeDismissableNavController.navigateUp()
}
}
composable("$ROUTE_THERMOSTAT_TILE/$SCREEN_SELECT_THERMOSTAT_TILE") {
SelectThermostatTileView(
tiles = mainViewModel.thermostatTiles.value,
onSelectTile = { tileId ->
swipeDismissableNavController.navigate("$ROUTE_THERMOSTAT_TILE/$tileId/$SCREEN_SET_THERMOSTAT_TILE")
}
)
}
composable(
route = "$ROUTE_THERMOSTAT_TILE/{$ARG_SCREEN_THERMOSTAT_TILE_ID}/$SCREEN_SET_THERMOSTAT_TILE",
arguments = listOf(
navArgument(name = ARG_SCREEN_THERMOSTAT_TILE_ID) {
type = NavType.IntType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_PREFIX_SET_THERMOSTAT_TILE/{$ARG_SCREEN_THERMOSTAT_TILE_ID}" }
)
) { backStackEntry ->
val tileId = backStackEntry.arguments?.getInt(ARG_SCREEN_THERMOSTAT_TILE_ID)
SetThermostatTileView(
tile = mainViewModel.thermostatTiles.value.firstOrNull { it.id == tileId },
entities = mainViewModel.climateEntitiesMap["climate"],
onSelectEntity = {
swipeDismissableNavController.navigate("$ROUTE_THERMOSTAT_TILE/$tileId/$SCREEN_SET_THERMOSTAT_TILE_ENTITY")
},
onSelectRefreshInterval = {
swipeDismissableNavController.navigate("$ROUTE_THERMOSTAT_TILE/$tileId/$SCREEN_SET_CAMERA_TILE_REFRESH_INTERVAL")
}
)
}
composable(
route = "$ROUTE_THERMOSTAT_TILE/{$ARG_SCREEN_THERMOSTAT_TILE_ID}/$SCREEN_SET_THERMOSTAT_TILE_ENTITY",
arguments = listOf(
navArgument(name = ARG_SCREEN_THERMOSTAT_TILE_ID) {
type = NavType.IntType
}
)
) { backStackEntry ->
val tileId = backStackEntry.arguments?.getInt(ARG_SCREEN_THERMOSTAT_TILE_ID)
val climateDomains = remember { mutableStateListOf("climate") }
val climateFavorites = remember { mutableStateOf(emptyList<String>()) } // There are no climate favorites
ChooseEntityView(
entitiesByDomainOrder = climateDomains,
entitiesByDomain = mainViewModel.climateEntitiesMap,
favoriteEntityIds = climateFavorites,
onNoneClicked = {},
onEntitySelected = { entity ->
tileId?.let {
mainViewModel.setThermostatTileEntity(it, entity.entityId)
TileService.getUpdater(context).requestUpdate(ThermostatTile::class.java)
}
swipeDismissableNavController.navigateUp()
},
allowNone = false
)
}
composable(
route = "$ROUTE_THERMOSTAT_TILE/{$ARG_SCREEN_THERMOSTAT_TILE_ID}/$SCREEN_SET_THERMOSTAT_TILE_REFRESH_INTERVAL",
arguments = listOf(
navArgument(name = ARG_SCREEN_THERMOSTAT_TILE_ID) {
type = NavType.IntType
}
)
) { backStackEntry ->
val tileId = backStackEntry.arguments?.getInt(ARG_SCREEN_THERMOSTAT_TILE_ID)
RefreshIntervalPickerView(
currentInterval = (
mainViewModel.thermostatTiles.value
.firstOrNull { it.id == tileId }?.refreshInterval
?: ThermostatTile.DEFAULT_REFRESH_INTERVAL
).toInt()
) { interval ->
tileId?.let {
mainViewModel.setThermostatTileRefreshInterval(it, interval.toLong())
}
swipeDismissableNavController.navigateUp()
}
}
composable("$ROUTE_SHORTCUTS_TILE/$SCREEN_SELECT_SHORTCUTS_TILE") {
SelectShortcutsTileView(
shortcutTileEntitiesCountById = mainViewModel.shortcutEntitiesMap.mapValues { (_, entities) -> entities.size },
Loading