mirror of
https://github.com/home-assistant/android
synced 2024-09-18 23:52:51 +00:00
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:
parent
c620bbabd2
commit
d095a15106
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,21 +42,33 @@ 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)
|
||||||
control.setControlTemplate(
|
if (currentSpeed != null) {
|
||||||
ToggleRangeTemplate(
|
control.setControlTemplate(
|
||||||
entity.entityId,
|
ToggleRangeTemplate(
|
||||||
entity.state != "off",
|
|
||||||
"",
|
|
||||||
RangeTemplate(
|
|
||||||
entity.entityId,
|
entity.entityId,
|
||||||
0f,
|
entity.state != "off",
|
||||||
speeds.size.toFloat(),
|
"",
|
||||||
speeds.indexOf(entity.state).toFloat(),
|
RangeTemplate(
|
||||||
1f,
|
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()
|
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(
|
||||||
|
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
|
return@runBlocking true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,25 +95,20 @@ 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 ->
|
subscriber.onSubscribe(object : Flow.Subscription {
|
||||||
ioScope.launch {
|
override fun request(n: Long) {
|
||||||
val entity = integrationRepository.getEntity(controlId)
|
Log.d(TAG, "request $n")
|
||||||
val domain = entity.entityId.split(".")[0]
|
updateSubscriber = subscriber
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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)
|
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) {
|
||||||
|
|
|
@ -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,
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue