Breaking change: send sensor list attributes as list to server (#2478)

* Breaking change: send sensor list attributes as list to server

 - Send a sensor attribute that is managed by the app as a list, to the server as a list as well instead of a stringified version of a list.

* Fallback to string for lists that include separator

 - To prevent issues where we can't distinguish the list items separator from a value that includes the separator, don't try to convert a value to a list if that is the case but instead use string

* Improve string fallback for lists that include separator

 - Instead of only falling back when a list includes a String with the separator, just use toString() to also support other objects which might include ", " when stringified, such as nested lists

* Store list data as JSON

 - To remove any confusion when sending individual list items, store data as JSON to escape characters if necessary
 - Map Any data in a list that is not any of the specific types to string, for consistency with the same types when not in a list and to prevent JSON serialization issues
This commit is contained in:
Joris Pelgröm 2022-04-30 13:47:44 +02:00 committed by GitHub
parent d6add75168
commit b418491667
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 51 additions and 5 deletions

View file

@ -130,12 +130,12 @@ class BluetoothSensorManager : SensorManager {
var totalConnectedDevices = 0
var connectedPairedDevices: List<String> = ArrayList()
var connectedNotPairedDevices: List<String> = ArrayList()
var bondedString = ""
var pairedDevices: List<String> = ArrayList()
if (checkPermission(context, bluetoothConnection.id)) {
val bluetoothDevices = BluetoothUtils.getBluetoothDevices(context)
bondedString = bluetoothDevices.filter { b -> b.paired }.map { it.address }.toString()
pairedDevices = bluetoothDevices.filter { b -> b.paired }.map { it.address }
connectedPairedDevices = bluetoothDevices.filter { b -> b.paired && b.connected }.map { it.address }
connectedNotPairedDevices = bluetoothDevices.filter { b -> !b.paired && b.connected }.map { it.address }
totalConnectedDevices = bluetoothDevices.filter { b -> b.connected }.count()
@ -148,7 +148,7 @@ class BluetoothSensorManager : SensorManager {
mapOf(
"connected_paired_devices" to connectedPairedDevices,
"connected_not_paired_devices" to connectedNotPairedDevices,
"paired_devices" to bondedString
"paired_devices" to pairedDevices
)
)
}

View file

@ -55,6 +55,8 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
@ -78,6 +80,7 @@ fun SensorDetailView(
) {
val context = LocalContext.current
var sensorUpdateTypeInfo by remember { mutableStateOf(false) }
val jsonMapper by lazy { jacksonObjectMapper() }
val sensorEnabled = viewModel.sensor?.sensor?.enabled
?: (
@ -145,9 +148,17 @@ fun SensorDetailView(
}
sensor.attributes.forEach { attribute ->
item {
val summary = when (attribute.valueType) {
"listboolean" -> jsonMapper.readValue<List<Boolean>>(attribute.value).toString()
"listfloat" -> jsonMapper.readValue<List<Number>>(attribute.value).toString()
"listlong" -> jsonMapper.readValue<List<Long>>(attribute.value).toString()
"listint" -> jsonMapper.readValue<List<Int>>(attribute.value).toString()
"liststring" -> jsonMapper.readValue<List<String>>(attribute.value).toString()
else -> attribute.value
}
SensorDetailRow(
title = attribute.name,
summary = attribute.value,
summary = summary,
clickable = false,
selectingEnabled = true
)

View file

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.os.Process.myPid
import android.os.Process.myUid
import androidx.core.content.getSystemService
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.homeassistant.companion.android.database.AppDatabase
import io.homeassistant.companion.android.database.sensor.Attribute
import io.homeassistant.companion.android.database.sensor.SensorSetting
@ -215,13 +216,31 @@ interface SensorManager {
is Int -> "int"
is Long -> "long"
is Number -> "float"
is List<*> -> {
when {
(item.value as List<*>).all { it is Boolean } -> "listboolean"
(item.value as List<*>).all { it is Int } -> "listint"
(item.value as List<*>).all { it is Long } -> "listlong"
(item.value as List<*>).all { it is Number } -> "listfloat"
else -> "liststring"
}
}
else -> "string" // Always default to String for attributes
}
val value =
when {
valueType == "liststring" ->
jacksonObjectMapper().writeValueAsString((item.value as List<*>).map { it.toString() })
valueType.startsWith("list") ->
jacksonObjectMapper().writeValueAsString(item.value)
else ->
item.value.toString()
}
Attribute(
basicSensor.id,
item.key,
item.value.toString(),
value,
valueType
)
}

View file

@ -2,6 +2,9 @@ package io.homeassistant.companion.android.database.sensor
import androidx.room.Embedded
import androidx.room.Relation
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.homeassistant.companion.android.common.data.integration.SensorRegistration
data class SensorWithAttributes(
@ -14,8 +17,21 @@ data class SensorWithAttributes(
val attributes: List<Attribute>
) {
fun toSensorRegistration(): SensorRegistration<Any> {
var objectMapper: ObjectMapper? = null
val attributes = attributes.map {
val attributeValue = when (it.valueType) {
"listboolean", "listfloat", "listlong", "listint", "liststring" -> {
if (objectMapper == null) objectMapper = jacksonObjectMapper()
objectMapper?.let { mapper ->
when (it.valueType) {
"listboolean" -> mapper.readValue<List<Boolean>>(it.value)
"listfloat" -> mapper.readValue<List<Number>>(it.value)
"listlong" -> mapper.readValue<List<Long>>(it.value)
"listint" -> mapper.readValue<List<Int>>(it.value)
else -> mapper.readValue<List<String>>(it.value)
}
} ?: it.value // Fallback: provide JSON string, but shouldn't happen
}
"boolean" -> it.value.toBoolean()
"float" -> it.value.toFloat()
"long" -> it.value.toLong()