Sync Thread datasets when starting Matter commissioning (#3349)

* Sync Thread datasets when starting Matter commissioning

 - When starting Matter commissioning from the frontend, check if there are Thread datasets on the device or in core but not the other, and if so try to sync them

* Also sync Thread datasets in shared device flow

 - Datasets may not be used by the system but it is useful for future devices
 - Make methods in ThreadManager a bit more general
This commit is contained in:
Joris Pelgröm 2023-02-21 19:27:29 +01:00 committed by GitHub
parent 5f164ff5a6
commit 25dd869272
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 406 additions and 20 deletions

View file

@ -168,6 +168,7 @@ dependencies {
"fullImplementation"("com.google.android.gms:play-services-location:21.0.1")
"fullImplementation"("com.google.android.gms:play-services-home:16.0.0")
"fullImplementation"("com.google.android.gms:play-services-threadnetwork:16.0.0-beta02")
"fullImplementation"(platform("com.google.firebase:firebase-bom:31.2.2"))
"fullImplementation"("com.google.firebase:firebase-messaging")
"fullImplementation"("io.sentry:sentry-android:6.14.0")

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="com.google.android.gms.threadnetwork" />
<application
android:name="io.homeassistant.companion.android.HomeAssistantApplication" >

View file

@ -3,11 +3,14 @@ package io.homeassistant.companion.android.matter
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.lifecycleScope
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.gms.home.matter.Matter
import com.google.android.gms.home.matter.commissioning.SharedDeviceData
@ -16,6 +19,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.matter.views.MatterCommissioningView
import io.homeassistant.companion.android.webview.WebViewActivity
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@ -33,6 +37,10 @@ class MatterCommissioningActivity : AppCompatActivity() {
private var deviceName by mutableStateOf<String?>(null)
private var servers by mutableStateOf<List<Server>>(emptyList())
private val threadPermissionLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
deviceCode?.let { viewModel.onThreadPermissionResult(result, it) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -43,7 +51,7 @@ class MatterCommissioningActivity : AppCompatActivity() {
deviceName = deviceName,
servers = servers,
onSelectServer = viewModel::checkSupport,
onConfirmCommissioning = { deviceCode?.let { viewModel.commissionDeviceWithCode(it) } },
onConfirmCommissioning = { startCommissioning() },
onClose = { finish() },
onContinue = { continueToApp(false) }
)
@ -80,6 +88,19 @@ class MatterCommissioningActivity : AppCompatActivity() {
}
}
private fun startCommissioning() {
lifecycleScope.launch {
val threadIntent = viewModel.syncThreadIfNecessary()
if (threadIntent != null) {
threadPermissionLauncher.launch(IntentSenderRequest.Builder(threadIntent).build())
} else {
deviceCode?.let {
viewModel.commissionDeviceWithCode(it)
}
}
}
}
private fun continueToApp(hideTransition: Boolean) {
startActivity(WebViewActivity.newInstance(this, null, viewModel.serverId))
finish()

View file

@ -1,6 +1,8 @@
package io.homeassistant.companion.android.matter
import android.app.Application
import android.content.IntentSender
import androidx.activity.result.ActivityResult
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@ -8,12 +10,14 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.thread.ThreadManager
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MatterCommissioningViewModel @Inject constructor(
private val matterManager: MatterManager,
private val threadManager: ThreadManager,
private val serverManager: ServerManager,
application: Application
) : AndroidViewModel(application) {
@ -75,6 +79,22 @@ class MatterCommissioningViewModel @Inject constructor(
}
}
suspend fun syncThreadIfNecessary(): IntentSender? {
step = CommissioningFlowStep.Working
return threadManager.syncPreferredDataset(
getApplication<Application>().applicationContext,
serverId,
viewModelScope
)
}
fun onThreadPermissionResult(result: ActivityResult, code: String) {
viewModelScope.launch {
threadManager.sendThreadDatasetExportResult(result, serverId)
commissionDeviceWithCode(code)
}
}
fun commissionDeviceWithCode(code: String) {
viewModelScope.launch {
step = CommissioningFlowStep.Working

View file

@ -0,0 +1,109 @@
package io.homeassistant.companion.android.thread
import android.app.Activity
import android.content.Context
import android.content.IntentSender
import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResult
import com.google.android.gms.threadnetwork.ThreadBorderAgent
import com.google.android.gms.threadnetwork.ThreadNetwork
import com.google.android.gms.threadnetwork.ThreadNetworkCredentials
import io.homeassistant.companion.android.common.data.HomeAssistantVersion
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class ThreadManagerImpl @Inject constructor(
private val serverManager: ServerManager
) : ThreadManager {
companion object {
private const val TAG = "ThreadManagerImpl"
// ID is a placeholder while we wait for Google to remove the requirement to provide one
private const val BORDER_AGENT_ID = "0000000000000001"
}
override fun appSupportsThread(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
override suspend fun coreSupportsThread(serverId: Int): Boolean {
if (!serverManager.isRegistered() || serverManager.getServer(serverId) == null) return false
val config = serverManager.webSocketRepository(serverId).getConfig()
return config != null &&
config.components.contains("thread") &&
HomeAssistantVersion.fromString(config.version)?.isAtLeast(2023, 3, 0) == true
}
override suspend fun syncPreferredDataset(
context: Context,
serverId: Int,
scope: CoroutineScope
): IntentSender? {
if (!appSupportsThread() || !coreSupportsThread(serverId)) return null
val getDeviceDataset = scope.async { getPreferredDatasetFromDevice(context) }
val getCoreDataset = scope.async { getPreferredDatasetFromServer(serverId) }
val deviceThreadIntent = getDeviceDataset.await()
val coreThreadDataset = getCoreDataset.await()
if (deviceThreadIntent == null && coreThreadDataset != null) {
try {
importDatasetFromServer(context, coreThreadDataset.datasetId, serverId)
Log.d(TAG, "Thread import to device completed")
} catch (e: Exception) {
Log.e(TAG, "Thread import to device failed", e)
}
} else if (deviceThreadIntent != null && coreThreadDataset == null) {
Log.d(TAG, "Thread export is ready")
return deviceThreadIntent
} // else if device and core both have or don't have datasets, continue
return null
}
override suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse? {
val datasets = serverManager.webSocketRepository(serverId).getThreadDatasets()
return datasets?.firstOrNull { it.preferred }
}
override suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int) {
val tlv = serverManager.webSocketRepository(serverId).getThreadDatasetTlv(datasetId)?.tlvAsByteArray
if (tlv != null) {
val threadBorderAgent = ThreadBorderAgent.newBuilder(BORDER_AGENT_ID.toByteArray()).build()
val threadNetworkCredentials = ThreadNetworkCredentials.fromActiveOperationalDataset(tlv)
suspendCoroutine { cont ->
ThreadNetwork.getClient(context).addCredentials(threadBorderAgent, threadNetworkCredentials)
.addOnSuccessListener { cont.resume(Unit) }
.addOnFailureListener { cont.resumeWithException(it) }
}
}
}
override suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender? = suspendCoroutine { cont ->
if (appSupportsThread()) {
ThreadNetwork.getClient(context)
.preferredCredentials
.addOnSuccessListener { cont.resume(it.intentSender) }
.addOnFailureListener { cont.resumeWithException(it) }
} else {
cont.resumeWithException(IllegalStateException("Thread is not supported on SDK <27"))
}
}
override suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int) {
if (result.resultCode == Activity.RESULT_OK && coreSupportsThread(serverId)) {
val threadNetworkCredentials = ThreadNetworkCredentials.fromIntentSenderResultData(result.data!!)
try {
serverManager.webSocketRepository(serverId).addThreadDataset(threadNetworkCredentials.activeOperationalDataset)
} catch (e: Exception) {
Log.e(TAG, "Error while executing server new Thread credentials request", e)
}
}
}
}

View file

@ -3,6 +3,7 @@ package io.homeassistant.companion.android.matter
enum class MatterFrontendCommissioningStatus {
NOT_STARTED,
REQUESTED,
THREAD_EXPORT_TO_SERVER,
IN_PROGRESS,
ERROR
}

View file

@ -29,13 +29,13 @@ interface MatterManager {
/**
* Send a request to the server to add a Matter device to the network and commission it
* @return `true` if the request was successful
* @return [MatterCommissionResponse], or `null` if it wasn't possible to complete the request
*/
suspend fun commissionDevice(code: String, serverId: Int): MatterCommissionResponse?
/**
* Send a request to the server to commission an "on network" Matter device
* @return `true` if the request was successful
* @return [MatterCommissionResponse], or `null` if it wasn't possible to complete the request
*/
suspend fun commissionOnNetworkDevice(pin: Long, serverId: Int): MatterCommissionResponse?
}

View file

@ -0,0 +1,60 @@
package io.homeassistant.companion.android.thread
import android.content.Context
import android.content.IntentSender
import androidx.activity.result.ActivityResult
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
import kotlinx.coroutines.CoroutineScope
interface ThreadManager {
/**
* Indicates if the app on this device supports Thread credential management.
*/
fun appSupportsThread(): Boolean
/**
* Indicates if the server supports Thread credential management.
*/
suspend fun coreSupportsThread(serverId: Int): Boolean
/**
* Try to sync the preferred Thread dataset with the device and server. If one has a preferred
* dataset while the other one doesn't, it will sync. If both have or don't have preferred
* datasets, skip syncing.
* @return [IntentSender] if permission is required to import the device dataset, `null` if
* syncing completed or there is nothing to sync
*/
suspend fun syncPreferredDataset(
context: Context,
serverId: Int,
scope: CoroutineScope
): IntentSender?
/**
* Get the preferred Thread dataset from the server.
*/
suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse?
/**
* Import a Thread dataset from the server to this device.
* @param datasetId The dataset ID as provided by the server
* @throws Exception if a preferred dataset exists on the server, but it wasn't possible to
* import it
*/
suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int)
/**
* Start a flow to get the preferred Thread dataset from this device to export to the server.
* @return [IntentSender] to ask the user for permission to share the preferred dataset, or
* `null` if there are no datasets to import
* @throws Exception if it is not possible to get the preferred dataset
*/
suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender?
/**
* Process the result from [syncPreferredDataset] or [getPreferredDatasetFromDevice]'s intent
* and add the Thread dataset, if any, to the server.
*/
suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int)
}

View file

@ -0,0 +1,15 @@
package io.homeassistant.companion.android.thread
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class ThreadModule {
@Binds
@Singleton
abstract fun bindThreadManager(threadManager: ThreadManagerImpl): ThreadManager
}

View file

@ -1,7 +1,6 @@
package io.homeassistant.companion.android.webview
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DownloadManager
import android.app.PictureInPictureParams
import android.content.ActivityNotFoundException
@ -164,9 +163,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
mFilePathCallback = null
}
private val commissionMatterDevice = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
// Any errors will have been shown in the UI provided by Play Services
if (result.resultCode == Activity.RESULT_OK) Log.d(TAG, "Matter commissioning returned success")
else Log.d(TAG, "Matter commissioning returned with non-OK code ${result.resultCode}")
presenter.onMatterCommissioningIntentResult(this, result)
}
@Inject
@ -683,6 +680,7 @@ class WebViewActivity : BaseActivity(), io.homeassistant.companion.android.webvi
presenter.getMatterCommissioningStatusFlow().collect {
Log.d(TAG, "Matter commissioning status changed to $it")
when (it) {
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER,
MatterFrontendCommissioningStatus.IN_PROGRESS -> {
presenter.getMatterCommissioningIntent()?.let { intentSender ->
commissionMatterDevice.launch(IntentSenderRequest.Builder(intentSender).build())

View file

@ -2,6 +2,7 @@ package io.homeassistant.companion.android.webview
import android.content.Context
import android.content.IntentSender
import androidx.activity.result.ActivityResult
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
import kotlinx.coroutines.flow.Flow
@ -47,5 +48,6 @@ interface WebViewPresenter {
fun startCommissioningMatterDevice(context: Context)
fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus>
fun getMatterCommissioningIntent(): IntentSender?
fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult)
fun confirmMatterCommissioningError()
}

View file

@ -1,10 +1,12 @@
package io.homeassistant.companion.android.webview
import android.app.Activity
import android.content.Context
import android.content.IntentSender
import android.graphics.Color
import android.net.Uri
import android.util.Log
import androidx.activity.result.ActivityResult
import dagger.hilt.android.qualifiers.ActivityContext
import io.homeassistant.companion.android.common.data.authentication.SessionState
import io.homeassistant.companion.android.common.data.prefs.PrefsRepository
@ -12,6 +14,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.common.util.DisabledLocationHandler
import io.homeassistant.companion.android.matter.MatterFrontendCommissioningStatus
import io.homeassistant.companion.android.matter.MatterManager
import io.homeassistant.companion.android.thread.ThreadManager
import io.homeassistant.companion.android.util.UrlHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -36,7 +39,8 @@ class WebViewPresenterImpl @Inject constructor(
@ActivityContext context: Context,
private val serverManager: ServerManager,
private val prefsRepository: PrefsRepository,
private val matterUseCase: MatterManager
private val matterUseCase: MatterManager,
private val threadUseCase: ThreadManager
) : WebViewPresenter {
companion object {
@ -289,21 +293,33 @@ class WebViewPresenterImpl @Inject constructor(
if (_matterCommissioningStatus.value != MatterFrontendCommissioningStatus.REQUESTED) {
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.REQUESTED)
matterUseCase.startNewCommissioningFlow(
context,
{ intentSender ->
Log.d(TAG, "Matter commissioning is ready")
matterCommissioningIntentSender = intentSender
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.IN_PROGRESS)
},
{ e ->
Log.e(TAG, "Matter commissioning couldn't be prepared", e)
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.ERROR)
mainScope.launch {
val deviceThreadIntent = threadUseCase.syncPreferredDataset(context, serverId, this)
if (deviceThreadIntent != null) {
matterCommissioningIntentSender = deviceThreadIntent
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER)
} else {
startMatterCommissioningFlow(context)
}
)
}
} // else already waiting for a result, don't send another request
}
private fun startMatterCommissioningFlow(context: Context) {
matterUseCase.startNewCommissioningFlow(
context,
{ intentSender ->
Log.d(TAG, "Matter commissioning is ready")
matterCommissioningIntentSender = intentSender
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.IN_PROGRESS)
},
{ e ->
Log.e(TAG, "Matter commissioning couldn't be prepared", e)
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.ERROR)
}
)
}
override fun getMatterCommissioningStatusFlow(): Flow<MatterFrontendCommissioningStatus> =
_matterCommissioningStatus.asStateFlow()
@ -313,6 +329,22 @@ class WebViewPresenterImpl @Inject constructor(
return intent
}
override fun onMatterCommissioningIntentResult(context: Context, result: ActivityResult) {
when (_matterCommissioningStatus.value) {
MatterFrontendCommissioningStatus.THREAD_EXPORT_TO_SERVER -> {
mainScope.launch {
threadUseCase.sendThreadDatasetExportResult(result, serverId)
startMatterCommissioningFlow(context)
}
}
else -> {
// Any errors will have been shown in the UI provided by Play Services
if (result.resultCode == Activity.RESULT_OK) Log.d(TAG, "Matter commissioning returned success")
else Log.d(TAG, "Matter commissioning returned with non-OK code ${result.resultCode}")
}
}
}
override fun confirmMatterCommissioningError() {
_matterCommissioningStatus.tryEmit(MatterFrontendCommissioningStatus.NOT_STARTED)
}

View file

@ -0,0 +1,34 @@
package io.homeassistant.companion.android.thread
import android.content.Context
import android.content.IntentSender
import androidx.activity.result.ActivityResult
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
import kotlinx.coroutines.CoroutineScope
import javax.inject.Inject
class ThreadManagerImpl @Inject constructor() : ThreadManager {
// Thread support currently depends on Google Play Services,
// and as a result Thread is not supported with the minimal flavor
override fun appSupportsThread(): Boolean = false
override suspend fun coreSupportsThread(serverId: Int): Boolean = false
override suspend fun syncPreferredDataset(
context: Context,
serverId: Int,
scope: CoroutineScope
): IntentSender? = null
override suspend fun getPreferredDatasetFromServer(serverId: Int): ThreadDatasetResponse? = null
override suspend fun importDatasetFromServer(context: Context, datasetId: String, serverId: Int) { }
override suspend fun getPreferredDatasetFromDevice(context: Context): IntentSender? {
throw IllegalStateException("Thread is not supported with the minimal flavor")
}
override suspend fun sendThreadDatasetExportResult(result: ActivityResult, serverId: Int) { }
}

View file

@ -16,6 +16,8 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ge
import io.homeassistant.companion.android.common.data.websocket.impl.entities.MatterCommissionResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent
import kotlinx.coroutines.flow.Flow
@ -41,6 +43,9 @@ interface WebSocketRepository {
suspend fun ackNotification(confirmId: String): Boolean
suspend fun commissionMatterDevice(code: String): MatterCommissionResponse?
suspend fun commissionMatterDeviceOnNetwork(pin: Long): MatterCommissionResponse?
suspend fun getThreadDatasets(): List<ThreadDatasetResponse>?
suspend fun getThreadDatasetTlv(datasetId: String): ThreadDatasetTlvResponse?
suspend fun addThreadDataset(tlv: ByteArray): Boolean
suspend fun getConversation(speech: String): ConversationResponse?
}

View file

@ -36,7 +36,10 @@ import io.homeassistant.companion.android.common.data.websocket.impl.entities.Ma
import io.homeassistant.companion.android.common.data.websocket.impl.entities.SocketResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.StateChangedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TemplateUpdatedEvent
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.ThreadDatasetTlvResponse
import io.homeassistant.companion.android.common.data.websocket.impl.entities.TriggerEvent
import io.homeassistant.companion.android.common.util.toHexString
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -394,6 +397,52 @@ class WebSocketRepositoryImpl @AssistedInject constructor(
}
}
/**
* Return a list of all Thread datasets known to the server.
* @return List with [ThreadDatasetResponse]s, or `null` if not an admin or no response.
*/
override suspend fun getThreadDatasets(): List<ThreadDatasetResponse>? {
val response = sendMessage(
mapOf(
"type" to "thread/list_datasets"
)
)
return if (response?.success == true && response.result?.contains("datasets") == true) {
mapper.convertValue(response.result["datasets"]!!)
} else null
}
/**
* Return the TLV value for a dataset.
* @return [ThreadDatasetTlvResponse] for the Thread dataset, or `null` if not found, not an
* admin or no response.
*/
override suspend fun getThreadDatasetTlv(datasetId: String): ThreadDatasetTlvResponse? {
val response = sendMessage(
mapOf(
"type" to "thread/get_dataset_tlv",
"dataset_id" to datasetId
)
)
return mapResponse(response)
}
/**
* Add a new set of Thread network credentials to the server.
* @return `true` if the server indicated success
*/
override suspend fun addThreadDataset(tlv: ByteArray): Boolean {
val response = sendMessage(
mapOf(
"type" to "thread/add_dataset_tlv",
"source" to "Google",
"tlv" to tlv.toHexString()
)
)
return response?.success == true
}
private suspend fun connect(): Boolean {
connectedMutex.withLock {
if (connection != null && connected.isCompleted) {

View file

@ -0,0 +1,13 @@
package io.homeassistant.companion.android.common.data.websocket.impl.entities
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class ThreadDatasetResponse(
val datasetId: String,
val extendedPanId: String,
val networkName: String,
val panId: String,
val preferred: Boolean,
val source: String
)

View file

@ -0,0 +1,8 @@
package io.homeassistant.companion.android.common.data.websocket.impl.entities
data class ThreadDatasetTlvResponse(
val tlv: String
) {
val tlvAsByteArray: ByteArray
get() = tlv.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}

View file

@ -0,0 +1,15 @@
package io.homeassistant.companion.android.common.util
import okhttp3.internal.and
private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
fun ByteArray.toHexString(): String { // From https://stackoverflow.com/a/9855338/4214819
val hexChars = CharArray(this.size * 2)
for (j in 0 until this.size) {
val v = get(j) and 0xFF
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
}
return String(hexChars)
}