Add ability for ClimateControl Card to cycle through entity states (#4142)

* Add ability for ClimateControl Card to cycle through entity states

* Fix ktlint issue

* Set `currentMode` after sending request for cycling through modes

* Display thermostat always as "on"

* Use "toggle" string from widget_tap_action_toggle

* Use hashMap to save states of entities as we're in a singleton

* Include support for colliding entityIds when using multi server within ClimateControl

* Add comment why ToggleRangeTemplate is always set to checked
This commit is contained in:
StopMotionCuber 2024-02-22 23:06:43 +01:00 committed by GitHub
parent f006127b01
commit 45e0ca9caa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 58 additions and 45 deletions

View File

@ -15,7 +15,6 @@ import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.R as commonR
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.websocket.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.util.STATE_UNAVAILABLE
import java.net.URL
import java.util.concurrent.TimeUnit
@ -32,11 +31,10 @@ object CameraControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
val image = if (baseUrl != null && (entity.attributes["entity_picture"] as? String)?.isNotBlank() == true) {
getThumbnail(baseUrl + entity.attributes["entity_picture"] as String)
val image = if (info.baseUrl != null && (entity.attributes["entity_picture"] as? String)?.isNotBlank() == true) {
getThumbnail(info.baseUrl + entity.attributes["entity_picture"] as String)
} else {
null
}

View File

@ -4,19 +4,22 @@ import android.content.Context
import android.os.Build
import android.service.controls.Control
import android.service.controls.DeviceTypes
import android.service.controls.actions.BooleanAction
import android.service.controls.actions.ControlAction
import android.service.controls.actions.FloatAction
import android.service.controls.actions.ModeAction
import android.service.controls.templates.RangeTemplate
import android.service.controls.templates.TemperatureControlTemplate
import android.service.controls.templates.ToggleRangeTemplate
import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.common.R as commonR
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.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object ClimateControl : HaControl {
private data class ClimateState(val currentMode: String, val supportedModes: ArrayList<String>)
private const val SUPPORT_TARGET_TEMPERATURE = 1
private const val SUPPORT_TARGET_TEMPERATURE_RANGE = 2
private val temperatureControlModes = mapOf(
@ -31,13 +34,13 @@ object ClimateControl : HaControl {
"heat_cool" to TemperatureControlTemplate.FLAG_MODE_HEAT_COOL,
"off" to TemperatureControlTemplate.FLAG_MODE_OFF
)
private val climateStates = HashMap<String, ClimateState>()
override fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
val minValue = (entity.attributes["min_temp"] as? Number)?.toFloat() ?: 0f
val maxValue = (entity.attributes["max_temp"] as? Number)?.toFloat() ?: 100f
@ -60,7 +63,7 @@ object ClimateControl : HaControl {
}
val temperatureFormatSize = if (temperatureStepSize < 1f) "1" else "0"
val rangeTemplate = RangeTemplate(
entity.entityId,
info.systemId,
minValue,
maxValue,
currentValue,
@ -68,14 +71,24 @@ object ClimateControl : HaControl {
"%.${temperatureFormatSize}f $temperatureUnit"
)
if (entityShouldBePresentedAsThermostat(entity)) {
val state = ClimateState(entity.state, ArrayList())
val toggleRangeTemplate = ToggleRangeTemplate(
info.systemId + "_range",
// Set checked to true to always show the temperature indicator, regardless of climate mode
true,
context.getString(commonR.string.widget_tap_action_toggle),
rangeTemplate
)
var modesFlag = 0
(entity.attributes["hvac_modes"] as? List<String>)?.forEach {
modesFlag = modesFlag or temperatureControlModeFlags[it]!!
state.supportedModes.add(it)
}
this.climateStates[info.systemId] = state
control.setControlTemplate(
TemperatureControlTemplate(
entity.entityId,
rangeTemplate,
info.systemId,
toggleRangeTemplate,
temperatureControlModes[entity.state]!!,
temperatureControlModes[entity.state]!!,
modesFlag
@ -102,13 +115,18 @@ object ClimateControl : HaControl {
integrationRepository: IntegrationRepository,
action: ControlAction
): Boolean {
val entityStr: String = if (action.templateId.split(".").size > 2) {
action.templateId.split(".", limit = 2)[1]
} else {
action.templateId
}
return when (action) {
is FloatAction -> {
integrationRepository.callService(
action.templateId.split(".")[0],
entityStr.split(".")[0],
"set_temperature",
hashMapOf(
"entity_id" to action.templateId,
"entity_id" to entityStr,
"temperature" to (action as? FloatAction)?.newValue.toString()
)
)
@ -119,7 +137,7 @@ object ClimateControl : HaControl {
action.templateId.split(".")[0],
"set_hvac_mode",
hashMapOf(
"entity_id" to action.templateId,
"entity_id" to entityStr,
"hvac_mode" to (
temperatureControlModes.entries.find {
it.value == ((action as? ModeAction)?.newMode ?: -1)
@ -129,6 +147,23 @@ object ClimateControl : HaControl {
)
true
}
is BooleanAction -> {
if (this.climateStates[action.templateId] == null) {
return false
}
val supportedModes = this.climateStates[action.templateId]!!.supportedModes
val currentMode = this.climateStates[action.templateId]!!.currentMode
val nextMode = (supportedModes.indexOf(currentMode) + 1) % supportedModes.count()
integrationRepository.callService(
entityStr.split(".")[0],
"set_hvac_mode",
hashMapOf(
"entity_id" to entityStr,
"hvac_mode" to supportedModes[nextMode]
)
)
true
}
else -> {
false
}

View File

@ -17,7 +17,6 @@ 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.getCoverPosition
import io.homeassistant.companion.android.common.data.integration.isActive
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object CoverControl : HaControl {
@ -26,8 +25,7 @@ object CoverControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
val position = entity.getCoverPosition()
control.setControlTemplate(

View File

@ -11,7 +11,6 @@ import io.homeassistant.companion.android.common.R as commonR
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.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.util.capitalize
import java.util.Locale
@ -21,8 +20,7 @@ object DefaultButtonControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
control.setStatusText("")
control.setControlTemplate(

View File

@ -11,7 +11,6 @@ import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.common.R as commonR
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.websocket.impl.entities.AreaRegistryResponse
import kotlinx.coroutines.runBlocking
@RequiresApi(Build.VERSION_CODES.R)
@ -20,8 +19,7 @@ object DefaultSliderControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
control.setStatusText("")
control.setControlTemplate(

View File

@ -14,7 +14,6 @@ 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.integration.isActive
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.util.capitalize
import java.util.Locale
@ -24,8 +23,7 @@ object DefaultSwitchControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
control.setControlTemplate(
ToggleTemplate(

View File

@ -18,7 +18,6 @@ import io.homeassistant.companion.android.common.data.integration.IntegrationRep
import io.homeassistant.companion.android.common.data.integration.getFanSpeed
import io.homeassistant.companion.android.common.data.integration.isActive
import io.homeassistant.companion.android.common.data.integration.supportsFanSetSpeed
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object FanControl : HaControl {
@ -26,8 +25,7 @@ object FanControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
if (entity.supportsFanSetSpeed()) {
val position = entity.getFanSpeed()

View File

@ -17,7 +17,6 @@ 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.integration.friendlyState
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.webview.WebViewActivity
@RequiresApi(Build.VERSION_CODES.R)
@ -77,15 +76,14 @@ interface HaControl {
}
}
return provideControlFeatures(context, control, entity, info.area, info.baseUrl).build()
return provideControlFeatures(context, control, entity, info).build()
}
fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder
fun getDeviceType(entity: Entity<Map<String, Any>>): Int

View File

@ -10,7 +10,6 @@ import androidx.annotation.RequiresApi
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.impl.entities.AreaRegistryResponse
import io.homeassistant.companion.android.common.util.capitalize
import java.util.Locale
@ -20,8 +19,7 @@ object HaFailedControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
control.setStatus(if (entity.state == "notfound") Control.STATUS_NOT_FOUND else Control.STATUS_ERROR)
control.setStatusText("")

View File

@ -18,7 +18,6 @@ import io.homeassistant.companion.android.common.data.integration.IntegrationRep
import io.homeassistant.companion.android.common.data.integration.getLightBrightness
import io.homeassistant.companion.android.common.data.integration.isActive
import io.homeassistant.companion.android.common.data.integration.supportsLightBrightness
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object LightControl : HaControl {
@ -26,8 +25,7 @@ object LightControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
val position = entity.getLightBrightness()
control.setControlTemplate(

View File

@ -13,7 +13,6 @@ import io.homeassistant.companion.android.common.R as commonR
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.isActive
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object LockControl : HaControl {
@ -21,8 +20,7 @@ object LockControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
control.setControlTemplate(
ToggleTemplate(

View File

@ -13,7 +13,6 @@ import io.homeassistant.companion.android.common.R as commonR
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.isActive
import io.homeassistant.companion.android.common.data.websocket.impl.entities.AreaRegistryResponse
@RequiresApi(Build.VERSION_CODES.R)
object VacuumControl : HaControl {
@ -24,8 +23,7 @@ object VacuumControl : HaControl {
context: Context,
control: Control.StatefulBuilder,
entity: Entity<Map<String, Any>>,
area: AreaRegistryResponse?,
baseUrl: String?
info: HaControlInfo
): Control.StatefulBuilder {
entitySupportedFeatures = entity.attributes["supported_features"] as Int
control.setControlTemplate(