From 8af25ec1f0f971a68811b02665b1a08c64408566 Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Thu, 7 May 2026 14:06:23 +0200 Subject: [PATCH] Add Improv external bus messages models --- .../android/frontend/FrontendViewModel.kt | 9 +++++ .../incoming/IncomingExternalBusMessage.kt | 33 +++++++++++++++ .../externalbus/outgoing/CommandMessage.kt | 29 ++++++++++++++ .../outgoing/OutgoingExternalBusMessage.kt | 2 + .../frontend/handler/FrontendHandlerEvent.kt | 20 ++++++++++ .../handler/FrontendMessageHandler.kt | 15 +++++++ .../frontend/improv/BluetoothCapabilities.kt | 16 ++++++++ .../frontend/improv/FrontendImprovModule.kt | 20 ++++++++++ .../IncomingExternalBusMessageTest.kt | 32 +++++++++++++++ .../outgoing/CommandMessageTest.kt | 22 ++++++++++ .../OutgoingExternalBusMessageTest.kt | 35 ++++++++++++++++ .../handler/FrontendMessageHandlerTest.kt | 40 +++++++++++++++++++ 12 files changed, 273 insertions(+) create mode 100644 app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/BluetoothCapabilities.kt create mode 100644 app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/FrontendImprovModule.kt diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt index d70bfa2903c..41bc6a360f9 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt @@ -588,6 +588,15 @@ internal class FrontendViewModel @VisibleForTesting constructor( exoPlayerManager.handle(result) } + is FrontendHandlerEvent.StartImprovScan, + is FrontendHandlerEvent.ConfigureImprovDevice, + -> { + // Improv handling lands in a follow-up PR; the messages are already typed and + // canSetupImprov will be `false` on devices without BLE so the frontend should + // not be sending these yet on hardware that lacks Bluetooth LE. + Timber.d("Improv event received but not yet handled: $result") + } + is FrontendHandlerEvent.ConfigSent, is FrontendHandlerEvent.UnknownMessage, -> { diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt index f457f6e9c11..2ef129b9820 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessage.kt @@ -216,3 +216,36 @@ data class ExoPlayerResizePayload( val right: Double = 0.0, val bottom: Double = 0.0, ) + +/** + * Message requesting the app to start scanning for nearby BLE devices that advertise the + * Improv Wi-Fi service. + * + * The frontend sends this once when the user opens its "Add device" flow. The app responds + * out-of-band with a stream of [io.homeassistant.companion.android.frontend.externalbus.outgoing.ImprovDiscoveredDeviceMessage] + * commands as devices are discovered. No `result`-shaped response is expected. + * + * Will not be sent by the frontend when the device reports + * [io.homeassistant.companion.android.frontend.externalbus.outgoing.ConfigResult.canSetupImprov] = `false`. + */ +@Serializable +@SerialName("improv/scan") +data class ImprovScanMessage(override val id: Int? = null) : IncomingExternalBusMessage + +/** + * Message requesting the app to begin Wi-Fi onboarding for the named improv device the user + * picked from the discovery list. + * + * The app should open the credentials dialog for [ImprovConfigureDevicePayload.name], submit + * the entered Wi-Fi credentials over BLE, and finally — once the device reports it has been + * provisioned — emit + * [io.homeassistant.companion.android.frontend.externalbus.outgoing.ImprovDeviceSetupDoneMessage] + * and navigate the frontend to the matching `config_flow_start` URL. + */ +@Serializable +@SerialName("improv/configure_device") +data class ImprovConfigureDeviceMessage(override val id: Int? = null, val payload: ImprovConfigureDevicePayload) : + IncomingExternalBusMessage + +@Serializable +data class ImprovConfigureDevicePayload(val name: String) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessage.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessage.kt index 90d56342e9e..7a3593da352 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessage.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessage.kt @@ -68,3 +68,32 @@ object NavigateToMessage { * @see CommandMessage */ val ShowSidebarMessage: OutgoingExternalBusMessage = CommandMessage(command = "sidebar/show") + +/** + * Notifies the frontend that an improv-capable BLE device has been discovered while scanning. + * + * Emitted once per device — the frontend deduplicates by [name] when adding the entry to its + * "Add device" list. This is a one-way command; the frontend does not respond. + * + * @see CommandMessage + */ +object ImprovDiscoveredDeviceMessage { + operator fun invoke(name: String): OutgoingExternalBusMessage = CommandMessage( + command = "improv/discovered_device", + payload = frontendExternalBusJson.encodeToJsonElement(DiscoveredDevicePayload(name = name)), + ) + + @Serializable + private data class DiscoveredDevicePayload(val name: String) +} + +/** + * Notifies the frontend that the user-selected improv device has finished its Wi-Fi onboarding + * (BLE state moved to provisioned). The frontend uses this to dismiss its progress UI before + * the app navigates the WebView to the `config_flow_start` URL for the device's domain. + * + * One-way command; no response is expected. + * + * @see CommandMessage + */ +val ImprovDeviceSetupDoneMessage: OutgoingExternalBusMessage = CommandMessage(command = "improv/device_setup_done") diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt index 0c7119d0903..68fca64ed2f 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessage.kt @@ -88,12 +88,14 @@ data class ConfigResult( canCommissionMatter: Boolean, canExportThread: Boolean, hasBarCodeScanner: Int, + canSetupImprov: Boolean, appVersion: AppVersion, ) = ConfigResult( canWriteTag = hasNfc, canCommissionMatter = canCommissionMatter, canImportThreadCredentials = canExportThread, hasBarCodeScanner = hasBarCodeScanner, + canSetupImprov = canSetupImprov, appVersion = appVersion.value, ) } diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt index 4f6d04207d7..523be578ee7 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendHandlerEvent.kt @@ -81,6 +81,26 @@ sealed interface FrontendHandlerEvent { */ data class WriteNfcTag(val messageId: Int, val tagId: String?) : FrontendHandlerEvent + /** + * Frontend requested the app to start scanning for improv (Wi-Fi onboarding) BLE devices. + * + * The ViewModel is responsible for the BLE-feature gate, the runtime permission flow + * (Bluetooth + Location), and emitting + * [io.homeassistant.companion.android.frontend.externalbus.outgoing.ImprovDiscoveredDeviceMessage] + * for each device the scanner reports. + */ + data object StartImprovScan : FrontendHandlerEvent + + /** + * User picked an improv device in the frontend's list. The ViewModel should open the + * Wi-Fi-credentials dialog seeded with [deviceName], drive the BLE provisioning flow, and + * emit [io.homeassistant.companion.android.frontend.externalbus.outgoing.ImprovDeviceSetupDoneMessage] + * once provisioning succeeds. + * + * @param deviceName Advertised name of the BLE device the user selected. + */ + data class ConfigureImprovDevice(val deviceName: String) : FrontendHandlerEvent + sealed interface ExoPlayerAction : FrontendHandlerEvent { /** diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt index 2b0dd7953f7..21b456897ef 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandler.kt @@ -16,6 +16,8 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.ExoPlaye import io.homeassistant.companion.android.frontend.externalbus.incoming.ExoPlayerStopMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.HandleBlobMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.ImprovConfigureDeviceMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.ImprovScanMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.IncomingExternalBusMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistSettingsMessage @@ -25,6 +27,7 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpd import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage import io.homeassistant.companion.android.frontend.externalbus.outgoing.ConfigResult import io.homeassistant.companion.android.frontend.externalbus.outgoing.ResultMessage +import io.homeassistant.companion.android.frontend.improv.BluetoothCapabilities import io.homeassistant.companion.android.frontend.js.FrontendJsHandler import io.homeassistant.companion.android.frontend.session.AuthPayload import io.homeassistant.companion.android.frontend.session.ExternalAuthResult @@ -64,6 +67,7 @@ class FrontendMessageHandler @Inject constructor( private val appVersionProvider: AppVersionProvider, private val sessionManager: ServerSessionManager, private val downloadManager: FrontendDownloadManager, + private val bluetoothCapabilities: BluetoothCapabilities, @param:IsAutomotive private val isAutomotive: Boolean, ) : FrontendJsHandler, FrontendBusObserver { @@ -224,6 +228,16 @@ class FrontendMessageHandler @Inject constructor( FrontendHandlerEvent.DownloadCompleted(result) } + is ImprovScanMessage -> { + Timber.d("improv/scan received with id: ${message.id}") + FrontendHandlerEvent.StartImprovScan + } + + is ImprovConfigureDeviceMessage -> { + Timber.d("improv/configure_device received with id: ${message.id}") + FrontendHandlerEvent.ConfigureImprovDevice(deviceName = message.payload.name) + } + is UnknownIncomingMessage -> { Timber.d("Unknown message type received: ${message.content}") FrontendHandlerEvent.UnknownMessage @@ -250,6 +264,7 @@ class FrontendMessageHandler @Inject constructor( canCommissionMatter = canCommissionMatter, canExportThread = canExportThread, hasBarCodeScanner = hasBarCodeScanner, + canSetupImprov = bluetoothCapabilities.hasBluetoothLe(), appVersion = appVersionProvider(), ), ) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/BluetoothCapabilities.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/BluetoothCapabilities.kt new file mode 100644 index 00000000000..e2152ca7618 --- /dev/null +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/BluetoothCapabilities.kt @@ -0,0 +1,16 @@ +package io.homeassistant.companion.android.frontend.improv + +/** + * Reports the device's Bluetooth-related capabilities. + * + * Exposed as an interface so call sites (e.g. the config/get response, the improv scan flow) + * can stay testable without Robolectric. + */ +fun interface BluetoothCapabilities { + + /** + * @return `true` when the device declares Bluetooth Low Energy support + * ([android.content.pm.PackageManager.FEATURE_BLUETOOTH_LE]). + */ + fun hasBluetoothLe(): Boolean +} diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/FrontendImprovModule.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/FrontendImprovModule.kt new file mode 100644 index 00000000000..79e35be9abf --- /dev/null +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/improv/FrontendImprovModule.kt @@ -0,0 +1,20 @@ +package io.homeassistant.companion.android.frontend.improv + +import android.content.pm.PackageManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +/** + * Hilt bindings for the frontend's improv (Wi-Fi onboarding for BLE devices) integration. + */ +@Module +@InstallIn(SingletonComponent::class) +object FrontendImprovModule { + + @Provides + fun provideBluetoothCapabilities(packageManager: PackageManager): BluetoothCapabilities = BluetoothCapabilities { + packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) + } +} diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt index 1409730d4e6..0f990d8bca1 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/incoming/IncomingExternalBusMessageTest.kt @@ -203,6 +203,38 @@ class IncomingExternalBusMessageTest { assertEquals(200.5, resize.payload.bottom) } + @Test + fun `Given improv scan JSON then parses to ImprovScanMessage`() { + val json = """{"type":"improv/scan","id":50}""" + + val message = frontendExternalBusJson.decodeFromString(json) + + assertInstanceOf(ImprovScanMessage::class.java, message) + assertEquals(50, (message as ImprovScanMessage).id) + } + + @Test + fun `Given improv scan JSON without id then parses to ImprovScanMessage with null id`() { + val json = """{"type":"improv/scan"}""" + + val message = frontendExternalBusJson.decodeFromString(json) + + assertInstanceOf(ImprovScanMessage::class.java, message) + assertNull((message as ImprovScanMessage).id) + } + + @Test + fun `Given improv configure_device JSON then parses to ImprovConfigureDeviceMessage with name`() { + val json = """{"type":"improv/configure_device","id":51,"payload":{"name":"Smart Plug"}}""" + + val message = frontendExternalBusJson.decodeFromString(json) + + assertInstanceOf(ImprovConfigureDeviceMessage::class.java, message) + val configureMessage = message as ImprovConfigureDeviceMessage + assertEquals(51, configureMessage.id) + assertEquals("Smart Plug", configureMessage.payload.name) + } + @Test fun `Given exoplayer resize JSON without payload then parses with zero defaults`() { val json = """{"type":"exoplayer/resize","id":25}""" diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessageTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessageTest.kt index 5f293fe8441..d2863a9dadf 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessageTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/CommandMessageTest.kt @@ -42,4 +42,26 @@ class CommandMessageTest { json, ) } + + @Test + fun `Given ImprovDiscoveredDeviceMessage when serializing then produces command with name payload`() { + val message = ImprovDiscoveredDeviceMessage(name = "Smart Plug") + + val json = frontendExternalBusJson.encodeToString(message) + + assertEquals( + """{"type":"command","id":null,"command":"improv/discovered_device","payload":{"name":"Smart Plug"}}""", + json, + ) + } + + @Test + fun `Given ImprovDeviceSetupDoneMessage when serializing then produces command without payload`() { + val json = frontendExternalBusJson.encodeToString(ImprovDeviceSetupDoneMessage) + + assertEquals( + """{"type":"command","id":null,"command":"improv/device_setup_done","payload":null}""", + json, + ) + } } diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessageTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessageTest.kt index af77011d742..642bf67511a 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessageTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/externalbus/outgoing/OutgoingExternalBusMessageTest.kt @@ -4,6 +4,7 @@ import io.homeassistant.companion.android.common.util.AppVersion import io.homeassistant.companion.android.frontend.externalbus.frontendExternalBusJson import io.homeassistant.companion.android.testing.unit.ConsoleLogExtension import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -21,6 +22,7 @@ class OutgoingExternalBusMessageTest { canCommissionMatter = true, canExportThread = true, hasBarCodeScanner = 0, + canSetupImprov = true, appVersion = AppVersion.from("1.0.0 (1)"), ), ), @@ -31,6 +33,24 @@ class OutgoingExternalBusMessageTest { ) } + @Test + fun `Given device without BLE when serializing config then canSetupImprov is false`() { + val json = frontendExternalBusJson.encodeToString( + ResultMessage.config( + id = 2, + config = ConfigResult.create( + hasNfc = false, + canCommissionMatter = false, + canExportThread = false, + hasBarCodeScanner = 0, + canSetupImprov = false, + appVersion = AppVersion.from("1.0.0 (1)"), + ), + ), + ) + assertTrue(json.contains(""""canSetupImprov":false""")) + } + @Test fun `Given ConfigResult then default values are correct`() { val config = ConfigResult.create( @@ -38,6 +58,7 @@ class OutgoingExternalBusMessageTest { canCommissionMatter = false, canExportThread = false, hasBarCodeScanner = 0, + canSetupImprov = true, appVersion = AppVersion.from("1.0.0 (1)"), ) @@ -49,4 +70,18 @@ class OutgoingExternalBusMessageTest { assertTrue(config.hasEntityAddTo) assertTrue(config.hasAssistSettings) } + + @Test + fun `Given canSetupImprov passed false to create then ConfigResult exposes canSetupImprov false`() { + val config = ConfigResult.create( + hasNfc = false, + canCommissionMatter = false, + canExportThread = false, + hasBarCodeScanner = 0, + canSetupImprov = false, + appVersion = AppVersion.from("1.0.0 (1)"), + ) + + assertFalse(config.canSetupImprov) + } } diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt index d379ef51362..f31c0dee6aa 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/handler/FrontendMessageHandlerTest.kt @@ -23,6 +23,9 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.ExoPlaye import io.homeassistant.companion.android.frontend.externalbus.incoming.HandleBlobMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.HapticType +import io.homeassistant.companion.android.frontend.externalbus.incoming.ImprovConfigureDeviceMessage +import io.homeassistant.companion.android.frontend.externalbus.incoming.ImprovConfigureDevicePayload +import io.homeassistant.companion.android.frontend.externalbus.incoming.ImprovScanMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistMessage import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistPayload import io.homeassistant.companion.android.frontend.externalbus.incoming.OpenAssistSettingsMessage @@ -33,6 +36,7 @@ import io.homeassistant.companion.android.frontend.externalbus.incoming.ThemeUpd import io.homeassistant.companion.android.frontend.externalbus.incoming.UnknownIncomingMessage import io.homeassistant.companion.android.frontend.externalbus.outgoing.OutgoingExternalBusMessage import io.homeassistant.companion.android.frontend.externalbus.outgoing.ResultMessage +import io.homeassistant.companion.android.frontend.improv.BluetoothCapabilities import io.homeassistant.companion.android.frontend.session.AuthPayload import io.homeassistant.companion.android.frontend.session.ExternalAuthResult import io.homeassistant.companion.android.frontend.session.RevokeAuthResult @@ -77,6 +81,7 @@ class FrontendMessageHandlerTest { private val appVersionProvider: AppVersionProvider = mockk() private val sessionManager: ServerSessionManager = mockk(relaxed = true) private val downloadManager: FrontendDownloadManager = mockk(relaxed = true) + private val bluetoothCapabilities: BluetoothCapabilities = BluetoothCapabilities { true } private lateinit var handler: FrontendMessageHandler @BeforeEach @@ -96,6 +101,7 @@ class FrontendMessageHandlerTest { appVersionProvider = appVersionProvider, sessionManager = sessionManager, downloadManager = downloadManager, + bluetoothCapabilities = bluetoothCapabilities, isAutomotive = false, ) } @@ -171,6 +177,7 @@ class FrontendMessageHandlerTest { appVersionProvider = appVersionProvider, sessionManager = sessionManager, downloadManager = downloadManager, + bluetoothCapabilities = BluetoothCapabilities { true }, isAutomotive = false, ) @@ -191,6 +198,7 @@ class FrontendMessageHandlerTest { assertEquals(true, configResult["canCommissionMatter"]?.jsonPrimitive?.content?.toBoolean()) assertEquals(true, configResult["canImportThreadCredentials"]?.jsonPrimitive?.content?.toBoolean()) assertEquals(1, configResult["hasBarCodeScanner"]?.jsonPrimitive?.int) + assertEquals(true, configResult["canSetupImprov"]?.jsonPrimitive?.content?.toBoolean()) assertEquals("2.0.0 (200)", configResult["appVersion"]?.jsonPrimitive?.content) } @@ -209,6 +217,7 @@ class FrontendMessageHandlerTest { appVersionProvider = appVersionProvider, sessionManager = sessionManager, downloadManager = downloadManager, + bluetoothCapabilities = BluetoothCapabilities { false }, isAutomotive = false, ) @@ -229,6 +238,7 @@ class FrontendMessageHandlerTest { assertEquals(false, configResult["canCommissionMatter"]?.jsonPrimitive?.content?.toBoolean()) assertEquals(false, configResult["canImportThreadCredentials"]?.jsonPrimitive?.content?.toBoolean()) assertEquals(0, configResult["hasBarCodeScanner"]?.jsonPrimitive?.int) + assertEquals(false, configResult["canSetupImprov"]?.jsonPrimitive?.content?.toBoolean()) } @Test @@ -328,6 +338,34 @@ class FrontendMessageHandlerTest { } } + @Test + fun `Given improv scan message when messageResults then emits StartImprovScan`() = runTest { + val message = ImprovScanMessage(id = 50) + every { externalBusRepository.incomingMessages() } returns flowOf(message) + + handler.messageResults().test { + val result = awaitItem() + assertTrue(result is FrontendHandlerEvent.StartImprovScan) + expectNoEvents() + } + } + + @Test + fun `Given improv configure_device message when messageResults then emits ConfigureImprovDevice with name`() = runTest { + val message = ImprovConfigureDeviceMessage( + id = 51, + payload = ImprovConfigureDevicePayload(name = "Smart Plug"), + ) + every { externalBusRepository.incomingMessages() } returns flowOf(message) + + handler.messageResults().test { + val result = awaitItem() + assertTrue(result is FrontendHandlerEvent.ConfigureImprovDevice) + assertEquals("Smart Plug", (result as FrontendHandlerEvent.ConfigureImprovDevice).deviceName) + expectNoEvents() + } + } + @Test fun `Given unknown message when messageResults then emits UnknownMessage`() = runTest { val message = UnknownIncomingMessage(discriminator = "unknown-type", content = JsonPrimitive("unknown-type")) @@ -363,6 +401,7 @@ class FrontendMessageHandlerTest { appVersionProvider = appVersionProvider, sessionManager = sessionManager, downloadManager = downloadManager, + bluetoothCapabilities = bluetoothCapabilities, isAutomotive = true, ) @@ -395,6 +434,7 @@ class FrontendMessageHandlerTest { appVersionProvider = appVersionProvider, sessionManager = sessionManager, downloadManager = downloadManager, + bluetoothCapabilities = bluetoothCapabilities, isAutomotive = false, )