diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/DomainChecks.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/DomainChecks.kt index bd97576ab05..1546eb65b59 100755 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/DomainChecks.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/vehicle/DomainChecks.kt @@ -2,7 +2,6 @@ package io.homeassistant.companion.android.util.vehicle import io.homeassistant.companion.android.common.R import io.homeassistant.companion.android.common.data.integration.Entity -import io.homeassistant.companion.android.common.data.integration.supportsAlarmControlPanelArmAway val SUPPORTED_DOMAINS_WITH_STRING = mapOf( "alarm_control_panel" to R.string.alarm_control_panels, @@ -45,9 +44,3 @@ fun canNavigate(entity: Entity): Boolean { ((entity.attributes["longitude"] as? Number)?.toDouble() != null) ) } - -fun alarmHasNoCode(entity: Entity): Boolean { - return entity.domain == "alarm_control_panel" && - entity.attributes["code_format"] as? String == null && - entity.supportsAlarmControlPanelArmAway() -} diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/EntityGridVehicleScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/EntityGridVehicleScreen.kt index ebd853b384a..b400ad7d2c0 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/EntityGridVehicleScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/vehicle/EntityGridVehicleScreen.kt @@ -27,6 +27,7 @@ import io.homeassistant.companion.android.common.data.integration.friendlyName import io.homeassistant.companion.android.common.data.integration.friendlyState import io.homeassistant.companion.android.common.data.integration.getIcon import io.homeassistant.companion.android.common.data.integration.isActive +import io.homeassistant.companion.android.common.data.integration.isAlarmActionable import io.homeassistant.companion.android.common.data.integration.isExecuting import io.homeassistant.companion.android.common.data.integration.onPressed import io.homeassistant.companion.android.common.data.prefs.PrefsRepository @@ -35,7 +36,6 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.En import io.homeassistant.companion.android.util.vehicle.MAP_DOMAINS import io.homeassistant.companion.android.util.vehicle.NOT_ACTIONABLE_DOMAINS import io.homeassistant.companion.android.util.vehicle.SUPPORTED_DOMAINS -import io.homeassistant.companion.android.util.vehicle.alarmHasNoCode import io.homeassistant.companion.android.util.vehicle.canNavigate import io.homeassistant.companion.android.util.vehicle.getChangeServerGridItem import io.homeassistant.companion.android.util.vehicle.getDomainList @@ -174,7 +174,7 @@ class EntityGridVehicleScreen( if (entity.isExecuting()) { gridItem.setLoading(entity.isExecuting()) } else { - if (entity.domain !in NOT_ACTIONABLE_DOMAINS || canNavigate(entity) || alarmHasNoCode(entity)) { + if (entity.domain !in NOT_ACTIONABLE_DOMAINS || canNavigate(entity) || entity.isAlarmActionable()) { gridItem .setOnClickListener { Timber.i("${entity.entityId} clicked") diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExt.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExt.kt new file mode 100644 index 00000000000..4b742427348 --- /dev/null +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExt.kt @@ -0,0 +1,74 @@ +package io.homeassistant.companion.android.common.data.integration + +import androidx.annotation.VisibleForTesting +import timber.log.Timber +import io.homeassistant.companion.android.common.data.integration.IntegrationDomains.ALARM_CONTROL_PANEL_DOMAIN + +@VisibleForTesting +internal const val ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY = 2 + +private fun Entity.isAlarmControlPanelEntity(): Boolean { + return domain == ALARM_CONTROL_PANEL_DOMAIN +} + +private fun Entity.alarmHasNoCode(): Boolean { + return isAlarmControlPanelEntity() && (attributes["code_format"] as? String)?.isNotEmpty() != true +} + +private fun Entity.alarmCanBeArmedWithoutCode(): Boolean { + return isAlarmControlPanelEntity() && attributes["code_arm_required"] as? Boolean == false +} + +private fun Entity.supportsAlarmControlPanelArmAway(): Boolean { + return try { + if (!isAlarmControlPanelEntity()) { + return false + } + + (attributes["supported_features"] as Number).toInt() and + ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY == ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY + } catch (e: Exception) { + Timber.tag(EntityExt.TAG).e(e, "Unable to get supportsArmedAway") + false + } +} + +private fun Entity.alarmIsDisarmed(): Boolean { + return isAlarmControlPanelEntity() && state == "disarmed" +} + +private fun Entity.alarmCanBeArmedAway(): Boolean { + if(!isAlarmControlPanelEntity()) { + return false + } + + if (!alarmIsDisarmed() || !supportsAlarmControlPanelArmAway()) { + return false + } + + return alarmHasNoCode() || alarmCanBeArmedWithoutCode() +} + +private fun Entity.alarmCanBeDisarmed(): Boolean { + return isAlarmControlPanelEntity() && !alarmIsDisarmed() && alarmHasNoCode() +} + +fun Entity.isAlarmActionable(): Boolean { + return isAlarmControlPanelEntity() && alarmCanBeDisarmed() || alarmCanBeArmedAway() +} + +fun Entity.getAlarmOnPressedAction(): String? { + if(!isAlarmControlPanelEntity()) { + return null + } + + if (alarmCanBeDisarmed()) { + return "alarm_disarm" + } + + if (alarmCanBeArmedAway()) { + return "alarm_arm_away" + } + + return null +} diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt index 7ac9880a686..eb6821d3732 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/Entity.kt @@ -55,7 +55,6 @@ object EntityExt { val LIGHT_MODE_NO_BRIGHTNESS_SUPPORT = listOf("unknown", "onoff") const val LIGHT_SUPPORT_BRIGHTNESS_DEPR = 1 const val LIGHT_SUPPORT_COLOR_TEMP_DEPR = 2 - const val ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY = 2 const val MEDIA_PLAYER_SUPPORT_VOLUME_SET = 4 val DOMAINS_PRESS = listOf("button", "input_button") @@ -170,17 +169,6 @@ fun Entity.getCoverPosition(): EntityPosition? { } } -fun Entity.supportsAlarmControlPanelArmAway(): Boolean { - return try { - if (domain != "alarm_control_panel") return false - (attributes["supported_features"] as Number).toInt() and - EntityExt.ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY == EntityExt.ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY - } catch (e: Exception) { - Timber.tag(EntityExt.TAG).e(e, "Unable to get supportsArmedAway") - false - } -} - fun Entity.supportsFanSetSpeed(): Boolean { return try { if (domain != "fan") return false @@ -808,9 +796,7 @@ suspend fun Entity.onPressed(integrationRepository: IntegrationRepository) { if (state == "unlocked") "lock" else "unlock" } - "alarm_control_panel" -> { - if (state != "disarmed") "alarm_disarm" else "alarm_arm_away" - } + "alarm_control_panel" -> getAlarmOnPressedAction() in EntityExt.DOMAINS_PRESS -> "press" "fan", @@ -825,6 +811,11 @@ suspend fun Entity.onPressed(integrationRepository: IntegrationRepository) { else -> "toggle" } + if (action == null) { + Timber.tag(EntityExt.TAG).w("No action called when entity '%s' was pressed", entityId) + return + } + integrationRepository.callAction( domain = this.domain, action = action, diff --git a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/IntegrationDomains.kt b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/IntegrationDomains.kt index af1ea8fdce1..9d220280814 100644 --- a/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/IntegrationDomains.kt +++ b/common/src/main/kotlin/io/homeassistant/companion/android/common/data/integration/IntegrationDomains.kt @@ -5,4 +5,6 @@ object IntegrationDomains { const val MEDIA_PLAYER_DOMAIN = "media_player" const val IMAGE_DOMAIN = "image" const val TODO_DOMAIN = "todo" + + const val ALARM_CONTROL_PANEL_DOMAIN = "alarm_control_panel" } diff --git a/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExtTest.kt b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExtTest.kt new file mode 100644 index 00000000000..788c9a1a5ed --- /dev/null +++ b/common/src/test/kotlin/io/homeassistant/companion/android/common/data/integration/AlarmControlPanelEntityExtTest.kt @@ -0,0 +1,75 @@ +package io.homeassistant.companion.android.common.data.integration + +import java.time.LocalDateTime +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue + +class AlarmControlPanelEntityExtTest { + + @Test + fun `Given disarmed alarm without code supporting arm_away When pressed Then alarm is actionable and action is alarm_arm_away`() { + val alarmEntity = createAlarmEntity("", requiredArmCode = false, supportArmAway = true, isArmed = false) + + assertTrue(alarmEntity.isAlarmActionable()) + assertEquals("alarm_arm_away", alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given disarmed alarm with not required arm code supporting arm_away When pressed Then alarm is actionable and action is alarm_arm_away`() { + val alarmEntity = createAlarmEntity("A_C0DE", requiredArmCode = false, supportArmAway = true, isArmed = false) + + assertTrue(alarmEntity.isAlarmActionable()) + assertEquals("alarm_arm_away", alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given armed alarm without code When pressed Then alarm is actionable and action is alarm_disarm`() { + val alarmEntity = createAlarmEntity("", requiredArmCode = false, supportArmAway = false, isArmed = true) + + assertTrue(alarmEntity.isAlarmActionable()) + assertEquals("alarm_disarm", alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given disarmed alarm without code not supporting arm_away When pressed Then alarm is not actionable and action is null`() { + val alarmEntity = createAlarmEntity("", requiredArmCode = false, supportArmAway = false, isArmed = false) + + assertFalse(alarmEntity.isAlarmActionable()) + assertEquals(null, alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given disarmed alarm with required arm code supporting arm_away When pressed Then alarm is not actionable and action is null`() { + val alarmEntity = createAlarmEntity("A_C0DE", requiredArmCode = true, supportArmAway = true, isArmed = false) + + assertFalse(alarmEntity.isAlarmActionable()) + assertEquals(null, alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given armed alarm with code When pressed Then alarm is not actionable and action is null`() { + val alarmEntity = createAlarmEntity("A_C0DE", requiredArmCode = false, supportArmAway = true, isArmed = true) + + assertFalse(alarmEntity.isAlarmActionable()) + assertEquals(null, alarmEntity.getAlarmOnPressedAction()) + } + + @Test + fun `Given not an alarm entity When pressed Then alarm is not actionable and action is null`() { + val otherEntity = Entity("other_domain.an_entity_id", "", mapOf(), LocalDateTime.now(), LocalDateTime.now()) + assertFalse(otherEntity.isAlarmActionable()) + assertEquals(null, otherEntity.getAlarmOnPressedAction()) + } + + private fun createAlarmEntity(code: String, requiredArmCode: Boolean, supportArmAway: Boolean, isArmed: Boolean): Entity { + val state = if (isArmed) "armed_away" else "disarmed" + + val attributes = mutableMapOf() + attributes["code_format"] = if (code.isEmpty()) null else "text" + attributes["code_arm_required"] = if (code.isEmpty()) false else requiredArmCode + attributes["supported_features"] = if (supportArmAway) ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY + 4 else 4 + return Entity("alarm_control_panel.an_alarm_id", state, attributes, LocalDateTime.now(), LocalDateTime.now()) + } +}