Skip to content

Commit 6e2867e

Browse files
authored
Add support for media players in device controls (#4917)
* Add support for media players in device controls * Review comments
1 parent 943b0d5 commit 6e2867e

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

app/src/main/java/io/homeassistant/companion/android/controls/HaControl.kt

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import io.homeassistant.companion.android.common.data.integration.Entity
1717
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
1818
import io.homeassistant.companion.android.common.data.integration.domain
1919
import io.homeassistant.companion.android.common.data.integration.friendlyState
20+
import io.homeassistant.companion.android.common.data.integration.getIcon
21+
import io.homeassistant.companion.android.common.data.integration.isActive
2022
import io.homeassistant.companion.android.webview.WebViewActivity
2123

2224
@RequiresApi(Build.VERSION_CODES.R)
@@ -74,6 +76,14 @@ interface HaControl {
7476
iconDrawable.setTint(ContextCompat.getColor(context, colorTint))
7577
control.setCustomIcon(iconDrawable.toAndroidIconCompat().toIcon(context))
7678
}
79+
} else {
80+
// Specific override for media_player icons to match HA frontend rather than provided device type
81+
if (entity.domain == "media_player") {
82+
val icon = IconicsDrawable(context, entity.getIcon(context)).apply { sizeDp = 48 }
83+
val tint = if (entity.isActive()) R.color.colorDeviceControlsDefaultOn else R.color.colorDeviceControlsOff
84+
icon.setTint(ContextCompat.getColor(context, tint))
85+
control.setCustomIcon(icon.toAndroidIconCompat().toIcon(context))
86+
}
7787
}
7888

7989
return provideControlFeatures(context, control, entity, info).build()

app/src/main/java/io/homeassistant/companion/android/controls/HaControlsProviderService.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class HaControlsProviderService : ControlsProviderService() {
5353
"input_number" to DefaultSliderControl,
5454
"light" to LightControl,
5555
"lock" to LockControl,
56-
"media_player" to null,
56+
"media_player" to MediaPlayerControl,
5757
"remote" to null,
5858
"scene" to DefaultButtonControl,
5959
"script" to DefaultButtonControl,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.homeassistant.companion.android.controls
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import android.service.controls.Control
6+
import android.service.controls.DeviceTypes
7+
import android.service.controls.actions.BooleanAction
8+
import android.service.controls.actions.ControlAction
9+
import android.service.controls.actions.FloatAction
10+
import android.service.controls.templates.ControlButton
11+
import android.service.controls.templates.RangeTemplate
12+
import android.service.controls.templates.ToggleRangeTemplate
13+
import android.service.controls.templates.ToggleTemplate
14+
import androidx.annotation.RequiresApi
15+
import io.homeassistant.companion.android.common.R as commonR
16+
import io.homeassistant.companion.android.common.data.integration.Entity
17+
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
18+
import io.homeassistant.companion.android.common.data.integration.getVolumeLevel
19+
import io.homeassistant.companion.android.common.data.integration.getVolumeStep
20+
import io.homeassistant.companion.android.common.data.integration.isActive
21+
import io.homeassistant.companion.android.common.data.integration.supportsVolumeSet
22+
import java.math.BigDecimal
23+
import java.math.RoundingMode
24+
25+
@RequiresApi(Build.VERSION_CODES.R)
26+
object MediaPlayerControl : HaControl {
27+
override fun provideControlFeatures(
28+
context: Context,
29+
control: Control.StatefulBuilder,
30+
entity: Entity<Map<String, Any>>,
31+
info: HaControlInfo
32+
): Control.StatefulBuilder {
33+
if (entity.supportsVolumeSet()) {
34+
val volumeLevel = entity.getVolumeLevel()
35+
control.setControlTemplate(
36+
ToggleRangeTemplate(
37+
entity.entityId,
38+
entity.isActive(),
39+
"",
40+
RangeTemplate(
41+
entity.entityId,
42+
volumeLevel?.min ?: 0f,
43+
volumeLevel?.max ?: 100f,
44+
volumeLevel?.value ?: 0f,
45+
entity.getVolumeStep(),
46+
"%.0f%%"
47+
)
48+
)
49+
)
50+
} else {
51+
control.setControlTemplate(
52+
ToggleTemplate(
53+
entity.entityId,
54+
ControlButton(
55+
entity.isActive(),
56+
""
57+
)
58+
)
59+
)
60+
}
61+
return control
62+
}
63+
64+
override fun getDeviceType(entity: Entity<Map<String, Any>>): Int =
65+
DeviceTypes.TYPE_TV
66+
67+
override fun getDomainString(context: Context, entity: Entity<Map<String, Any>>): String =
68+
context.getString(commonR.string.media_player)
69+
70+
override suspend fun performAction(
71+
integrationRepository: IntegrationRepository,
72+
action: ControlAction
73+
): Boolean {
74+
when (action) {
75+
is BooleanAction -> {
76+
integrationRepository.callAction(
77+
action.templateId.split(".")[0],
78+
"media_play_pause",
79+
hashMapOf("entity_id" to action.templateId)
80+
)
81+
}
82+
is FloatAction -> {
83+
// Convert back to accepted format:
84+
// https://github.com/home-assistant/frontend/blob/dev/src/dialogs/more-info/controls/more-info-media_player.ts#L289
85+
val volumeLevel = action.newValue.div(100)
86+
integrationRepository.callAction(
87+
action.templateId.split(".")[0],
88+
"volume_set",
89+
hashMapOf(
90+
"entity_id" to action.templateId,
91+
"volume_level" to BigDecimal(volumeLevel.toDouble()).setScale(2, RoundingMode.HALF_UP)
92+
)
93+
)
94+
}
95+
}
96+
return true
97+
}
98+
}

common/src/main/java/io/homeassistant/companion/android/common/data/integration/Entity.kt

+45
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ object EntityExt {
4242
const val LIGHT_SUPPORT_BRIGHTNESS_DEPR = 1
4343
const val LIGHT_SUPPORT_COLOR_TEMP_DEPR = 2
4444
const val ALARM_CONTROL_PANEL_SUPPORT_ARM_AWAY = 2
45+
const val MEDIA_PLAYER_SUPPORT_VOLUME_SET = 4
4546

4647
val DOMAINS_PRESS = listOf("button", "input_button")
4748
val DOMAINS_TOGGLE = listOf(
@@ -298,6 +299,50 @@ fun <T> Entity<T>.getLightColor(): Int? {
298299
}
299300
}
300301

302+
fun <T> Entity<T>.supportsVolumeSet(): Boolean {
303+
return try {
304+
if (domain != "media_player") return false
305+
((attributes as Map<*, *>)["supported_features"] as Int) and EntityExt.MEDIA_PLAYER_SUPPORT_VOLUME_SET == EntityExt.MEDIA_PLAYER_SUPPORT_VOLUME_SET
306+
} catch (e: Exception) {
307+
Log.e(EntityExt.TAG, "Unable to get supportsVolumeSet", e)
308+
false
309+
}
310+
}
311+
312+
fun <T> Entity<T>.getVolumeLevel(): EntityPosition? {
313+
return try {
314+
if (!supportsVolumeSet()) return null
315+
316+
val minValue = 0f
317+
val maxValue = 100f
318+
319+
// Convert to percentage to match frontend behavior:
320+
// https://github.com/home-assistant/frontend/blob/dev/src/dialogs/more-info/controls/more-info-media_player.ts#L137
321+
val currentValue = ((attributes as Map<*, *>)["volume_level"] as? Number)?.toFloat()?.times(100) ?: 0f
322+
323+
EntityPosition(
324+
value = currentValue.coerceAtLeast(minValue).coerceAtMost(maxValue),
325+
min = minValue,
326+
max = maxValue
327+
)
328+
} catch (e: Exception) {
329+
Log.e(EntityExt.TAG, "Unable to get getVolumeLevel", e)
330+
null
331+
}
332+
}
333+
334+
fun <T> Entity<T>.getVolumeStep(): Float {
335+
return try {
336+
if (!supportsVolumeSet()) return 0.1f
337+
338+
val volumeStep = ((attributes as Map<*, *>)["volume_step"] as? Number)?.toFloat() ?: 0.1f
339+
volumeStep.coerceAtLeast(0.01f)
340+
} catch (e: Exception) {
341+
Log.e(EntityExt.TAG, "Unable to get getVolumeStep", e)
342+
0.1f
343+
}
344+
}
345+
301346
fun <T> Entity<T>.getIcon(context: Context): IIcon {
302347
val attributes = this.attributes as Map<String, Any?>
303348
val icon = attributes["icon"] as? String

0 commit comments

Comments
 (0)