Power Menu Enhancements (#1057)

* Add scene support.

* Add automatic refresh.

* Add climate support (hasn't been fully tested)...

* Add cover control.

* Add Fan Support.

* ktlint

* Fix climate and cleanup light.
This commit is contained in:
Justin Bassett 2020-10-15 20:57:38 -04:00 committed by GitHub
parent c620bbabd2
commit d095a15106
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 285 additions and 51 deletions

View file

@ -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<Map<String, Any>>
): 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
}
}
}
}

View file

@ -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<Map<String, Any>>
): 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
}
}
}
}

View file

@ -8,8 +8,10 @@ import android.service.controls.DeviceTypes
import android.service.controls.actions.BooleanAction import android.service.controls.actions.BooleanAction
import android.service.controls.actions.ControlAction import android.service.controls.actions.ControlAction
import android.service.controls.actions.FloatAction import android.service.controls.actions.FloatAction
import android.service.controls.templates.ControlButton
import android.service.controls.templates.RangeTemplate import android.service.controls.templates.RangeTemplate
import android.service.controls.templates.ToggleRangeTemplate import android.service.controls.templates.ToggleRangeTemplate
import android.service.controls.templates.ToggleTemplate
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import io.homeassistant.companion.android.common.data.integration.Entity 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.IntegrationRepository
@ -23,7 +25,10 @@ class FanControl {
context: Context, context: Context,
entity: Entity<Map<String, Any>> entity: Entity<Map<String, Any>>
): Control { ): 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( val control = Control.StatefulBuilder(
entity.entityId, entity.entityId,
@ -37,6 +42,7 @@ class FanControl {
control.setTitle(entity.attributes["friendly_name"].toString()) control.setTitle(entity.attributes["friendly_name"].toString())
control.setDeviceType(DeviceTypes.TYPE_FAN) control.setDeviceType(DeviceTypes.TYPE_FAN)
control.setStatus(Control.STATUS_OK) control.setStatus(Control.STATUS_OK)
if (currentSpeed != null) {
control.setControlTemplate( control.setControlTemplate(
ToggleRangeTemplate( ToggleRangeTemplate(
entity.entityId, entity.entityId,
@ -45,13 +51,24 @@ class FanControl {
RangeTemplate( RangeTemplate(
entity.entityId, entity.entityId,
0f, 0f,
speeds.size.toFloat(), speeds.size.toFloat() - 1,
speeds.indexOf(entity.state).toFloat(), speeds.indexOf(currentSpeed.toString()).toFloat(),
1f, 1f,
"" ""
) )
) )
) )
} else {
control.setControlTemplate(
ToggleTemplate(
entity.entityId,
ControlButton(
entity.state != "off",
""
)
)
)
}
return control.build() return control.build()
} }
@ -60,23 +77,29 @@ class FanControl {
action: ControlAction action: ControlAction
): Boolean { ): Boolean {
return runBlocking { return runBlocking {
// TODO: Get these from entity when (action) {
val speeds = listOf("off", "low", "medium", "high") is BooleanAction -> {
val speed: String = if (action is BooleanAction) { integrationRepository.callService(
if (action.newState) speeds.last() else speeds.first() action.templateId.split(".")[0],
} else if (action is FloatAction) { if (action.newState) "turn_on" else "turn_off",
speeds[action.newValue.toInt()] hashMapOf("entity_id" to action.templateId)
} else { )
""
} }
is FloatAction -> {
val speeds = integrationRepository.getEntity(action.templateId)
.attributes["speed_list"].toString()
.removeSurrounding("[", "]")
.split(", ")
integrationRepository.callService( integrationRepository.callService(
action.templateId.split(".")[0], action.templateId.split(".")[0],
"set_speed", "set_speed",
hashMapOf( hashMapOf(
"entity_id" to action.templateId, "entity_id" to action.templateId,
"speed" to speed "speed" to speeds[action.newValue.toInt()]
) )
) )
}
}
return@runBlocking true return@runBlocking true
} }
} }

View file

@ -1,11 +1,14 @@
package io.homeassistant.companion.android.controls package io.homeassistant.companion.android.controls
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.service.controls.Control import android.service.controls.Control
import android.service.controls.ControlsProviderService import android.service.controls.ControlsProviderService
import android.service.controls.actions.ControlAction import android.service.controls.actions.ControlAction
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.os.postDelayed
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor 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.Entity
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository 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 ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private val monitoredEntities = mutableListOf<String>()
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<in Control>? = null private var updateSubscriber: Flow.Subscriber<in Control>? = null
private val domainToHaControl = mapOf( private val domainToHaControl = mapOf(
"camera" to null, "camera" to null,
"climate" to null, "climate" to ClimateControl,
"fan" to null, "cover" to CoverControl,
"fan" to FanControl,
"light" to LightControl, "light" to LightControl,
"media_player" to null, "media_player" to null,
"remote" to null, "remote" to null,
"input_boolean" to DefaultSwitchControl, "scene" to SceneControl,
"switch" to DefaultSwitchControl, "switch" to DefaultSwitchControl,
"input_boolean" to DefaultSwitchControl,
"input_number" to DefaultSliderControl "input_number" to DefaultSliderControl
) )
@ -72,11 +95,6 @@ class HaControlsProviderService : ControlsProviderService() {
override fun createPublisherFor(controlIds: MutableList<String>): Flow.Publisher<Control> { override fun createPublisherFor(controlIds: MutableList<String>): Flow.Publisher<Control> {
Log.d(TAG, "publisherFor $controlIds") Log.d(TAG, "publisherFor $controlIds")
return Flow.Publisher { subscriber -> 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 { subscriber.onSubscribe(object : Flow.Subscription {
override fun request(n: Long) { override fun request(n: Long) {
Log.d(TAG, "request $n") Log.d(TAG, "request $n")
@ -86,11 +104,11 @@ class HaControlsProviderService : ControlsProviderService() {
override fun cancel() { override fun cancel() {
Log.d(TAG, "cancel") Log.d(TAG, "cancel")
updateSubscriber = null updateSubscriber = null
handler.removeCallbacks(refresh)
} }
}) })
subscriber.onNext(control) monitoredEntities.addAll(controlIds)
} handler.post(refresh)
}
} }
} }
@ -110,6 +128,11 @@ class HaControlsProviderService : ControlsProviderService() {
val entity = integrationRepository.getEntity(controlId) val entity = integrationRepository.getEntity(controlId)
updateSubscriber?.onNext(haControl.createControl(applicationContext, entity)) 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) { if (actionSuccess) {

View file

@ -44,7 +44,7 @@ class LightControl {
entity.entityId, entity.entityId,
0f, 0f,
255f, 255f,
(entity.attributes["brightness"] as? Int)?.toFloat() ?: 0f, (entity.attributes["brightness"] as? Number)?.toFloat() ?: 0f,
1f, 1f,
"" ""
) )

View file

@ -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<Map<String, Any>>
): 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
}
}
}
}