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

View file

@ -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<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 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<String>): Flow.Publisher<Control> {
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) {

View file

@ -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,
""
)

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
}
}
}
}