From c7f630ff05973f65d6f783dbdfdfc8965c54c9c9 Mon Sep 17 00:00:00 2001 From: "Kim A. Betti" Date: Wed, 14 Feb 2024 10:00:49 +0100 Subject: [PATCH] Add support for additional thing state like online and vendor (#260) --- .../src/main/proto/adapter.proto | 40 ++++++++++++++ .../adapter/protobuf/ProtobufParser.kt | 52 +++++++++++++++++-- .../adapter/protobuf/ProtobufSerializer.kt | 42 +++++++++++++++ .../oslonokkelen/adapter/thing/ThingState.kt | 38 ++++++++++++++ build.gradle.kts | 6 +-- 5 files changed, 170 insertions(+), 8 deletions(-) diff --git a/adapter-protobuf-java/src/main/proto/adapter.proto b/adapter-protobuf-java/src/main/proto/adapter.proto index f6e2202..fc9da5a 100644 --- a/adapter-protobuf-java/src/main/proto/adapter.proto +++ b/adapter-protobuf-java/src/main/proto/adapter.proto @@ -305,6 +305,10 @@ message TextContent { // locked, unlocked, open, close, offline, online etc. Implementing this can // help administrators of Oslonøkkelen to configure alerts when things go // offline, remain open too long etc. +// +// Not all devices will support reporting all this information. Just add those +// your device supports. Example: Don't send "online = false" if your device +// does not support online/offline reporting. message ThingState { // ISO-8601 @@ -330,6 +334,15 @@ message ThingState { // be communicated to Oslonøkkelen DebugLog debug_log = 7; + // Information about the device like vendor and model. + DeviceInfo device_type = 8; + + // Is the device online right now? When was the last time it was seen? + Online online = 9; + + // Rough indicator of battery health + BatteryStatus battery_status = 10; + } reserved 4; @@ -364,6 +377,33 @@ message ThingState { } + // All fields are optional, but make sure you include at least + // one of the fields. If your device does not support either of these + // fields you should omit the DeviceInfo from your manifest. + message DeviceInfo { + string vendor = 1; + string model = 2; + string firmware_version = 3; + } + + enum BatteryStatus { + + // The battery should have been replaced already + EMPTY = 0; + + // No need to change the battery + GOOD = 1; + + // Battery should be replaced as soon as possible + POOR = 2; + } + + message Online { + bool is_online = 1; + + // ISO-8601 + string last_seen = 2; + } // Used to communicate a few lines of technical logs to Oslonøkkelen. message DebugLog { diff --git a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufParser.kt b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufParser.kt index 897bd5e..a66ed01 100644 --- a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufParser.kt +++ b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufParser.kt @@ -9,6 +9,10 @@ import com.github.oslokommune.oslonokkelen.adapter.error.ErrorCodeDescription import com.github.oslokommune.oslonokkelen.adapter.error.ErrorCodes import com.github.oslokommune.oslonokkelen.adapter.manifest.ManifestSnapshot import com.github.oslokommune.oslonokkelen.adapter.proto.Adapter +import com.github.oslokommune.oslonokkelen.adapter.proto.Adapter.ThingState.BatteryStatus.EMPTY +import com.github.oslokommune.oslonokkelen.adapter.proto.Adapter.ThingState.BatteryStatus.GOOD +import com.github.oslokommune.oslonokkelen.adapter.proto.Adapter.ThingState.BatteryStatus.POOR +import com.github.oslokommune.oslonokkelen.adapter.proto.Adapter.ThingState.BatteryStatus.UNRECOGNIZED import com.github.oslokommune.oslonokkelen.adapter.thing.ThingDescription import com.github.oslokommune.oslonokkelen.adapter.thing.ThingId import com.github.oslokommune.oslonokkelen.adapter.thing.ThingState @@ -30,7 +34,7 @@ object ProtobufParser { private val log: Logger = LoggerFactory.getLogger(ProtobufParser::class.java) - fun parseActionRequestFromClaims(verifiedClaims: JWTClaimsSet) : Adapter.ActionRequest { + fun parseActionRequestFromClaims(verifiedClaims: JWTClaimsSet): Adapter.ActionRequest { val requestClaim = verifiedClaims.getJSONObjectClaim("request") val requestBuilder = Adapter.ActionRequest.newBuilder() .setRequestId(requireString(requestClaim, "requestId")) @@ -46,11 +50,14 @@ object ProtobufParser { val value = attachment.values.firstOrNull() as Map<*, *> Adapter.Attachment.newBuilder() - .setNorwegianFodselsnummer(Adapter.Attachment.NorwegianFodselsnummer.newBuilder() - .setNumber(requireString(value, "number")) - .build()) + .setNorwegianFodselsnummer( + Adapter.Attachment.NorwegianFodselsnummer.newBuilder() + .setNumber(requireString(value, "number")) + .build() + ) .build() } + else -> { null } @@ -82,7 +89,7 @@ object ProtobufParser { return value ?: throw missingKey(key, requestClaim) } - private fun missingKey(key: String,requestClaim: Map<*, *>): IllegalStateException { + private fun missingKey(key: String, requestClaim: Map<*, *>): IllegalStateException { return IllegalStateException("Missing key $key (found: ${requestClaim.keys.joinToString(", ")})") } @@ -240,6 +247,41 @@ object ProtobufParser { ) } + Adapter.ThingState.ValueCase.DEVICE_TYPE -> { + ThingState.DeviceType( + thingId = thing.id, + timestamp = lastUpdate, + vendor = serializedState.deviceType.vendor.ifBlank { null }, + model = serializedState.deviceType.model.ifBlank { null }, + firmwareVersion = serializedState.deviceType.firmwareVersion.ifBlank { null } + ) + } + + Adapter.ThingState.ValueCase.ONLINE -> { + ThingState.Online( + thingId = thing.id, + timestamp = lastUpdate, + online = serializedState.online.isOnline, + lastSeen = if (serializedState.online.lastSeen.isNotBlank()) { + Instant.parse(serializedState.online.lastSeen) + } else { + null + } + ) + } + + Adapter.ThingState.ValueCase.BATTERY_STATUS -> { + ThingState.BatteryStatus( + thingId = thing.id, + timestamp = lastUpdate, + state = when (serializedState.batteryStatus) { + GOOD -> ThingState.BatteryStatus.State.GOOD + POOR -> ThingState.BatteryStatus.State.POOR + EMPTY, UNRECOGNIZED, null -> ThingState.BatteryStatus.State.EMPTY + } + ) + } + Adapter.ThingState.ValueCase.VALUE_NOT_SET, null -> { log.warn("Unsupported thing state detected: {}", serializedState.valueCase) null diff --git a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufSerializer.kt b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufSerializer.kt index b40ea41..8f25444 100644 --- a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufSerializer.kt +++ b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/protobuf/ProtobufSerializer.kt @@ -193,6 +193,48 @@ object ProtobufSerializer { ) .build() } + + is ThingState.BatteryStatus -> { + Adapter.ThingState.newBuilder() + .setLastUpdate(state.timestamp.toString()) + .setBatteryStatus(when (state.state) { + ThingState.BatteryStatus.State.EMPTY -> Adapter.ThingState.BatteryStatus.EMPTY + ThingState.BatteryStatus.State.GOOD -> Adapter.ThingState.BatteryStatus.GOOD + ThingState.BatteryStatus.State.POOR -> Adapter.ThingState.BatteryStatus.POOR + }) + .build() + } + is ThingState.DeviceType -> { + val deviceInfoBuilder = Adapter.ThingState.DeviceInfo.newBuilder() + + if (state.vendor != null) { + deviceInfoBuilder.setVendor(state.vendor) + } + if (state.model != null) { + deviceInfoBuilder.setModel(state.model) + } + if (state.firmwareVersion != null) { + deviceInfoBuilder.setFirmwareVersion(state.firmwareVersion) + } + + Adapter.ThingState.newBuilder() + .setLastUpdate(state.timestamp.toString()) + .setDeviceType(deviceInfoBuilder.build()) + .build() + } + is ThingState.Online -> { + val onlineBuilder = Adapter.ThingState.Online.newBuilder() + onlineBuilder.setIsOnline(state.online) + + if (state.lastSeen != null) { + onlineBuilder.setLastSeen(state.lastSeen.toString()) + } + + Adapter.ThingState.newBuilder() + .setLastUpdate(state.timestamp.toString()) + .setOnline(onlineBuilder.build()) + .build() + } } } ?: emptyList() } diff --git a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/thing/ThingState.kt b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/thing/ThingState.kt index 5d16ec9..f3197e8 100644 --- a/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/thing/ThingState.kt +++ b/adapter-sdk-kotlin/src/main/kotlin/com/github/oslokommune/oslonokkelen/adapter/thing/ThingState.kt @@ -112,6 +112,44 @@ sealed class ThingState { } + data class BatteryStatus( + override val timestamp: Instant, + override val thingId: ThingId, + val state: State + ) : ThingState() { + + override val key = Key(thingId, "thing.${thingId.value}.battery") + + enum class State { + EMPTY, GOOD, POOR + } + + } + + data class Online( + override val timestamp: Instant, + override val thingId: ThingId, + val lastSeen: Instant?, + val online: Boolean + ) : ThingState() { + + override val key = Key(thingId, "thing.${thingId.value}.online") + + } + + data class DeviceType( + override val timestamp: Instant, + override val thingId: ThingId, + val vendor: String?, + val model: String?, + val firmwareVersion: String? + ) : ThingState() { + + override val key = Key(thingId, "thing.${thingId.value}.online") + + } + + data class Key( val thingId: ThingId, val value: String diff --git a/build.gradle.kts b/build.gradle.kts index ddde31f..82459b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,13 +19,13 @@ allprojects { pluginManager.withPlugin("kotlin") { tasks.withType { - kotlinOptions.jvmTarget = JavaVersion.VERSION_18.toString() + kotlinOptions.jvmTarget = JavaVersion.VERSION_21.toString() } } pluginManager.withPlugin("java") { tasks.withType { - targetCompatibility = JavaVersion.VERSION_18.toString() - sourceCompatibility = JavaVersion.VERSION_18.toString() + targetCompatibility = JavaVersion.VERSION_21.toString() + sourceCompatibility = JavaVersion.VERSION_21.toString() } } }