Skip to content

Commit c7d29ac

Browse files
authored
Define CommandMessage pattern and implement first messages (#6640)
1 parent 039ba62 commit c7d29ac

File tree

2 files changed

+110
-0
lines changed
  • app/src
    • main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing
    • test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing

2 files changed

+110
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.homeassistant.companion.android.frontend.externalbus.outgoing
2+
3+
import io.homeassistant.companion.android.frontend.externalbus.frontendExternalBusJson
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
import kotlinx.serialization.json.JsonElement
7+
import kotlinx.serialization.json.encodeToJsonElement
8+
9+
/**
10+
* Outgoing command message sent to the Home Assistant frontend via the external bus.
11+
*
12+
* Command messages instruct the frontend to perform an action.
13+
*
14+
* The Home Assistant frontend uses `"type": "command"` for all commands, distinguishing them
15+
* only by the `command` string field. This means kotlinx.serialization's sealed polymorphism
16+
* cannot give each command its own subtype (all would need the same `@SerialName("command")`).
17+
*
18+
* Instead, this single private class handles serialization, and each command is exposed as a
19+
* top-level factory: [NavigateToMessage] uses `operator fun invoke` for constructor-like syntax,
20+
* and [ShowSidebarMessage] is a pre-built instance. Call sites read naturally:
21+
* ```
22+
* send(NavigateTo(path = "/dashboard", replace = true))
23+
* send(ShowSidebar)
24+
* ```
25+
*/
26+
@Serializable
27+
@SerialName("command")
28+
private data class CommandMessage(
29+
override val id: Int? = null,
30+
val command: String,
31+
val payload: JsonElement? = null,
32+
) : OutgoingExternalBusMessage
33+
34+
/**
35+
* Creates a navigation command to navigate the frontend to the given path.
36+
*
37+
* When [replace] is `true`, the current history entry is replaced instead of
38+
* pushing a new one (useful for resetting to the default dashboard).
39+
*
40+
* Requires Home Assistant 2025.6 or later. Callers must check the server version
41+
* before sending this command.
42+
*
43+
* @see CommandMessage
44+
*/
45+
object NavigateToMessage {
46+
operator fun invoke(path: String, replace: Boolean = false): OutgoingExternalBusMessage = CommandMessage(
47+
command = "navigate",
48+
payload = frontendExternalBusJson.encodeToJsonElement(
49+
NavigatePayload(path = path, options = NavigateOptions(replace = replace)),
50+
),
51+
)
52+
53+
@Serializable
54+
private data class NavigatePayload(val path: String, val options: NavigateOptions = NavigateOptions())
55+
56+
@Serializable
57+
private data class NavigateOptions(val replace: Boolean = false)
58+
}
59+
60+
/**
61+
* Command to toggle the frontend sidebar visibility.
62+
*
63+
* @see CommandMessage
64+
*/
65+
val ShowSidebarMessage: OutgoingExternalBusMessage = CommandMessage(command = "sidebar/show")
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.homeassistant.companion.android.frontend.externalbus.outgoing
2+
3+
import io.homeassistant.companion.android.frontend.externalbus.frontendExternalBusJson
4+
import io.homeassistant.companion.android.testing.unit.ConsoleLogExtension
5+
import org.junit.jupiter.api.Assertions.assertEquals
6+
import org.junit.jupiter.api.Test
7+
import org.junit.jupiter.api.extension.ExtendWith
8+
9+
@ExtendWith(ConsoleLogExtension::class)
10+
class CommandMessageTest {
11+
12+
@Test
13+
fun `Given NavigateToMessage with replace when serializing then produces correct JSON`() {
14+
val message = NavigateToMessage(path = "/", replace = true)
15+
16+
val json = frontendExternalBusJson.encodeToString<OutgoingExternalBusMessage>(message)
17+
18+
assertEquals(
19+
"""{"type":"command","id":null,"command":"navigate","payload":{"path":"/","options":{"replace":true}}}""",
20+
json,
21+
)
22+
}
23+
24+
@Test
25+
fun `Given NavigateToMessage without replace when serializing then defaults replace to false`() {
26+
val message = NavigateToMessage(path = "/lovelace/dashboard")
27+
28+
val json = frontendExternalBusJson.encodeToString<OutgoingExternalBusMessage>(message)
29+
30+
assertEquals(
31+
"""{"type":"command","id":null,"command":"navigate","payload":{"path":"/lovelace/dashboard","options":{"replace":false}}}""",
32+
json,
33+
)
34+
}
35+
36+
@Test
37+
fun `Given ShowSidebarMessage when serializing then produces correct JSON without payload`() {
38+
val json = frontendExternalBusJson.encodeToString<OutgoingExternalBusMessage>(ShowSidebarMessage)
39+
40+
assertEquals(
41+
"""{"type":"command","id":null,"command":"sidebar/show","payload":null}""",
42+
json,
43+
)
44+
}
45+
}

0 commit comments

Comments
 (0)