diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/ClimateControl.kt b/app/src/main/java/io/homeassistant/companion/android/controls/ClimateControl.kt new file mode 100644 index 000000000..5e3fc6bad --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/controls/ClimateControl.kt @@ -0,0 +1,68 @@ +package io.homeassistant.companion.android.controls + +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import android.service.controls.Control +import android.service.controls.DeviceTypes +import android.service.controls.actions.ControlAction +import android.service.controls.actions.FloatAction +import android.service.controls.templates.RangeTemplate +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.webview.WebViewActivity +import kotlinx.coroutines.runBlocking + +@RequiresApi(Build.VERSION_CODES.R) +class ClimateControl { + companion object : HaControl { + + override fun createControl( + context: Context, + entity: Entity> + ): Control { + val control = Control.StatefulBuilder( + entity.entityId, + PendingIntent.getActivity( + context, + 0, + WebViewActivity.newInstance(context), + PendingIntent.FLAG_CANCEL_CURRENT + ) + ) + control.setTitle(entity.attributes["friendly_name"].toString()) + control.setDeviceType(DeviceTypes.TYPE_AC_HEATER) + control.setStatus(Control.STATUS_OK) + control.setControlTemplate( + RangeTemplate( + entity.entityId, + 0f, + 100f, + (entity.attributes["temperature"] as? Number)?.toFloat() ?: 0f, + .5f, + "" + ) + + ) + return control.build() + } + + override fun performAction( + integrationRepository: IntegrationRepository, + action: ControlAction + ): Boolean { + return runBlocking { + integrationRepository.callService( + action.templateId.split(".")[0], + "set_temperature", + hashMapOf( + "entity_id" to action.templateId, + "temperature" to (action as? FloatAction)?.newValue.toString() + ) + ) + return@runBlocking true + } + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/CoverControl.kt b/app/src/main/java/io/homeassistant/companion/android/controls/CoverControl.kt new file mode 100644 index 000000000..fc5e51903 --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/controls/CoverControl.kt @@ -0,0 +1,62 @@ +package io.homeassistant.companion.android.controls + +import android.app.PendingIntent +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.templates.ControlButton +import android.service.controls.templates.ToggleTemplate +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.webview.WebViewActivity +import kotlinx.coroutines.runBlocking + +@RequiresApi(Build.VERSION_CODES.R) +class CoverControl { + companion object : HaControl { override fun createControl( + context: Context, + entity: Entity> + ): Control { + val control = Control.StatefulBuilder( + entity.entityId, + PendingIntent.getActivity( + context, + 0, + WebViewActivity.newInstance(context), + PendingIntent.FLAG_CANCEL_CURRENT + ) + ) + control.setTitle(entity.attributes["friendly_name"].toString()) + control.setDeviceType(DeviceTypes.TYPE_GARAGE) + control.setStatus(Control.STATUS_OK) + control.setControlTemplate( + ToggleTemplate( + entity.entityId, + ControlButton( + entity.state == "open", + "Description" + ) + ) + ) + return control.build() + } + + override fun performAction( + integrationRepository: IntegrationRepository, + action: ControlAction + ): Boolean { + return runBlocking { + integrationRepository.callService( + action.templateId.split(".")[0], + if ((action as? BooleanAction)?.newState == true) "open_cover" else "close_cover", + hashMapOf("entity_id" to action.templateId) + ) + return@runBlocking true + } + } + } +} diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/FanControl.kt b/app/src/main/java/io/homeassistant/companion/android/controls/FanControl.kt index ba651912f..78e97b6d8 100644 --- a/app/src/main/java/io/homeassistant/companion/android/controls/FanControl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/controls/FanControl.kt @@ -8,8 +8,10 @@ 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.templates.ControlButton import android.service.controls.templates.RangeTemplate import android.service.controls.templates.ToggleRangeTemplate +import android.service.controls.templates.ToggleTemplate import androidx.annotation.RequiresApi import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.IntegrationRepository @@ -23,7 +25,10 @@ class FanControl { context: Context, entity: Entity> ): Control { - val speeds = entity.attributes["speed_list"].toString().split(", ") + val speeds = entity.attributes["speed_list"].toString() + .removeSurrounding("[", "]") + .split(", ") + val currentSpeed: Int? = entity.attributes["speed"].toString().toIntOrNull() val control = Control.StatefulBuilder( entity.entityId, @@ -37,21 +42,33 @@ class FanControl { control.setTitle(entity.attributes["friendly_name"].toString()) control.setDeviceType(DeviceTypes.TYPE_FAN) control.setStatus(Control.STATUS_OK) - control.setControlTemplate( - ToggleRangeTemplate( - entity.entityId, - entity.state != "off", - "", - RangeTemplate( + if (currentSpeed != null) { + control.setControlTemplate( + ToggleRangeTemplate( entity.entityId, - 0f, - speeds.size.toFloat(), - speeds.indexOf(entity.state).toFloat(), - 1f, - "" + entity.state != "off", + "", + RangeTemplate( + entity.entityId, + 0f, + speeds.size.toFloat() - 1, + speeds.indexOf(currentSpeed.toString()).toFloat(), + 1f, + "" + ) ) ) - ) + } else { + control.setControlTemplate( + ToggleTemplate( + entity.entityId, + ControlButton( + entity.state != "off", + "" + ) + ) + ) + } return control.build() } @@ -60,23 +77,29 @@ class FanControl { action: ControlAction ): Boolean { return runBlocking { - // TODO: Get these from entity - val speeds = listOf("off", "low", "medium", "high") - val speed: String = if (action is BooleanAction) { - if (action.newState) speeds.last() else speeds.first() - } else if (action is FloatAction) { - speeds[action.newValue.toInt()] - } else { - "" + when (action) { + is BooleanAction -> { + integrationRepository.callService( + action.templateId.split(".")[0], + if (action.newState) "turn_on" else "turn_off", + hashMapOf("entity_id" to action.templateId) + ) + } + is FloatAction -> { + val speeds = integrationRepository.getEntity(action.templateId) + .attributes["speed_list"].toString() + .removeSurrounding("[", "]") + .split(", ") + integrationRepository.callService( + action.templateId.split(".")[0], + "set_speed", + hashMapOf( + "entity_id" to action.templateId, + "speed" to speeds[action.newValue.toInt()] + ) + ) + } } - integrationRepository.callService( - action.templateId.split(".")[0], - "set_speed", - hashMapOf( - "entity_id" to action.templateId, - "speed" to speed - ) - ) return@runBlocking true } } diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/HaControlsProviderService.kt b/app/src/main/java/io/homeassistant/companion/android/controls/HaControlsProviderService.kt index 9475d2a01..e43069924 100644 --- a/app/src/main/java/io/homeassistant/companion/android/controls/HaControlsProviderService.kt +++ b/app/src/main/java/io/homeassistant/companion/android/controls/HaControlsProviderService.kt @@ -1,11 +1,14 @@ package io.homeassistant.companion.android.controls import android.os.Build +import android.os.Handler +import android.os.Looper import android.service.controls.Control import android.service.controls.ControlsProviderService import android.service.controls.actions.ControlAction import android.util.Log import androidx.annotation.RequiresApi +import androidx.core.os.postDelayed import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor import io.homeassistant.companion.android.common.data.integration.Entity import io.homeassistant.companion.android.common.data.integration.IntegrationRepository @@ -29,17 +32,37 @@ class HaControlsProviderService : ControlsProviderService() { private val ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO) + private val monitoredEntities = mutableListOf() + private val handler = Handler(Looper.getMainLooper()) + // This is the poor mans way to do this. We should really connect via websocket and update + // on events. But now we get updates every 5 seconds while on power menu. + private val refresh = object : Runnable { + override fun run() { + monitoredEntities.forEach { entityId -> + ioScope.launch { + val entity = integrationRepository.getEntity(entityId) + val domain = entity.entityId.split(".")[0] + val control = domainToHaControl[domain]?.createControl(applicationContext, entity) + updateSubscriber?.onNext(control) + } + } + handler.postDelayed(this, 5000) + } + } + private var updateSubscriber: Flow.Subscriber? = null private val domainToHaControl = mapOf( "camera" to null, - "climate" to null, - "fan" to null, + "climate" to ClimateControl, + "cover" to CoverControl, + "fan" to FanControl, "light" to LightControl, "media_player" to null, "remote" to null, - "input_boolean" to DefaultSwitchControl, + "scene" to SceneControl, "switch" to DefaultSwitchControl, + "input_boolean" to DefaultSwitchControl, "input_number" to DefaultSliderControl ) @@ -72,25 +95,20 @@ class HaControlsProviderService : ControlsProviderService() { override fun createPublisherFor(controlIds: MutableList): Flow.Publisher { Log.d(TAG, "publisherFor $controlIds") return Flow.Publisher { subscriber -> - controlIds.forEach { controlId -> - ioScope.launch { - val entity = integrationRepository.getEntity(controlId) - val domain = entity.entityId.split(".")[0] - val control = domainToHaControl[domain]?.createControl(applicationContext, entity) - subscriber.onSubscribe(object : Flow.Subscription { - override fun request(n: Long) { - Log.d(TAG, "request $n") - updateSubscriber = subscriber - } - - override fun cancel() { - Log.d(TAG, "cancel") - updateSubscriber = null - } - }) - subscriber.onNext(control) + subscriber.onSubscribe(object : Flow.Subscription { + override fun request(n: Long) { + Log.d(TAG, "request $n") + updateSubscriber = subscriber } - } + + override fun cancel() { + Log.d(TAG, "cancel") + updateSubscriber = null + handler.removeCallbacks(refresh) + } + }) + monitoredEntities.addAll(controlIds) + handler.post(refresh) } } @@ -110,6 +128,11 @@ class HaControlsProviderService : ControlsProviderService() { val entity = integrationRepository.getEntity(controlId) updateSubscriber?.onNext(haControl.createControl(applicationContext, entity)) + handler.postDelayed(750) { + // This is here because the state isn't aways instantly updated. This should + // cause us to update a second time rapidly to ensure we display the correct state + updateSubscriber?.onNext(haControl.createControl(applicationContext, entity)) + } } } if (actionSuccess) { diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/LightControl.kt b/app/src/main/java/io/homeassistant/companion/android/controls/LightControl.kt index df2193964..f6aa3fce9 100644 --- a/app/src/main/java/io/homeassistant/companion/android/controls/LightControl.kt +++ b/app/src/main/java/io/homeassistant/companion/android/controls/LightControl.kt @@ -44,7 +44,7 @@ class LightControl { entity.entityId, 0f, 255f, - (entity.attributes["brightness"] as? Int)?.toFloat() ?: 0f, + (entity.attributes["brightness"] as? Number)?.toFloat() ?: 0f, 1f, "" ) diff --git a/app/src/main/java/io/homeassistant/companion/android/controls/SceneControl.kt b/app/src/main/java/io/homeassistant/companion/android/controls/SceneControl.kt new file mode 100644 index 000000000..6c603145f --- /dev/null +++ b/app/src/main/java/io/homeassistant/companion/android/controls/SceneControl.kt @@ -0,0 +1,58 @@ +package io.homeassistant.companion.android.controls + +import android.app.PendingIntent +import android.content.Context +import android.os.Build +import android.service.controls.Control +import android.service.controls.DeviceTypes +import android.service.controls.actions.ControlAction +import android.service.controls.templates.StatelessTemplate +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.webview.WebViewActivity +import kotlinx.coroutines.runBlocking + +@RequiresApi(Build.VERSION_CODES.R) +class SceneControl { + companion object : HaControl { + + override fun createControl( + context: Context, + entity: Entity> + ): Control { + val control = Control.StatefulBuilder( + entity.entityId, + PendingIntent.getActivity( + context, + 0, + WebViewActivity.newInstance(context), + PendingIntent.FLAG_CANCEL_CURRENT + ) + ) + control.setTitle(entity.attributes["friendly_name"].toString()) + control.setDeviceType(DeviceTypes.TYPE_ROUTINE) + control.setStatus(Control.STATUS_OK) + control.setControlTemplate( + StatelessTemplate( + entity.entityId + ) + ) + return control.build() + } + + override fun performAction( + integrationRepository: IntegrationRepository, + action: ControlAction + ): Boolean { + return runBlocking { + integrationRepository.callService( + action.templateId.split(".")[0], + "turn_on", + hashMapOf("entity_id" to action.templateId) + ) + return@runBlocking true + } + } + } +}