From 7dcc7cd0efc599c0f5c8bea112cf9b81996e36cc Mon Sep 17 00:00:00 2001 From: paulwinter Date: Tue, 30 Sep 2025 14:09:50 +0200 Subject: [PATCH 1/2] feat: add ChangeZoneMetadataValue command and test (#163) --- .../com/open200/xesar/connect/Topics.kt | 3 + .../connect/extension/XesarConnectZoneExt.kt | 29 +++++ .../command/ChangeZoneMetadataValueMapi.kt | 23 ++++ .../connect/messages/event/ZoneChanged.kt | 3 + .../xesar/connect/messages/query/Zone.kt | 14 +++ .../encodingDecoding/query/ZoneElementTest.kt | 2 +- .../encodingDecoding/query/ZoneListTest.kt | 2 +- .../it/command/ChangeZoneMetadataValueTest.kt | 104 ++++++++++++++++++ .../xesar/connect/util/fixture/ZoneFixture.kt | 1 + 9 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/command/ChangeZoneMetadataValueMapi.kt create mode 100644 xesar-connect/src/test/kotlin/com/open200/xesar/connect/it/command/ChangeZoneMetadataValueTest.kt diff --git a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/Topics.kt b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/Topics.kt index 49f5958d..77133a96 100644 --- a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/Topics.kt +++ b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/Topics.kt @@ -480,6 +480,9 @@ class Topics(vararg val topics: String) { /** MQTT topic string for the "ChangePersonMetadataValueMapi" command. */ val CHANGE_PERSON_METADATA_VALUE = "xs3/1/cmd/ChangePersonMetadataValueMapi" + + /** MQTT topic string for the "ChangeZoneMetadataValueMapi" command. */ + val CHANGE_ZONE_METADATA_VALUE = "xs3/1/cmd/ChangeZoneMetadataValueMapi" } } diff --git a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/extension/XesarConnectZoneExt.kt b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/extension/XesarConnectZoneExt.kt index dc920c4a..bebff88d 100644 --- a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/extension/XesarConnectZoneExt.kt +++ b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/extension/XesarConnectZoneExt.kt @@ -167,6 +167,35 @@ suspend fun XesarConnect.removeInstallationPointFromZoneAsync( ) } +/** + * Changes the value of custom data field of a zone. + * + * @param id The ID of the zone. + * @param metadataId The metadataID of the data field. + * @param value The new value of the field. + * @param requestConfig The request configuration (optional). + */ +suspend fun XesarConnect.changeZoneMetadataValueAsync( + id: UUID, + metadataId: UUID, + value: String, + requestConfig: XesarConnect.RequestConfig = buildRequestConfig(), +): SingleEventResult { + return sendCommandAsync( + Topics.Command.CHANGE_ZONE_METADATA_VALUE, + Topics.Event.ZONE_CHANGED, + true, + ChangeZoneMetadataValueMapi( + config.uuidGenerator.generateId(), + id, + metadataId, + value, + token, + ), + requestConfig, + ) +} + /** * Retrieves a cold stream of [Zone] objects, fetching them incrementally in smaller,more manageable * chunks rather than retrieving the entire dataset at once. Use [Query.Params.pageLimit] to choose diff --git a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/command/ChangeZoneMetadataValueMapi.kt b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/command/ChangeZoneMetadataValueMapi.kt new file mode 100644 index 00000000..022179eb --- /dev/null +++ b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/command/ChangeZoneMetadataValueMapi.kt @@ -0,0 +1,23 @@ +package com.open200.xesar.connect.messages.command + +import com.open200.xesar.connect.utils.UUIDSerializer +import java.util.* +import kotlinx.serialization.Serializable + +/** + * Represents a command POJO to change a custom data field value for a zone. + * + * @param commandId The id of the command. + * @param id The id of the zone. + * @param metadataId The id of the custom data field. + * @param value The new value of the custom data field. + * @param token The token of the command. + */ +@Serializable +data class ChangeZoneMetadataValueMapi( + override val commandId: @Serializable(with = UUIDSerializer::class) UUID, + val id: @Serializable(with = UUIDSerializer::class) UUID, + val metadataId: @Serializable(with = UUIDSerializer::class) UUID, + val value: String, + val token: String, +) : Command diff --git a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/event/ZoneChanged.kt b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/event/ZoneChanged.kt index 380d879d..46a6f485 100644 --- a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/event/ZoneChanged.kt +++ b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/event/ZoneChanged.kt @@ -1,5 +1,6 @@ package com.open200.xesar.connect.messages.event +import com.open200.xesar.connect.messages.EntityMetadata import com.open200.xesar.connect.utils.UUIDSerializer import java.util.* import kotlinx.serialization.Serializable @@ -10,10 +11,12 @@ import kotlinx.serialization.Serializable * @param name The name of the zone. * @param description The description of the zone. * @param id The id of the zone. + * @param entityMetadata Contains the information for all defined custom data fields for the zone. */ @Serializable data class ZoneChanged( val name: String, val description: String, @Serializable(with = UUIDSerializer::class) val id: UUID, + val entityMetadata: List? = null, ) : Event diff --git a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/query/Zone.kt b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/query/Zone.kt index 3df3440d..9e31083f 100644 --- a/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/query/Zone.kt +++ b/xesar-connect/src/main/kotlin/com/open200/xesar/connect/messages/query/Zone.kt @@ -1,9 +1,22 @@ package com.open200.xesar.connect.messages.query +import com.open200.xesar.connect.messages.EntityMetadata import com.open200.xesar.connect.utils.UUIDSerializer import java.util.* import kotlinx.serialization.Serializable +/** + * Represents a zone. + * + * @param installationPoints The list of installation points of the zone (optional). + * @param partitionId The partition identifier of the zone. + * @param installationPointCount The installationPointCount of the zone (optional). + * @param name The name of the zone. + * @param description The description of the zone (optional). + * @param id The id of the zone. + * @param entityMetadata Contains the information for all defined custom data fields for the zone + * (optional). + */ @Serializable data class Zone( val installationPoints: List<@Serializable(with = UUIDSerializer::class) UUID> = emptyList(), @@ -12,6 +25,7 @@ data class Zone( val name: String, val description: String? = null, @Serializable(with = UUIDSerializer::class) val id: UUID, + val entityMetadata: List? = null, ) : QueryListResource, QueryElementResource { companion object { diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt index 2f4ff5cd..4e512cc5 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt @@ -16,7 +16,7 @@ class ZoneElementTest : ) val zoneString = - "{\"requestId\":\"d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6\",\"response\":{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\"}}" + "{\"requestId\":\"d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6\",\"response\":{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[]}}" test("encoding QueryElement for a zone") { val zoneEncoded = encodeQueryElement(zone) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt index 0c04c38f..156b1dc3 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt @@ -33,7 +33,7 @@ class ZoneListTest : ) val zoneString = - "{\"requestId\":\"00000000-1281-40ae-89d7-5c541d77a757\",\"response\":{\"data\":[{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\"},{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\",\"f6a5bdf2-7c7d-11ee-b962-0242ac120002\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"a4c838a8-f6be-49e0-abee-c1d3b2897279\"}],\"totalCount\":2,\"filterCount\":2}}" + "{\"requestId\":\"00000000-1281-40ae-89d7-5c541d77a757\",\"response\":{\"data\":[{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[]},{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\",\"f6a5bdf2-7c7d-11ee-b962-0242ac120002\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"a4c838a8-f6be-49e0-abee-c1d3b2897279\",\"entityMetadata\":[]}],\"totalCount\":2,\"filterCount\":2}}" test("encoding QueryList for a list of zones") { val zoneEncoded = encodeQueryList(zoneList) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/it/command/ChangeZoneMetadataValueTest.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/it/command/ChangeZoneMetadataValueTest.kt new file mode 100644 index 00000000..a0bfd903 --- /dev/null +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/it/command/ChangeZoneMetadataValueTest.kt @@ -0,0 +1,104 @@ +package com.open200.xesar.connect.it.command + +import com.open200.xesar.connect.Topics +import com.open200.xesar.connect.XesarConnect +import com.open200.xesar.connect.XesarMqttClient +import com.open200.xesar.connect.extension.changeZoneMetadataValueAsync +import com.open200.xesar.connect.it.MosquittoContainer +import com.open200.xesar.connect.messages.EntityMetadata +import com.open200.xesar.connect.messages.event.ApiEvent +import com.open200.xesar.connect.messages.event.ZoneChanged +import com.open200.xesar.connect.messages.event.encodeEvent +import io.kotest.common.runBlocking +import io.kotest.core.spec.style.FunSpec +import io.kotest.extensions.testcontainers.perProject +import io.kotest.matchers.equals.shouldBeEqual +import io.mockk.coEvery +import java.util.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch + +class ChangeZoneMetadataValueTest : + FunSpec({ + val container = MosquittoContainer.container() + val config = MosquittoContainer.config(container) + listener(container.perProject()) + + test("change zone metadata value") { + coEvery { config.uuidGenerator.generateId() } + .returns(UUID.fromString("00000000-1281-40ae-89d7-5c541d77a757")) + + val zoneId = UUID.fromString("11111111-2222-3333-4444-555555555555") + val metadataId = UUID.fromString("aaaaaaaa-0000-0000-0000-000000000001") + + runBlocking { + val simulatedBackendReady = CompletableDeferred() + val commandReceived = CompletableDeferred() + + launch { + XesarMqttClient.connectAsync(config).await().use { client -> + client.subscribeAsync(arrayOf(Topics.ALL_TOPICS)).await() + + client.onMessage = { topic, payload -> + when (topic) { + Topics.Command.CHANGE_ZONE_METADATA_VALUE -> { + commandReceived.complete(payload.decodeToString()) + } + } + } + + simulatedBackendReady.complete(Unit) + + val commandContent = commandReceived.await() + + commandContent.shouldBeEqual( + "{\"commandId\":\"00000000-1281-40ae-89d7-5c541d77a757\",\"id\":\"11111111-2222-3333-4444-555555555555\",\"metadataId\":\"aaaaaaaa-0000-0000-0000-000000000001\",\"value\":\"Top Secret\",\"token\":\"JDJhJDEwJDFSNEljZ2FaRUNXUXBTQ25XN05KbE9qRzFHQ1VjMzkvWTBVcFpZb1M4Vmt0dnJYZ0tJVFBx\"}" + ) + + val apiEvent = + ApiEvent( + UUID.fromString("00000000-1281-40ae-89d7-5c541d77a757"), + ZoneChanged( + name = "Test Zone", + description = "Test Description", + id = zoneId, + entityMetadata = + listOf( + EntityMetadata( + id = metadataId, + name = "security clearance required", + value = "Top Secret", + ) + ), + ), + ) + + client + .publishAsync(Topics.Event.ZONE_CHANGED, encodeEvent(apiEvent)) + .await() + } + } + + launch { + simulatedBackendReady.await() + + val api = XesarConnect.connectAndLoginAsync(config).await() + api.subscribeAsync(Topics(Topics.Event.ZONE_CHANGED)).await() + + val result = + api.changeZoneMetadataValueAsync( + id = zoneId, + metadataId = metadataId, + value = "Top Secret", + ) + .await() + + result.id.shouldBeEqual(zoneId) + result.entityMetadata!! + .single { it.id == metadataId } + .value + ?.shouldBeEqual("Top Secret") + } + } + } + }) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt index df71a14d..08809314 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt @@ -13,5 +13,6 @@ object ZoneFixture { description = "zone description", id = UUID.fromString("497f6eca-6276-4993-bfeb-53cbbbba6f08"), installationPoints = listOf(UUID.fromString("7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a")), + entityMetadata = emptyList(), ) } From a0c620c5db665426869e3270132b155cabea0165 Mon Sep 17 00:00:00 2001 From: paulwinter Date: Fri, 10 Oct 2025 12:54:32 +0200 Subject: [PATCH 2/2] test: add an actual list of entityMetadata to the fixture and query tests (#163) --- .../encodingDecoding/query/ZoneElementTest.kt | 2 +- .../encodingDecoding/query/ZoneListTest.kt | 2 +- .../xesar/connect/util/fixture/ZoneFixture.kt | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt index 4e512cc5..3e8782a3 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneElementTest.kt @@ -16,7 +16,7 @@ class ZoneElementTest : ) val zoneString = - "{\"requestId\":\"d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6\",\"response\":{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[]}}" + "{\"requestId\":\"d385ab22-0f51-4b97-9ecd-b8ff3fd4fcb6\",\"response\":{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[{\"id\":\"123e4567-e89b-12d3-a456-426614174000\",\"name\":\"type\",\"value\":\"floor\"},{\"id\":\"0f8fad5b-d9cb-469f-a165-70867728950e\",\"name\":\"city\",\"value\":null}]}}" test("encoding QueryElement for a zone") { val zoneEncoded = encodeQueryElement(zone) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt index 156b1dc3..bcc62db5 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/encodingDecoding/query/ZoneListTest.kt @@ -33,7 +33,7 @@ class ZoneListTest : ) val zoneString = - "{\"requestId\":\"00000000-1281-40ae-89d7-5c541d77a757\",\"response\":{\"data\":[{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[]},{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\",\"f6a5bdf2-7c7d-11ee-b962-0242ac120002\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"a4c838a8-f6be-49e0-abee-c1d3b2897279\",\"entityMetadata\":[]}],\"totalCount\":2,\"filterCount\":2}}" + "{\"requestId\":\"00000000-1281-40ae-89d7-5c541d77a757\",\"response\":{\"data\":[{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"497f6eca-6276-4993-bfeb-53cbbbba6f08\",\"entityMetadata\":[{\"id\":\"123e4567-e89b-12d3-a456-426614174000\",\"name\":\"type\",\"value\":\"floor\"},{\"id\":\"0f8fad5b-d9cb-469f-a165-70867728950e\",\"name\":\"city\",\"value\":null}]},{\"installationPoints\":[\"7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a\",\"f6a5bdf2-7c7d-11ee-b962-0242ac120002\"],\"partitionId\":\"7b4399a0-21ce-4bee-ba43-e06e291248d2\",\"installationPointCount\":0,\"name\":\"zone name\",\"description\":\"zone description\",\"id\":\"a4c838a8-f6be-49e0-abee-c1d3b2897279\",\"entityMetadata\":[{\"id\":\"123e4567-e89b-12d3-a456-426614174000\",\"name\":\"type\",\"value\":\"floor\"},{\"id\":\"0f8fad5b-d9cb-469f-a165-70867728950e\",\"name\":\"city\",\"value\":null}]}],\"totalCount\":2,\"filterCount\":2}}" test("encoding QueryList for a list of zones") { val zoneEncoded = encodeQueryList(zoneList) diff --git a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt index 08809314..6273b9ff 100644 --- a/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt +++ b/xesar-connect/src/test/kotlin/com/open200/xesar/connect/util/fixture/ZoneFixture.kt @@ -1,5 +1,6 @@ package com.open200.xesar.connect.util.fixture +import com.open200.xesar.connect.messages.EntityMetadata import com.open200.xesar.connect.messages.query.Zone import java.util.* @@ -13,6 +14,18 @@ object ZoneFixture { description = "zone description", id = UUID.fromString("497f6eca-6276-4993-bfeb-53cbbbba6f08"), installationPoints = listOf(UUID.fromString("7ca59670-bd30-4ea9-9bd1-2103a9bd2f2a")), - entityMetadata = emptyList(), + entityMetadata = + listOf( + EntityMetadata( + id = UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), + name = "type", + value = "floor", + ), + EntityMetadata( + id = UUID.fromString("0f8fad5b-d9cb-469f-a165-70867728950e"), + name = "city", + value = null, + ), + ), ) }