From dc610bc7885844ec73a2322848c7fc3a0cc703c0 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:15:15 +0200 Subject: [PATCH] Advertise uint32 format for valve duration characteristics (#202) SetDurationCharacteristic (D3) and RemainingDurationCharacteristic (D4) serialized their metadata with "format":"int", but the HAP spec defines both as UInt32. Add a protected IntegerCharacteristic constructor that takes the HAP format and use it for the two duration characteristics. All other integer characteristics keep advertising "int". Add regression tests for the D3/D4 formats and for an unchanged int-format characteristic. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 3 + .../impl/base/IntegerCharacteristic.java | 49 +++++++++++++++- .../RemainingDurationCharacteristic.java | 1 + .../impl/valve/SetDurationCharacteristic.java | 1 + .../DurationCharacteristicFormatTest.java | 56 +++++++++++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/test/java/io/github/hapjava/characteristics/impl/valve/DurationCharacteristicFormatTest.java diff --git a/CHANGES.md b/CHANGES.md index 8da353402..4997cae9d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.9 +* Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202) + # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 * Updated netty to 4.1.126.Final diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/IntegerCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/IntegerCharacteristic.java index 96448eea8..6b6cd2982 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/IntegerCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/IntegerCharacteristic.java @@ -47,8 +47,55 @@ public IntegerCharacteristic( Optional> setter, Optional> subscriber, Optional unsubscriber) { + this( + type, + "int", + description, + minValue, + maxValue, + unit, + getter, + setter, + subscriber, + unsubscriber); + } + + /** + * Constructor for characteristics whose HAP format differs from the default "int", e.g. "uint8", + * "uint16" or "uint32". Values are still handled as Java {@link Integer}, so the supported range + * must fit into a signed 32-bit integer. + * + * @param type a string containing a UUID that indicates the type of characteristic. Apple defines + * a set of these, however implementors can create their own as well. + * @param format the HAP format to advertise, e.g. "int" or "uint32". + * @param description a description of the characteristic to be passed to the consuming device. + * @param minValue the minimum supported value. + * @param maxValue the maximum supported value + * @param unit a description of the unit this characteristic supports. + * @param getter getter for value + * @param setter setter for value + * @param subscriber subscribers to changes + * @param unsubscriber unsubscribers to changes + */ + protected IntegerCharacteristic( + String type, + String format, + String description, + int minValue, + int maxValue, + String unit, + Optional>> getter, + Optional> setter, + Optional> subscriber, + Optional unsubscriber) { super( - type, "int", description, getter.isPresent(), setter.isPresent(), subscriber, unsubscriber); + type, + format, + description, + getter.isPresent(), + setter.isPresent(), + subscriber, + unsubscriber); this.minValue = minValue; this.maxValue = maxValue; this.unit = unit; diff --git a/src/main/java/io/github/hapjava/characteristics/impl/valve/RemainingDurationCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/valve/RemainingDurationCharacteristic.java index 828fafab1..29409027a 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/valve/RemainingDurationCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/valve/RemainingDurationCharacteristic.java @@ -26,6 +26,7 @@ public RemainingDurationCharacteristic( Runnable unsubscriber) { super( "000000D4-0000-1000-8000-0026BB765291", + "uint32", "remaining duration", minValue, maxValue, diff --git a/src/main/java/io/github/hapjava/characteristics/impl/valve/SetDurationCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/valve/SetDurationCharacteristic.java index 59cef5646..109a7bb6e 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/valve/SetDurationCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/valve/SetDurationCharacteristic.java @@ -24,6 +24,7 @@ public SetDurationCharacteristic( Runnable unsubscriber) { super( "000000D3-0000-1000-8000-0026BB765291", + "uint32", "set duration", minValue, maxValue, diff --git a/src/test/java/io/github/hapjava/characteristics/impl/valve/DurationCharacteristicFormatTest.java b/src/test/java/io/github/hapjava/characteristics/impl/valve/DurationCharacteristicFormatTest.java new file mode 100644 index 000000000..2e12b2daa --- /dev/null +++ b/src/test/java/io/github/hapjava/characteristics/impl/valve/DurationCharacteristicFormatTest.java @@ -0,0 +1,56 @@ +package io.github.hapjava.characteristics.impl.valve; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.hapjava.characteristics.impl.windowcovering.CurrentPositionCharacteristic; +import java.util.concurrent.CompletableFuture; +import javax.json.JsonObject; +import org.junit.Test; + +/** Regression tests for https://github.com/hap-java/HAP-Java/issues/202 */ +public class DurationCharacteristicFormatTest { + + @Test + public void setDurationAdvertisesUint32() { + SetDurationCharacteristic characteristic = + new SetDurationCharacteristic( + () -> CompletableFuture.completedFuture(300), value -> {}, callback -> {}, () -> {}); + + JsonObject json = characteristic.toJson(12).join(); + + assertThat(json.getString("type")).isEqualTo("D3"); + assertThat(json.getString("format")).isEqualTo("uint32"); + assertThat(json.getInt("value")).isEqualTo(300); + assertThat(json.getInt("minValue")).isEqualTo(0); + assertThat(json.getInt("maxValue")).isEqualTo(3600); + assertThat(json.getInt("minStep")).isEqualTo(1); + assertThat(json.getString("unit")).isEqualTo("s"); + } + + @Test + public void remainingDurationAdvertisesUint32() { + RemainingDurationCharacteristic characteristic = + new RemainingDurationCharacteristic( + () -> CompletableFuture.completedFuture(180), callback -> {}, () -> {}); + + JsonObject json = characteristic.toJson(13).join(); + + assertThat(json.getString("type")).isEqualTo("D4"); + assertThat(json.getString("format")).isEqualTo("uint32"); + assertThat(json.getInt("value")).isEqualTo(180); + assertThat(json.getInt("minValue")).isEqualTo(0); + assertThat(json.getInt("maxValue")).isEqualTo(3600); + assertThat(json.getString("unit")).isEqualTo("s"); + } + + @Test + public void otherIntegerCharacteristicsKeepIntFormat() { + CurrentPositionCharacteristic characteristic = + new CurrentPositionCharacteristic( + () -> CompletableFuture.completedFuture(50), callback -> {}, () -> {}); + + JsonObject json = characteristic.toJson(14).join(); + + assertThat(json.getString("format")).isEqualTo("int"); + } +}