Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import android.service.controls.templates.ThumbnailTemplate
import androidx.annotation.RequiresApi
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.integration.display.EntityDisplayWithContext
import io.homeassistant.companion.android.common.util.STATE_UNAVAILABLE
import java.net.URL
import java.util.concurrent.TimeUnit
Expand All @@ -28,13 +28,11 @@ object CameraControl : HaControl {
override fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity,
item: EntityDisplayWithContext,
info: HaControlInfo,
): Control.StatefulBuilder {
val image = if (info.baseUrl != null &&
(entity.attributes["entity_picture"] as? String)?.isNotBlank() == true
) {
getThumbnail(info.baseUrl + entity.attributes["entity_picture"] as String)
val image = if (info.baseUrl != null && item.entityPicturePath != null) {
getThumbnail(info.baseUrl + item.entityPicturePath)
} else {
null
}
Expand All @@ -45,21 +43,25 @@ object CameraControl : HaControl {
}
control.setControlTemplate(
ThumbnailTemplate(
entity.entityId,
entity.state != STATE_UNAVAILABLE && image != null,
item.entityId,
item.rawState != STATE_UNAVAILABLE && image != null,
icon,
context.getString(commonR.string.widget_camera_contentdescription),
),
)
return control
}

override fun getDeviceType(entity: Entity): Int = DeviceTypes.TYPE_CAMERA
override fun getDeviceType(item: EntityDisplayWithContext): Int = DeviceTypes.TYPE_CAMERA

override fun getDomainString(context: Context, entity: Entity): String =
override fun getDomainString(context: Context, item: EntityDisplayWithContext): String =
context.getString(commonR.string.domain_camera)

override suspend fun performAction(integrationRepository: IntegrationRepository, action: ControlAction): Boolean {
override suspend fun performAction(
integrationRepository: IntegrationRepository,
action: ControlAction,
serverId: Int,
): Boolean {
// No action is received, Android immediately invokes long press
return true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ 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.integration.display.EntityDisplayWithContext
import java.util.concurrent.ConcurrentHashMap

@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(
"cool" to TemperatureControlTemplate.MODE_COOL,
"heat" to TemperatureControlTemplate.MODE_HEAT,
Expand All @@ -34,19 +33,18 @@ object ClimateControl : HaControl {
"heat_cool" to TemperatureControlTemplate.FLAG_MODE_HEAT_COOL,
"off" to TemperatureControlTemplate.FLAG_MODE_OFF,
)
private val climateStates = HashMap<String, ClimateState>()
private val climateStates = ConcurrentHashMap<String, ClimateState>()

override fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity,
item: EntityDisplayWithContext,
info: HaControlInfo,
): Control.StatefulBuilder {
val minValue = (entity.attributes["min_temp"] as? Number)?.toFloat() ?: 0f
val maxValue = (entity.attributes["max_temp"] as? Number)?.toFloat() ?: 100f
var currentValue = (entity.attributes["temperature"] as? Number)?.toFloat() ?: (
entity.attributes["current_temperature"] as? Number
)?.toFloat() ?: 0f
val controls = item.climateControls
val minValue = controls?.minTemperature ?: 0f
val maxValue = controls?.maxTemperature ?: 100f
var currentValue = controls?.targetTemperature ?: controls?.currentTemperature ?: 0f
// Ensure the current value is never lower than the minimum or higher than the maximum
if (currentValue < minValue) {
currentValue = minValue
Expand All @@ -55,8 +53,8 @@ object ClimateControl : HaControl {
currentValue = maxValue
}

val temperatureUnit = entity.attributes["temperature_unit"] ?: ""
val temperatureStepSize = (entity.attributes["target_temp_step"] as? Number)?.toFloat()
val temperatureUnit = controls?.temperatureUnit ?: ""
val temperatureStepSize = controls?.targetTemperatureStep
?: when (temperatureUnit) {
"°C" -> 0.5f
else -> 1f
Expand All @@ -70,8 +68,8 @@ object ClimateControl : HaControl {
temperatureStepSize,
"%.${temperatureFormatSize}f $temperatureUnit",
)
if (entityShouldBePresentedAsThermostat(entity)) {
val state = ClimateState(entity.state, ArrayList())
if (shouldBePresentedAsThermostat(item)) {
val state = ClimateState(item.rawState, ArrayList())
val toggleRangeTemplate = ToggleRangeTemplate(
info.systemId + "_range",
// Set checked to true to always show the temperature indicator, regardless of climate mode
Expand All @@ -80,7 +78,7 @@ object ClimateControl : HaControl {
rangeTemplate,
)
var modesFlag = 0
(entity.attributes["hvac_modes"] as? List<String>)?.forEach {
controls?.hvacModes?.forEach {
modesFlag = modesFlag or temperatureControlModeFlags[it]!!
state.supportedModes.add(it)
}
Expand All @@ -89,8 +87,8 @@ object ClimateControl : HaControl {
TemperatureControlTemplate(
info.systemId,
toggleRangeTemplate,
temperatureControlModes[entity.state]!!,
temperatureControlModes[entity.state]!!,
temperatureControlModes[item.rawState]!!,
temperatureControlModes[item.rawState]!!,
modesFlag,
),
)
Expand All @@ -101,16 +99,20 @@ object ClimateControl : HaControl {
return control
}

override fun getDeviceType(entity: Entity): Int = if (entityShouldBePresentedAsThermostat(entity)) {
override fun getDeviceType(item: EntityDisplayWithContext): Int = if (shouldBePresentedAsThermostat(item)) {
DeviceTypes.TYPE_THERMOSTAT
} else {
DeviceTypes.TYPE_AC_HEATER
}

override fun getDomainString(context: Context, entity: Entity): String =
override fun getDomainString(context: Context, item: EntityDisplayWithContext): String =
context.getString(commonR.string.domain_climate)

override suspend fun performAction(integrationRepository: IntegrationRepository, action: ControlAction): Boolean {
override suspend fun performAction(
integrationRepository: IntegrationRepository,
action: ControlAction,
serverId: Int,
): Boolean {
val entityStr: String = if (action.templateId.split(".").size > 2) {
action.templateId.split(".", limit = 2)[1]
} else {
Expand All @@ -123,7 +125,7 @@ object ClimateControl : HaControl {
"set_temperature",
hashMapOf(
"entity_id" to entityStr,
"temperature" to (action as? FloatAction)?.newValue.toString(),
"temperature" to action.newValue.toString(),
),
)
true
Expand All @@ -136,7 +138,7 @@ object ClimateControl : HaControl {
"entity_id" to entityStr,
"hvac_mode" to (
temperatureControlModes.entries.find {
it.value == ((action as? ModeAction)?.newMode ?: -1)
it.value == action.newMode
}?.key ?: ""
),
),
Expand Down Expand Up @@ -166,21 +168,12 @@ object ClimateControl : HaControl {
}
}

private fun entityShouldBePresentedAsThermostat(entity: Entity): Boolean =
(entity.attributes["hvac_modes"] as? List<String>).let { modes ->
temperatureControlModes.containsKey(entity.state) &&
modes?.isNotEmpty() == true &&
modes.any { it == entity.state } &&
modes.all { temperatureControlModes.containsKey(it) } &&
(
(
(entity.attributes["supported_features"] as Int) and SUPPORT_TARGET_TEMPERATURE ==
SUPPORT_TARGET_TEMPERATURE
) ||
(
(entity.attributes["supported_features"] as Int) and SUPPORT_TARGET_TEMPERATURE_RANGE ==
SUPPORT_TARGET_TEMPERATURE_RANGE
)
)
}
private fun shouldBePresentedAsThermostat(item: EntityDisplayWithContext): Boolean {
val controls = item.climateControls ?: return false
return temperatureControlModes.containsKey(item.rawState) &&
controls.hvacModes.isNotEmpty() &&
controls.hvacModes.any { it == item.rawState } &&
controls.hvacModes.all { temperatureControlModes.containsKey(it) } &&
controls.supportsTargetTemperature
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,26 @@ import android.service.controls.templates.ToggleRangeTemplate
import android.service.controls.templates.ToggleTemplate
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.integration.getCoverPosition
import io.homeassistant.companion.android.common.data.integration.isActive
import io.homeassistant.companion.android.common.data.integration.display.EntityDisplayWithContext

@RequiresApi(Build.VERSION_CODES.R)
object CoverControl : HaControl {
private const val SUPPORT_SET_POSITION = 4
override fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity,
item: EntityDisplayWithContext,
info: HaControlInfo,
): Control.StatefulBuilder {
val position = entity.getCoverPosition()
val position = item.coverControls?.position
control.setControlTemplate(
if ((entity.attributes["supported_features"] as Int) and SUPPORT_SET_POSITION == SUPPORT_SET_POSITION) {
if (item.coverControls?.supportsSetPosition == true) {
ToggleRangeTemplate(
entity.entityId,
entity.isActive(),
item.entityId,
item.isActive,
"",
RangeTemplate(
entity.entityId,
item.entityId,
position?.min ?: 0f,
position?.max ?: 100f,
position?.value ?: 0f,
Expand All @@ -45,9 +42,9 @@ object CoverControl : HaControl {
)
} else {
ToggleTemplate(
entity.entityId,
item.entityId,
ControlButton(
entity.isActive(),
item.isActive,
"Description",
),
)
Expand All @@ -56,7 +53,7 @@ object CoverControl : HaControl {
return control
}

override fun getDeviceType(entity: Entity): Int = when (entity.attributes["device_class"]) {
override fun getDeviceType(item: EntityDisplayWithContext): Int = when (item.deviceClass) {
"awning" -> DeviceTypes.TYPE_AWNING
"blind" -> DeviceTypes.TYPE_BLINDS
"curtain" -> DeviceTypes.TYPE_CURTAIN
Expand All @@ -68,10 +65,14 @@ object CoverControl : HaControl {
else -> DeviceTypes.TYPE_GENERIC_OPEN_CLOSE
}

override fun getDomainString(context: Context, entity: Entity): String =
override fun getDomainString(context: Context, item: EntityDisplayWithContext): String =
context.getString(commonR.string.domain_cover)

override suspend fun performAction(integrationRepository: IntegrationRepository, action: ControlAction): Boolean {
override suspend fun performAction(
integrationRepository: IntegrationRepository,
action: ControlAction,
serverId: Int,
): Boolean {
return when (action) {
is BooleanAction -> {
integrationRepository.callAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import android.service.controls.actions.ControlAction
import android.service.controls.templates.StatelessTemplate
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.integration.display.EntityDisplayWithContext
import io.homeassistant.companion.android.common.util.capitalize
import java.util.Locale

Expand All @@ -18,32 +18,36 @@ object DefaultButtonControl : HaControl {
override fun provideControlFeatures(
context: Context,
control: Control.StatefulBuilder,
entity: Entity,
item: EntityDisplayWithContext,
info: HaControlInfo,
): Control.StatefulBuilder {
control.setStatusText("")
control.setControlTemplate(
StatelessTemplate(
entity.entityId,
item.entityId,
),
)
return control
}

override fun getDeviceType(entity: Entity): Int = when (entity.domain) {
override fun getDeviceType(item: EntityDisplayWithContext): Int = when (item.domain) {
"scene", "script" -> DeviceTypes.TYPE_ROUTINE
else -> DeviceTypes.TYPE_UNKNOWN
}

override fun getDomainString(context: Context, entity: Entity): String = when (entity.domain) {
override fun getDomainString(context: Context, item: EntityDisplayWithContext): String = when (item.domain) {
"button" -> context.getString(commonR.string.domain_button)
"input_button" -> context.getString(commonR.string.domain_input_button)
"scene" -> context.getString(commonR.string.domain_scene)
"script" -> context.getString(commonR.string.domain_script)
else -> entity.domain.capitalize(Locale.getDefault())
else -> item.domain.capitalize(Locale.getDefault())
}

override suspend fun performAction(integrationRepository: IntegrationRepository, action: ControlAction): Boolean {
override suspend fun performAction(
integrationRepository: IntegrationRepository,
action: ControlAction,
serverId: Int,
): Boolean {
integrationRepository.callAction(
action.templateId.split(".")[0],
when (action.templateId.split(".")[0]) {
Expand Down
Loading
Loading