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