From b2d378f2b075202cde991b968f87e2eca5abd56c Mon Sep 17 00:00:00 2001 From: joenicorata <44491399+joenicorata@users.noreply.github.com> Date: Thu, 2 Mar 2023 13:57:45 -0600 Subject: [PATCH 01/26] TelevisionAccessory Related Fixes (#185) * RemoteKeyEnum code for Info button is incorrect. Release version shows code 115, actual code is 15. Tested on IOS 16 and 15. * Add support for accessory category on standalone server. previously category was implemented for a bridge, but not for standalone accessories. --- .../impl/television/RemoteKeyEnum.java | 2 +- .../hapjava/server/impl/HomekitServer.java | 9 ++++++++ .../HomekitStandaloneAccessoryServer.java | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java index 443c23644..d62298b84 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java @@ -18,7 +18,7 @@ public enum RemoteKeyEnum implements CharacteristicEnum { BACK(9), EXIT(10), PLAY_PAUSE(11), - INFO(115); + INFO(15); private static final Map reverse; diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java index af129a7a8..fc8ecaf76 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java @@ -117,6 +117,15 @@ public HomekitStandaloneAccessoryServer createStandaloneAccessory( return new HomekitStandaloneAccessoryServer(accessory, http, localAddress, authInfo); } } + public HomekitStandaloneAccessoryServer createStandaloneAccessory( + HomekitAuthInfo authInfo, HomekitAccessory accessory, int category) + throws IOException, ExecutionException, InterruptedException { + if (jmdns != null) { + return new HomekitStandaloneAccessoryServer(accessory, http, jmdns, authInfo, category); + } else { + return new HomekitStandaloneAccessoryServer(accessory, http, localAddress, authInfo, category); + } + } /** * Creates a bridge accessory, capable of holding multiple child accessories. This has the diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java index 5eecfe45c..99ff7b746 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java @@ -40,6 +40,28 @@ public class HomekitStandaloneAccessoryServer { root = new HomekitRoot(accessory.getName().get(), webHandler, jmdns, authInfo); root.addAccessory(accessory); } + + HomekitStandaloneAccessoryServer( + HomekitAccessory accessory, + HomekitWebHandler webHandler, + InetAddress localhost, + HomekitAuthInfo authInfo, + int category) + throws UnknownHostException, IOException, ExecutionException, InterruptedException { + root = new HomekitRoot(accessory.getName().get(), category, webHandler, localhost, authInfo); + root.addAccessory(accessory); + } + + HomekitStandaloneAccessoryServer( + HomekitAccessory accessory, + HomekitWebHandler webHandler, + JmDNS jmdns, + HomekitAuthInfo authInfo, + int category) + throws UnknownHostException, IOException, ExecutionException, InterruptedException { + root = new HomekitRoot(accessory.getName().get(), category, webHandler, jmdns, authInfo); + root.addAccessory(accessory); + } /** Begins advertising and handling requests for this accessory. */ public void start() { From 1b20ab04dc310756ee37ab74863d57b8c58e3948 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 2 Mar 2023 13:00:40 -0700 Subject: [PATCH 02/26] attempt to fix error in release github action --- .github/workflows/release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 804e4b2bd..48b5ee7cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,9 @@ on: description: Version to release required: true +permissions: + contents: write + jobs: build: @@ -17,6 +20,7 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: git-actions/set-user@v1 - name: Set up JDK 11 uses: actions/setup-java@v3 with: @@ -28,10 +32,6 @@ jobs: gpg-passphrase: MAVEN_GPG_PASSPHRASE server-username: MAVEN_USERNAME server-password: MAVEN_CENTRAL_TOKEN - - name: Setup git profile - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Maven release run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -Possrh env: From 50cd0d689adbffa165bb40f39a6d321cffb2aff9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Mar 2023 20:03:15 +0000 Subject: [PATCH 03/26] [maven-release-plugin] prepare release hap-2.0.7 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 29db614f1..4a689cbdd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.7-SNAPSHOT + 2.0.7 jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - HEAD + hap-2.0.7 From 26bf0903e8d23639b9e380078a442c86a23ac9b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Mar 2023 20:03:16 +0000 Subject: [PATCH 04/26] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4a689cbdd..ffd5798a3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.7 + 2.0.8-SNAPSHOT jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - hap-2.0.7 + HEAD From ea14c5db7c10a2772a33da1e2d1ac161aecd2898 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 13 Oct 2025 17:58:36 -0600 Subject: [PATCH 05/26] Allow characteristic setters to get the username of the triggering connection --- CHANGES.md | 3 +++ .../characteristics/Characteristic.java | 10 +++++++++ .../characteristics/ExceptionalConsumer.java | 4 ++++ .../impl/base/BaseCharacteristic.java | 22 +++++++++++++++++-- .../impl/base/BooleanCharacteristic.java | 7 +++++- .../impl/base/EnumCharacteristic.java | 13 +++++++++-- .../impl/base/FloatCharacteristic.java | 7 +++++- .../impl/base/IntegerCharacteristic.java | 7 +++++- .../impl/base/StringCharacteristic.java | 8 ++++++- .../hapjava/server/impl/HomekitServer.java | 4 +++- .../HomekitStandaloneAccessoryServer.java | 2 +- .../impl/connections/ConnectionImpl.java | 7 ++++++ .../impl/http/HomekitClientConnection.java | 4 ++++ .../impl/json/CharacteristicsController.java | 2 +- .../impl/pairing/PairVerifyManager.java | 10 ++++----- .../server/impl/pairing/UpgradeResponse.java | 8 ++++++- 16 files changed, 101 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index ab4bc7e4f..a7e700425 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.7 +* Add overloads to characteristics so that the username can be passed through. + # HAP-Java 2.0.6 * Several methods allowing library users to manipulate characteristics themselves * Allow library users to provider their own implementation of AccessoryInformationService diff --git a/src/main/java/io/github/hapjava/characteristics/Characteristic.java b/src/main/java/io/github/hapjava/characteristics/Characteristic.java index d2e87030a..b2454e5f9 100644 --- a/src/main/java/io/github/hapjava/characteristics/Characteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/Characteristic.java @@ -44,4 +44,14 @@ public interface Characteristic { * @param jsonValue the JSON serialized value to set. */ void setValue(JsonValue jsonValue); + + /** + * Invoked by the remote client, this updates the current value of the characteristic. + * + * @param jsonValue the JSON serialized value to set. + * @param username the authenticated username making the request + */ + default void setValue(JsonValue jsonValue, String username) { + setValue(jsonValue); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/ExceptionalConsumer.java b/src/main/java/io/github/hapjava/characteristics/ExceptionalConsumer.java index 653d73c5f..1c625470d 100644 --- a/src/main/java/io/github/hapjava/characteristics/ExceptionalConsumer.java +++ b/src/main/java/io/github/hapjava/characteristics/ExceptionalConsumer.java @@ -2,4 +2,8 @@ public interface ExceptionalConsumer { void accept(T t) throws Exception; + + default void accept(T t, String username) throws Exception { + accept(t); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/BaseCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/BaseCharacteristic.java index 38d1d8e6d..0808883bb 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/BaseCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/BaseCharacteristic.java @@ -128,13 +128,20 @@ protected CompletableFuture makeBuilder(int instanceId) { /** {@inheritDoc} */ @Override public final void setValue(JsonValue jsonValue) { + setValue(jsonValue, null); + } + + /** {@inheritDoc} */ + @Override + public final void setValue(JsonValue jsonValue, String username) { try { - setValue(convert(jsonValue)); + setValue(convert(jsonValue), username); } catch (Exception e) { logger.warn( - "Error while setting JSON value {} for characteristic {}", + "Error while setting JSON value {} for characteristic {} from user {}", jsonValue, getClass().getName(), + username, e); } } @@ -185,6 +192,17 @@ public void unsubscribe() { */ public abstract void setValue(T value) throws Exception; + /** + * Update the characteristic value using a new value supplied by the connected client. + * + * @param value the new value to set. + * @param username the authenticated username making the request + * @throws Exception if the value cannot be set. + */ + public void setValue(T value, String username) throws Exception { + setValue(value); + } + /** * Retrieves the current value of the characteristic. * diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/BooleanCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/BooleanCharacteristic.java index 4b484eb85..b8c93a4e9 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/BooleanCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/BooleanCharacteristic.java @@ -66,7 +66,12 @@ public CompletableFuture getValue() { @Override public void setValue(Boolean value) throws Exception { - if (setter.isPresent()) setter.get().accept(value); + setValue(value, null); + } + + @Override + public void setValue(Boolean value, String username) throws Exception { + if (setter.isPresent()) setter.get().accept(value, username); } /** {@inheritDoc} */ diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/EnumCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/EnumCharacteristic.java index 4d8dfdb5f..a12cb0673 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/EnumCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/EnumCharacteristic.java @@ -113,15 +113,24 @@ public CompletableFuture getValue() { } public void setValue(T value) throws Exception { + setValue(value, null); + } + + public void setValue(T value, String username) throws Exception { if (!setter.isPresent()) { return; } - setter.get().accept(value); + setter.get().accept(value, username); } @Override public void setValue(Integer value) throws Exception { + setValue(value, null); + } + + @Override + public void setValue(Integer value, String username) throws Exception { if (!setter.isPresent()) { return; } @@ -130,7 +139,7 @@ public void setValue(Integer value) throws Exception { if (validValues != null && value != null) { for (T valid : validValues) { if (valid.getCode() == value) { - setValue(valid); + setValue(valid, username); return; } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/FloatCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/FloatCharacteristic.java index 727cea645..f4b5fa562 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/FloatCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/FloatCharacteristic.java @@ -130,7 +130,12 @@ public final CompletableFuture getValue() { @Override public void setValue(Double value) throws Exception { - if (setter.isPresent()) setter.get().accept(value); + setValue(value, null); + } + + @Override + public void setValue(Double value, String username) throws Exception { + if (setter.isPresent()) setter.get().accept(value, username); } /** {@inheritDoc} */ 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 6d1b7df6d..96448eea8 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 @@ -77,7 +77,12 @@ public CompletableFuture getValue() { @Override public void setValue(Integer value) throws Exception { - if (setter.isPresent()) setter.get().accept(value); + setValue(value, null); + } + + @Override + public void setValue(Integer value, String username) throws Exception { + if (setter.isPresent()) setter.get().accept(value, username); } /** {@inheritDoc} */ diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java index 53196efb9..72ff0afd6 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java @@ -63,7 +63,13 @@ public String convert(JsonValue jsonValue) { /** {@inheritDoc} */ @Override public void setValue(String value) throws Exception { - if (setter.isPresent()) setter.get().accept(value); + setValue(value, null); + } + + /** {@inheritDoc} */ + @Override + public void setValue(String value, String username) throws Exception { + if (setter.isPresent()) setter.get().accept(value, username); } /** {@inheritDoc} */ diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java index fc8ecaf76..7728896a2 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java @@ -117,13 +117,15 @@ public HomekitStandaloneAccessoryServer createStandaloneAccessory( return new HomekitStandaloneAccessoryServer(accessory, http, localAddress, authInfo); } } + public HomekitStandaloneAccessoryServer createStandaloneAccessory( HomekitAuthInfo authInfo, HomekitAccessory accessory, int category) throws IOException, ExecutionException, InterruptedException { if (jmdns != null) { return new HomekitStandaloneAccessoryServer(accessory, http, jmdns, authInfo, category); } else { - return new HomekitStandaloneAccessoryServer(accessory, http, localAddress, authInfo, category); + return new HomekitStandaloneAccessoryServer( + accessory, http, localAddress, authInfo, category); } } diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java index 99ff7b746..4a5dbabee 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitStandaloneAccessoryServer.java @@ -40,7 +40,7 @@ public class HomekitStandaloneAccessoryServer { root = new HomekitRoot(accessory.getName().get(), webHandler, jmdns, authInfo); root.addAccessory(accessory); } - + HomekitStandaloneAccessoryServer( HomekitAccessory accessory, HomekitWebHandler webHandler, diff --git a/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java b/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java index 4869712e1..759506722 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/ConnectionImpl.java @@ -27,6 +27,7 @@ class ConnectionImpl implements HomekitClientConnection { private int outboundBinaryMessageCount = 0; private byte[] readKey; private byte[] writeKey; + private String username; private boolean isUpgraded = false; private final Consumer outOfBandMessageCallback; private final SubscriptionManager subscriptions; @@ -58,6 +59,7 @@ private HttpResponse doHandleRequest(HttpRequest request) throws IOException { isUpgraded = true; readKey = ((UpgradeResponse) response).getReadKey().array(); writeKey = ((UpgradeResponse) response).getWriteKey().array(); + username = ((UpgradeResponse) response).getUsername(); } LOGGER.trace("{} {} {}", response.getStatusCode(), request.getMethod(), request.getUri()); return response; @@ -145,4 +147,9 @@ public void close() { public void outOfBand(HttpResponse message) { outOfBandMessageCallback.accept(message); } + + @Override + public String getUsername() { + return username; + } } diff --git a/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java b/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java index 58ef23c6e..2648808bb 100644 --- a/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java +++ b/src/main/java/io/github/hapjava/server/impl/http/HomekitClientConnection.java @@ -13,4 +13,8 @@ public interface HomekitClientConnection { void close(); void outOfBand(HttpResponse message); + + default String getUsername() { + return null; + } } diff --git a/src/main/java/io/github/hapjava/server/impl/json/CharacteristicsController.java b/src/main/java/io/github/hapjava/server/impl/json/CharacteristicsController.java index d061a11fc..f5f8bb87c 100644 --- a/src/main/java/io/github/hapjava/server/impl/json/CharacteristicsController.java +++ b/src/main/java/io/github/hapjava/server/impl/json/CharacteristicsController.java @@ -89,7 +89,7 @@ public HttpResponse put(HttpRequest request, HomekitClientConnection connection) } if (jsonCharacteristic.containsKey("value")) { - characteristic.setValue(jsonCharacteristic.get("value")); + characteristic.setValue(jsonCharacteristic.get("value"), connection.getUsername()); } if (jsonCharacteristic.containsKey("ev") && characteristic instanceof EventableCharacteristic) { diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java index 44f25c6d1..4960c6f6a 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java @@ -105,11 +105,10 @@ private HttpResponse handleVerifyFinishRequest(VerifyFinishRequest request) thro byte[] material = ByteUtils.joinBytes(clientPublicKey, clientUsername, publicKey); - byte[] clientLtpk = - authInfo.getUserPublicKey( - authInfo.getMac() + new String(clientUsername, StandardCharsets.UTF_8)); + String username = new String(clientUsername, StandardCharsets.UTF_8); + byte[] clientLtpk = authInfo.getUserPublicKey(authInfo.getMac() + username); if (clientLtpk == null) { - logger.warn("Unknown user: {}", new String(clientUsername, StandardCharsets.UTF_8)); + logger.warn("Unknown user: {}", username); return new PairingResponse(4, ErrorCode.AUTHENTICATION); } @@ -120,7 +119,8 @@ private HttpResponse handleVerifyFinishRequest(VerifyFinishRequest request) thro return new UpgradeResponse( encoder.toByteArray(), createKey("Control-Write-Encryption-Key"), - createKey("Control-Read-Encryption-Key")); + createKey("Control-Read-Encryption-Key"), + username); } else { logger.warn("Invalid signature. Could not pair " + registry.getLabel()); return new PairingResponse(4, ErrorCode.AUTHENTICATION); diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/UpgradeResponse.java b/src/main/java/io/github/hapjava/server/impl/pairing/UpgradeResponse.java index 87168aed1..121fc7fd8 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/UpgradeResponse.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/UpgradeResponse.java @@ -6,11 +6,13 @@ public class UpgradeResponse extends PairingResponse { private final byte[] readKey; private final byte[] writeKey; + private final String username; - UpgradeResponse(byte[] body, byte[] readKey, byte[] writeKey) { + UpgradeResponse(byte[] body, byte[] readKey, byte[] writeKey, String username) { super(body); this.readKey = readKey; this.writeKey = writeKey; + this.username = username; } @Override @@ -25,4 +27,8 @@ public ByteBuffer getReadKey() { public ByteBuffer getWriteKey() { return ByteBuffer.wrap(writeKey); } + + public String getUsername() { + return username; + } } From 4b40cd9421535d156f9ac7dd75842e95ddf3bbcc Mon Sep 17 00:00:00 2001 From: jrburk Date: Fri, 2 Jan 2026 19:38:59 +0100 Subject: [PATCH 06/26] Update to bouncy castle 1.8.3 (#193) * Update to bouncy castle 1.8.2 * bcprov 1.83 --------- Co-authored-by: Jochen Burkhardt Co-authored-by: Cody Cutrer --- CHANGES.md | 3 + pom.xml | 10 +- .../server/impl/crypto/ChachaDecoder.java | 98 ++++++++++++++----- .../server/impl/crypto/ChachaEncoder.java | 95 +++++++++++++----- .../server/impl/pairing/ExchangeHandler.java | 28 +++++- .../server/impl/pairing/PairSetupRequest.java | 34 ++++++- 6 files changed, 209 insertions(+), 59 deletions(-) mode change 100644 => 100755 src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java mode change 100644 => 100755 src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java mode change 100644 => 100755 src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java mode change 100644 => 100755 src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java diff --git a/CHANGES.md b/CHANGES.md index a7e700425..95f83e79f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.8 +* Updated bouncy castle to 1.83 + # HAP-Java 2.0.7 * Add overloads to characteristics so that the username can be passed through. diff --git a/pom.xml b/pom.xml index ffd5798a3..b25007f26 100644 --- a/pom.xml +++ b/pom.xml @@ -105,10 +105,16 @@ org.bouncycastle - bcprov-jdk15on - 1.51 + bcpkix-jdk18on + 1.83 + + org.bouncycastle + bctls-jdk18on + 1.82 + + net.vrallev.ecc ecc-25519-java diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java old mode 100644 new mode 100755 index 4c74a469d..014f4b500 --- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java +++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java @@ -1,54 +1,98 @@ package io.github.hapjava.server.impl.crypto; import java.io.IOException; -import org.bouncycastle.crypto.engines.ChaChaEngine; -import org.bouncycastle.crypto.generators.Poly1305KeyGenerator; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.modes.ChaCha20Poly1305; +import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; -import org.bouncycastle.crypto.tls.AlertDescription; -import org.bouncycastle.crypto.tls.TlsFatalAlert; -import org.bouncycastle.util.Arrays; +import org.bouncycastle.tls.AlertDescription; +import org.bouncycastle.tls.TlsFatalAlert; public class ChachaDecoder { - private final ChaChaEngine decryptCipher; + private final ChaCha20Poly1305 cipher; + private final byte[] key; + private final byte[] nonce; public ChachaDecoder(byte[] key, byte[] nonce) throws IOException { + this.key = key; + // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce + this.nonce = ensureNonceSize(nonce); + this.cipher = new ChaCha20Poly1305(); + } - this.decryptCipher = new ChaChaEngine(20); + private byte[] ensureNonceSize(byte[] nonce) { + if (nonce == null) { + return new byte[12]; // Default to zero nonce if null + } - this.decryptCipher.init(false, new ParametersWithIV(new KeyParameter(key), nonce)); + // For HomeKit pairing messages, handle Apple's string-based nonces + if (nonce.length == 8) { + // Apple's HomeKit implementation uses a specific nonce format + // Based on RFC 7539 and Apple's implementation, the nonce should be: + // - 4 bytes constant (0x00000000) + // - 8 bytes nonce string + // This matches ChaCha20's 96-bit nonce requirement + byte[] adjustedNonce = new byte[12]; + // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning + System.arraycopy(nonce, 0, adjustedNonce, 4, 8); + // First 4 bytes remain zero (counter initialization) + return adjustedNonce; + } + + if (nonce.length == 12) { + return nonce; // Already correct size + } + + // For other nonce lengths, pad or truncate to 12 bytes + byte[] adjustedNonce = new byte[12]; + if (nonce.length < 12) { + // Pad with zeros if too short - put nonce at beginning + System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length); + // Remaining bytes are already zero + } else { + // Truncate if too long - take first 12 bytes + System.arraycopy(nonce, 0, adjustedNonce, 0, 12); + } + + return adjustedNonce; } public byte[] decodeCiphertext(byte[] receivedMAC, byte[] additionalData, byte[] ciphertext) throws IOException { - KeyParameter macKey = initRecordMAC(decryptCipher); + try { + byte[] ciphertextWithTag = new byte[ciphertext.length + receivedMAC.length]; + System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length); + System.arraycopy(receivedMAC, 0, ciphertextWithTag, ciphertext.length, receivedMAC.length); + + ChaCha20Poly1305 cipher1 = new ChaCha20Poly1305(); + AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData); + cipher1.init(false, params); - byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext); + byte[] output = new byte[cipher1.getOutputSize(ciphertextWithTag.length)]; + int len = cipher1.processBytes(ciphertextWithTag, 0, ciphertextWithTag.length, output, 0); + len += cipher1.doFinal(output, len); - if (!Arrays.constantTimeAreEqual(calculatedMAC, receivedMAC)) { + byte[] result = new byte[len]; + System.arraycopy(output, 0, result, 0, len); + + return result; + + } catch (InvalidCipherTextException e) { throw new TlsFatalAlert(AlertDescription.bad_record_mac); } + } - byte[] output = new byte[ciphertext.length]; - decryptCipher.processBytes(ciphertext, 0, ciphertext.length, output, 0); - - return output; + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); } public byte[] decodeCiphertext(byte[] receivedMAC, byte[] ciphertext) throws IOException { return decodeCiphertext(receivedMAC, null, ciphertext); } - - private KeyParameter initRecordMAC(ChaChaEngine cipher) { - byte[] firstBlock = new byte[64]; - cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0); - - // NOTE: The BC implementation puts 'r' after 'k' - System.arraycopy(firstBlock, 0, firstBlock, 32, 16); - KeyParameter macKey = new KeyParameter(firstBlock, 16, 32); - Poly1305KeyGenerator.clamp(macKey.getKey()); - return macKey; - } } diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java old mode 100644 new mode 100755 index 3304e7d9a..889c9ca86 --- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java +++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java @@ -1,20 +1,64 @@ package io.github.hapjava.server.impl.crypto; import java.io.IOException; -import org.bouncycastle.crypto.engines.ChaChaEngine; -import org.bouncycastle.crypto.generators.Poly1305KeyGenerator; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.modes.ChaCha20Poly1305; +import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; public class ChachaEncoder { - private final ChaChaEngine encryptCipher; + private final ChaCha20Poly1305 cipher; + private final byte[] key; + private final byte[] nonce; public ChachaEncoder(byte[] key, byte[] nonce) throws IOException { + this.key = key; + // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce + this.nonce = ensureNonceSize(nonce); + this.cipher = new ChaCha20Poly1305(); + } + + private byte[] ensureNonceSize(byte[] nonce) { + if (nonce == null) { + return new byte[12]; // Default to zero nonce if null + } + + // For HomeKit pairing messages, handle Apple's string-based nonces + if (nonce.length == 8) { + // Apple's HomeKit implementation uses a specific nonce format + // Based on RFC 7539 and Apple's implementation, the nonce should be: + // - 4 bytes constant (0x00000000) + // - 8 bytes nonce string + // This matches ChaCha20's 96-bit nonce requirement and ChachaDecoder format + byte[] adjustedNonce = new byte[12]; + // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning + System.arraycopy(nonce, 0, adjustedNonce, 4, 8); + // First 4 bytes remain zero (counter initialization) + return adjustedNonce; + } + + if (nonce.length == 12) { + return nonce; // Already correct size + } - this.encryptCipher = new ChaChaEngine(20); + byte[] adjustedNonce = new byte[12]; + if (nonce.length < 12) { + // Pad with zeros if too short + System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length); + } else { + // Truncate if too long + System.arraycopy(nonce, 0, adjustedNonce, 0, 12); + } + return adjustedNonce; + } - this.encryptCipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce)); + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); } public byte[] encodeCiphertext(byte[] plaintext) throws IOException { @@ -22,27 +66,28 @@ public byte[] encodeCiphertext(byte[] plaintext) throws IOException { } public byte[] encodeCiphertext(byte[] plaintext, byte[] additionalData) throws IOException { - KeyParameter macKey = initRecordMAC(encryptCipher); - - byte[] ciphertext = new byte[plaintext.length]; - encryptCipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); + try { + // Use the nonce provided during construction + AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData); + cipher.init(true, params); - byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext); - - byte[] ret = new byte[ciphertext.length + 16]; - System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length); - System.arraycopy(calculatedMAC, 0, ret, ciphertext.length, 16); - return ret; - } + byte[] output = new byte[cipher.getOutputSize(plaintext.length)]; + int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0); + len += cipher.doFinal(output, len); - private KeyParameter initRecordMAC(ChaChaEngine cipher) { - byte[] firstBlock = new byte[64]; - cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0); + // Split the result into ciphertext and MAC + byte[] ciphertext = new byte[plaintext.length]; + byte[] mac = new byte[16]; + System.arraycopy(output, 0, ciphertext, 0, plaintext.length); + System.arraycopy(output, plaintext.length, mac, 0, 16); - // NOTE: The BC implementation puts 'r' after 'k' - System.arraycopy(firstBlock, 0, firstBlock, 32, 16); - KeyParameter macKey = new KeyParameter(firstBlock, 16, 32); - Poly1305KeyGenerator.clamp(macKey.getKey()); - return macKey; + // Return combined ciphertext + MAC as expected by the original interface + byte[] ret = new byte[ciphertext.length + 16]; + System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length); + System.arraycopy(mac, 0, ret, ciphertext.length, 16); + return ret; + } catch (InvalidCipherTextException e) { + throw new IOException("Encryption failed", e); + } } } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java old mode 100644 new mode 100755 index 2310975fa..4b7c3eb11 --- a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java @@ -31,6 +31,8 @@ public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) { } public HttpResponse handle(PairSetupRequest req) throws Exception { + LOGGER.debug("ExchangeHandler: Starting M5 exchange with shared secret K: {}", bytesToHex(k)); + HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init( new HKDFParameters( @@ -40,12 +42,26 @@ public HttpResponse handle(PairSetupRequest req) throws Exception { byte[] okm = hkdf_enc_key = new byte[32]; hkdf.generateBytes(okm, 0, 32); + LOGGER.debug("ExchangeHandler: HKDF encryption key: {}", bytesToHex(okm)); + return decrypt((ExchangeRequest) req, okm); } private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception { - ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8)); - byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData()); + LOGGER.debug("ExchangeHandler: Received AuthTag: {}", bytesToHex(req.getAuthTagData())); + LOGGER.debug("ExchangeHandler: Received MessageData: {}", bytesToHex(req.getMessageData())); + + try { + ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8)); + byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData()); + return processDecryptedData(plaintext); + } catch (Exception e) { + LOGGER.error("ExchangeHandler: M5 decryption failed: {}", e.getMessage()); + throw new RuntimeException("HomeKit M5 message decryption failed", e); + } + } + + private HttpResponse processDecryptedData(byte[] plaintext) throws Exception { DecodeResult d = TypeLengthValueUtils.decode(plaintext); byte[] username = d.getBytes(MessageType.USERNAME); @@ -54,6 +70,14 @@ private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception { return createUser(username, ltpk, proof); } + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) throws Exception { HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init( diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java old mode 100644 new mode 100755 index 4e7260fed..6fa099be4 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java @@ -85,10 +85,38 @@ static class ExchangeRequest extends PairSetupRequest { private final byte[] authTagData; public ExchangeRequest(DecodeResult d) { - messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16]; + // Get the complete encrypted data field + byte[] encryptedData = d.getBytes(MessageType.ENCRYPTED_DATA); + logger.debug("ExchangeRequest: Total encrypted data length: {}", encryptedData.length); + logger.debug("ExchangeRequest: Raw encrypted data: {}", bytesToHex(encryptedData)); + + // For HomeKit M5, the encrypted data contains ciphertext + 16-byte auth tag + // The auth tag is the LAST 16 bytes + if (encryptedData.length < 16) { + throw new RuntimeException( + "Encrypted data too short, expected at least 16 bytes for auth tag"); + } + + int ciphertextLength = encryptedData.length - 16; + messageData = new byte[ciphertextLength]; authTagData = new byte[16]; - d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0); - d.getBytes(MessageType.ENCRYPTED_DATA, authTagData, messageData.length); + + // Copy ciphertext (everything except last 16 bytes) + System.arraycopy(encryptedData, 0, messageData, 0, ciphertextLength); + // Copy auth tag (last 16 bytes) + System.arraycopy(encryptedData, ciphertextLength, authTagData, 0, 16); + + logger.debug("ExchangeRequest: Parsed ciphertext length: {}", messageData.length); + logger.debug("ExchangeRequest: Parsed ciphertext: {}", bytesToHex(messageData)); + logger.debug("ExchangeRequest: Parsed auth tag: {}", bytesToHex(authTagData)); + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); } public byte[] getMessageData() { From 9b7abd344300384e5b31fb964793bb471e76d628 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 2 Jan 2026 11:43:48 -0700 Subject: [PATCH 07/26] Update netty Closes #189 --- CHANGES.md | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 95f83e79f..8da353402 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 +* Updated netty to 4.1.126.Final # HAP-Java 2.0.7 * Add overloads to characteristics so that the username can be passed through. diff --git a/pom.xml b/pom.xml index b25007f26..3041f7396 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 - 4.1.72.Final + 4.1.126.Final From a62d34f498e8d589c951a7a8e66efacad05a35f8 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 2 Jan 2026 11:54:52 -0700 Subject: [PATCH 08/26] remove unused travis config --- .travis.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f14d2e2b9..000000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: java -jdk: openjdk8 -after_success: -- openssl aes-256-cbc -pass pass:$ENCRYPTION_PASSWORD -in deploy/pubring.gpg.enc -out - deploy/pubring.gpg -d -- openssl aes-256-cbc -pass pass:$ENCRYPTION_PASSWORD -in deploy/secring.gpg.enc -out - deploy/secring.gpg -d -- deploy/publish.sh -env: - global: - - secure: f/Ka8GVA6NnyUktn+TtBhdjVuM1wQI24ANxKsinfm4ldjfC6jueONgt3bog40fQ+ud5gVtJBAhven/qRicx9hT8bnrNUBSbeSD74KfuaZ6ICUmFyG64mNgbfkbc/pRzSbUf5WVAHPtgCo/p5P2Kt5/4l6/+DKmxLEPQ6K8bMz3nQCv0VtsbuGWh+QRxewA3AYLXF/O3nmP5YbP/X081C+TLiQQoibaL9UUGGWmcdEdsupgNdivvPfoYSji+gw3Dj6dn/gFZC1Y3tdcciyA4WOcspCuhZ4OVHZrencHr7PvvCjwjTQQ9bOQZi3l2zcWMGtnZlwlREdzTjUDP6sc2sjDJYy4FL1I87LRAhkiCO+Bo/SznQxAlljtng5R7eqYplt7ByLDpA6GJZQbchn6O+3YqqC9bScgQlyGQiHpbNGAzY5RQRFmvOosYIFTJm6KxMNMO2jogwID72e/+iU/nn5VaUA1btTziTdshb4en2g6f2jLLKURFHKEcXKmZgEOUV1ajqTUh4e4I++drhM8Nq+7vSYpuXbmnVleNbqbq4fWmH6Y0JlMSTn9Hk/K2tq2zw+Jz1oFWeaE39JCkK5gf3IsyGoOSy+dXqI0f9LtG7bPV0dDQGaE3Dh8P1gsXIqTo1E8fD8C1CvZjFgTV4rHukRTk5nfiNxItyWqRIQiLfBqU= - - secure: wHq7ZQPVYVuHrxnQj5bE4KZscBWr9pYL6U1YO1n/P/obkUz4ni6wG9KgLSOk+v9zj69of/HNRtBuo794tup76QREwEYBdKZsBg7cf9Fe14M44WJjY0O9FXJnUbuRfRqTQ+WhbugwiYPVaNZkrBM3FNj/kt0VP/0CtFqB180vrzv9Gokh0ZUYEuqrA9DGV2OszCt27FUQKAGUxDvVdV7d+9D+2FugkeMRg4CLqpFFx5E0THvnQnoKGCLiMZKnv8DN5BGUedzXHFHZtldJNCve2XNYCVNp8xKlCslsR1bwNYsHNM1nuG4kPjWYQfGAIBzbratTpMUcUTIC4l3liG9PkRvg5MBxi2HEoB+FlH4r3X1Pzj5QBDvNP6/+hZjgJQhhKHEdgau1qcC/fVT6Rf/Hmc3EwPSYOKRobzxXJcSG2TDxijAQD4QO41ekMqQJ+FXHIMR7xL+hK//xZSPJJxvjaKgC081/L1GP5mNvXuuo5FjG+1dr4yCpRtpqAShvmcybGK9VHaRdh+MRG+Ec81PdXm2lqGAjHWkfZnDo79WeyEVkqcjTnv1+lgBrRC5SL1WLy3fErLGMJG1HjM41G2O8B6wVIm0nMM8ejoTocHNw3k1l1rSicM7s8wamk8hlU4QhF6wpfgY1LKs0hSkfqlaIdGLHldCsyKakTQSPSgq/TA0= - - secure: hknjOefKy9U3A9PLrrf+s3uUaD9Is2XnkKr4X4o5mT79iWBjHbNbGllK+o/lrh8oV+SNKs5l+mp+FVtB0JBJK1bPGb7r7nR8zJcCUNrfJlOYePAmRhlc6fyGcclPQ81LLrBvbgn1PXFOfcttjWjZH8uIVMc4uEgobYQeS1l6jPPL2w471PtBgajeF1MSpy1/pwnZEdAFVSAB8RMNnqR/s7KjwG+qhkFgLEr5nz3AhIj3JSXlBQRiUjDH+qlxIt8SOunQZVtt6nYAVDBGp89v6SYk6w2CrEri1aUu8/9I7OT/X9+jSD2ksVFvoWW1hXNHhf7FCWqhbQzO7fGOg90ZM/lmgOVbnA+dihtT60GfBfSET52bInqG1QjDnig+/kggVcupUjThVI7VFIl8SGJXfhQtlnwlCWG2OYq+JBXoy0xUpEdvUnbnJv/8i4uvFpGZ/cgqPazw3arrURL+Shd82l+aRFBhHAa+qTvWdmcxBF1TvkWIodfaEPa95PS4mmao709/2bo7rf+FwaTRFtikiW2Asw7xSNlWmRBl90cS+B62xFiie8H6OeeOFcSKTN07MfuYaCdWKfViHaBU/W7Dfp2Sc7isPEEuBiJYDHucH2dsw0qBk6DyWlnWMaxo297/9QGyopiQNocgG+efKd+x7/cENLKUEVUXKEiyD1aebXY= - - secure: r5CcVp0fSkOunI63uI0Y0LETKeWMGStDOJayKzHsSmUg5t0GlH/8Zd8YrItbppCW/WNOjZ2c3XHAnFf55N4hcOvaHIMLJQaPh4LrMEe1uhFXQFfF8RFte4Ek1KBD0l//MeZQZMqykyz+O/5q4dsAvOi6B617OW2bmpUaF8hs+x+/+6C7hWxYaOU/zHpRrb+LC4YpnHqwfVMgpUdKxo83uysMTPcJDjjBvn+MTiXQvxBBAz1k+3fLQxJSUur+pdc22D/i0VZFpkQ9UpQRosfu8hAjZo4NzTLt/PeKbLvIx4kPRBlJqsU89iRcBE8QKUi3Y/Kelif2e30SdDUhoDSHN66mRe2fx/QmsP29yM7myC3dtxRUedXYNw9nTyx+Gmj23nL/U4IzifNVizWZgoHNheAt0E7+Z0WVhGKTtrYyy/2ffn1DCa80A9jO5DS/nDHkqukaW0cwHFq2EbdKv1UQBacM0QFzgHTiH1qkOzAQ1dh5ekViAyUIQ8qR05M06yZzIEeCxY/p5x6fs/yzSoOI37t5t5V1qWW7QaJYDAGGQ/u+tNNieq9y2/w2umDPRHvcPS7jA6ElNlK/XIlwkJA8n9xOANRYVLI4AgaXm7dYBopK/bs88cMJ7l6rZt3TMJ78gafL1XA3E3qNoceuoSHYIdu5m3oBvKvqfgcrPt8QrKI= - - secure: cMqrDc1B+hsAQOguaJUWNDSb9Jdo51CiRBPXDP5dBhgf+oErOPED9obSmKiCPaDfaWjdZWENpBs5CPjm+drNF4xEPIoYKaLI4che16wxko+To6I7k+/aCDT+thzgi1qIoWTn7KTq0ZB73tRLDktyugND0fwetgKYMqBOn5laCazWQeLpdIq3r4zWG/dX30lOB4kdnsY2X4Yb+9OPZi9+ZEr8AAiI+FomEGCv22tKAUyA/SYqPA/XXg6XJ2hyDdkTfLrmuz+dJDasglIDj2Js4p/my0vsFtlvGIubdzfnbqCPmtFjECSfVmQz92947/Rb36zK5wrQH8UDsNWRqFmd/WDYbGX7Eg2hSrWpHGO8ury6yyZRvPL4R2XiOQ4/T1yTOosc57M/wG8+wLavKlULXiH+yDWwXJPTGbKbaXxfVxnu9t1VAoLx6f9dSUEd+NP5TdDVOyp0dyvjs/dAsTedCooMiuJw/Y2+bktmhr14I3XWs8+w1jFDLO6/GhH1QOoNf9wi3ywEKnYGfY5YXiFp8meBm7koZ5DvU21uxUG2OBrOYgYrt1KatmN9A0og7JR6QeHJ1yawT0wM3L14JbqOjktG2e3MNBCqNyP+hWDqD7RDRm1pITbTr5oz7gjXeAI1E00okQ3y2/SdbHEPOUu/tSc2YYsrPCfriRz1AHpedr0= From 5a8f9483c7ca7d299bd089f225ea5e9869de1e8a Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 2 Jan 2026 13:01:30 -0700 Subject: [PATCH 09/26] publish to GitHub Packages instead --- .github/workflows/deploy.yml | 18 +++++++----------- .github/workflows/release.yml | 16 +++++++--------- pom.xml | 8 ++++---- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 61753eab7..4b1e82997 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,6 +7,9 @@ on: push: branches: [ master ] +permissions: + packages: write + jobs: build: @@ -14,26 +17,19 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up JDK 11 for Maven Central + - name: Set up JDK 11 uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' architecture: x64 - server-id: ossrh - gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} - gpg-passphrase: MAVEN_GPG_PASSPHRASE - server-username: MAVEN_USERNAME - server-password: MAVEN_CENTRAL_TOKEN - name: Setup git profile run: | git config --global user.name "github-actions[bot]" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - name: Publish Snapshot to Maven Central - run: mvn deploy -B -Possrh + - name: Publish Snapshot to GitHub Packages + run: mvn deploy -B env: - MAVEN_USERNAME: hap-java-dev - MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish site run: mvn -B site-deploy -Dusername=github-actions -Dpassword=${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48b5ee7cf..380426925 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ on: permissions: contents: write + packages: write jobs: build: @@ -27,14 +28,11 @@ jobs: distribution: 'zulu' java-version: '11' architecture: x64 - server-id: ossrh - gpg-private-key: ${{ secrets.GPG_SECRET_KEY }} - gpg-passphrase: MAVEN_GPG_PASSPHRASE - server-username: MAVEN_USERNAME - server-password: MAVEN_CENTRAL_TOKEN + - name: Setup git profile + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Maven release - run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -Possrh + run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} env: - MAVEN_USERNAME: hap-java-dev - MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/pom.xml b/pom.xml index 3041f7396..e1bf50876 100644 --- a/pom.xml +++ b/pom.xml @@ -278,12 +278,12 @@ https://hap-java.github.io/ - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + github + https://maven.pkg.github.com/hap-java/HAP-Java - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + github + https://maven.pkg.github.com/hap-java/HAP-Java From c23a813afefa27154378089362fceec0b94f96c8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:05:27 +0000 Subject: [PATCH 10/26] [maven-release-plugin] prepare release hap-2.0.8 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e1bf50876..549efbad5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.8-SNAPSHOT + 2.0.8 jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - HEAD + hap-2.0.8 From 92dd7476f19744221d4a150d64185a9b1d8685a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:05:28 +0000 Subject: [PATCH 11/26] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 549efbad5..5db91b7d5 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.8 + 2.0.9-SNAPSHOT jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - hap-2.0.8 + HEAD From b11d77f5fa9a21b94865261bc752f839a4e4fc39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:10:03 -0700 Subject: [PATCH 12/26] Bump org.assertj:assertj-core from 3.19.0 to 3.27.7 (#195) Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.19.0 to 3.27.7. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-core-3.19.0...assertj-build-3.27.7) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-version: 3.27.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5db91b7d5..c292424af 100644 --- a/pom.xml +++ b/pom.xml @@ -162,7 +162,7 @@ org.assertj assertj-core - 3.19.0 + 3.27.7 test From dc610bc7885844ec73a2322848c7fc3a0cc703c0 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:15:15 +0200 Subject: [PATCH 13/26] 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"); + } +} From d6d4dce3150c23bbe8d0246f0b8b3f73f7633dee Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:31:43 +0200 Subject: [PATCH 14/26] Do not log pairing secrets in ExchangeHandler Remove DEBUG log statements that printed the SRP shared secret K, the HKDF-derived encryption key and the raw M5 auth tag / message data. No logic changes. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 1 + .../hapjava/server/impl/pairing/ExchangeHandler.java | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4997cae9d..147cdd37e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +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) +* Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java index 4b7c3eb11..6bae3ab84 100755 --- a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java @@ -31,8 +31,6 @@ public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) { } public HttpResponse handle(PairSetupRequest req) throws Exception { - LOGGER.debug("ExchangeHandler: Starting M5 exchange with shared secret K: {}", bytesToHex(k)); - HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init( new HKDFParameters( @@ -42,15 +40,10 @@ public HttpResponse handle(PairSetupRequest req) throws Exception { byte[] okm = hkdf_enc_key = new byte[32]; hkdf.generateBytes(okm, 0, 32); - LOGGER.debug("ExchangeHandler: HKDF encryption key: {}", bytesToHex(okm)); - return decrypt((ExchangeRequest) req, okm); } private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception { - LOGGER.debug("ExchangeHandler: Received AuthTag: {}", bytesToHex(req.getAuthTagData())); - LOGGER.debug("ExchangeHandler: Received MessageData: {}", bytesToHex(req.getMessageData())); - try { ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8)); byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData()); From bf5955412a023304ffc0f8ffcd198538092329d8 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:32:47 +0200 Subject: [PATCH 15/26] Fix frame splitting in LengthPrefixedByteArrayProcessor - Read the length of each frame at the current position instead of always at the start of the read, so several frames of different length in one read are split correctly. - Clear the stored first length byte once it has been used, and do not take the fast path while such a byte is pending, so a read that ends after the first length byte is handled by step(). Adds tests that lock in the existing behaviour (single frame, several frames in one read, a frame split across two reads) plus regression tests for the two cases above. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 1 + .../LengthPrefixedByteArrayProcessor.java | 5 +- .../LengthPrefixedByteArrayProcessorTest.java | 125 ++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java diff --git a/CHANGES.md b/CHANGES.md index 147cdd37e..a50fc7fae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # 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) * Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level +* Fix splitting of encrypted frames when several frames of different length arrive in one read, or when a read ends after the first length byte # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 diff --git a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java index 124acbb51..34ec3a41c 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java @@ -21,9 +21,9 @@ public synchronized Collection handle(byte[] data) { int pos = 0; LOGGER.trace( "Received message of length {}. Existing buffer is {}", data.length, buffer.size()); - if (buffer.size() == 0) { + if (buffer.size() == 0 && firstLengthByteBuffer == null) { while (data.length - pos > 18) { - int targetLength = (data[0] & 0xFF) + (data[1] & 0xFF) * 256 + 16 + 2; + int targetLength = (data[pos] & 0xFF) + (data[pos + 1] & 0xFF) * 256 + 16 + 2; LOGGER.trace("Attempting to read message of length {}", targetLength); if (data.length >= pos + targetLength) { byte[] b = new byte[targetLength - 2]; @@ -56,6 +56,7 @@ private void step(byte[] data, int pos, Collection results) { if (targetLength == 0) { if (firstLengthByteBuffer != null) { targetLength = (firstLengthByteBuffer & 0xFF) + (data[pos] & 0xFF) * 256 + 16; + firstLengthByteBuffer = null; pos += 1; LOGGER.trace( "Received the second byte after storing the first byte. New length is {}", diff --git a/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java b/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java new file mode 100644 index 000000000..9e839848d --- /dev/null +++ b/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java @@ -0,0 +1,125 @@ +package io.github.hapjava.server.impl.connections; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.Test; + +public class LengthPrefixedByteArrayProcessorTest { + + private static final int TAG_LENGTH = 16; + + /** + * Builds a frame as it arrives on the wire: 2-byte little-endian payload length, payload, 16-byte + * tag. The payload and tag bytes start at 0x80 so that they never look like a short length. + */ + private static byte[] frame(int payloadLength, int seed) { + byte[] frame = new byte[2 + payloadLength + TAG_LENGTH]; + frame[0] = (byte) (payloadLength & 0xFF); + frame[1] = (byte) ((payloadLength >> 8) & 0xFF); + for (int i = 2; i < frame.length; i++) { + frame[i] = (byte) (0x80 + seed + i); + } + return frame; + } + + /** What the processor is expected to emit for a frame: payload + tag, without the length. */ + private static byte[] body(byte[] frame) { + return Arrays.copyOfRange(frame, 2, frame.length); + } + + private static byte[] concat(byte[]... parts) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (byte[] part : parts) { + out.write(part, 0, part.length); + } + return out.toByteArray(); + } + + private static List feed(LengthPrefixedByteArrayProcessor processor, byte[]... reads) { + List results = new ArrayList<>(); + for (byte[] read : reads) { + results.addAll(processor.handle(read)); + } + return results; + } + + private static List sizes(Collection results) { + List sizes = new ArrayList<>(); + for (byte[] result : results) { + sizes.add(result.length); + } + return sizes; + } + + private static void assertBodies(List results, byte[]... frames) { + List expectedSizes = new ArrayList<>(); + for (byte[] frame : frames) { + expectedSizes.add(frame.length - 2); + } + assertThat(sizes(results)).isEqualTo(expectedSizes); + for (int i = 0; i < frames.length; i++) { + assertThat(results.get(i)).as("frame %d", i).isEqualTo(body(frames[i])); + } + } + + @Test + public void singleFrameInOneRead() { + byte[] a = frame(100, 1); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), a), a); + } + + @Test + public void threeFramesInOneRead() { + byte[] a = frame(1024, 1); + byte[] b = frame(1024, 2); + byte[] c = frame(200, 3); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), concat(a, b, c)), a, b, c); + } + + @Test + public void frameSplitAcrossTwoReads() { + byte[] a = frame(1024, 1); + int[] offsets = {1, 2, 3, 10, 18, 19, 20, 500, a.length - 1}; + for (int offset : offsets) { + byte[] first = Arrays.copyOfRange(a, 0, offset); + byte[] second = Arrays.copyOfRange(a, offset, a.length); + List results = feed(new LengthPrefixedByteArrayProcessor(), first, second); + assertThat(sizes(results)).as("split at %d", offset).containsExactly(a.length - 2); + assertThat(results.get(0)).as("split at %d", offset).isEqualTo(body(a)); + } + } + + @Test + public void frameSplitWithNextFrameInSecondRead() { + byte[] a = frame(1024, 1); + byte[] b = frame(200, 2); + byte[] first = Arrays.copyOfRange(a, 0, 500); + byte[] second = concat(Arrays.copyOfRange(a, 500, a.length), b); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), first, second), a, b); + } + + @Test + public void shortFrameFollowedByLongerFrameInOneRead() { + byte[] a = frame(5, 1); + byte[] b = frame(100, 2); + List results = feed(new LengthPrefixedByteArrayProcessor(), concat(a, b)); + assertThat(sizes(results)).containsExactly(21, 116); + assertBodies(results, a, b); + } + + @Test + public void singleLengthByteThenRestThenAnotherFrame() { + byte[] a = frame(5, 1); + byte[] b = frame(40, 2); + byte[] first = Arrays.copyOfRange(a, 0, 1); + byte[] second = concat(Arrays.copyOfRange(a, 1, a.length), b); + List results = feed(new LengthPrefixedByteArrayProcessor(), first, second); + assertThat(sizes(results)).containsExactly(21, 56); + assertBodies(results, a, b); + } +} From c5d9dbd351cc28aa8c1b5de2a97c135d0060817f Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:36:01 +0200 Subject: [PATCH 16/26] Grant contents: write to deploy workflow so site-deploy can push gh-pages The permissions block added in 5a8f9483 only granted packages: write, which drops the GITHUB_TOKEN's contents scope. maven-scm-publish-plugin then fails to push the site to gh-pages with a 403. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4b1e82997..3206d8a70 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,6 +8,7 @@ on: branches: [ master ] permissions: + contents: write packages: write jobs: From 25ced5e44a2ce18ceb2e2b3b4d7099b923a6158c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:44:28 +0000 Subject: [PATCH 17/26] [maven-release-plugin] prepare release hap-2.0.9 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c292424af..d476d8e49 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.9-SNAPSHOT + 2.0.9 jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - HEAD + hap-2.0.9 From bc039779085d7f967dbe4c25c47e73ca62c89d8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:44:31 +0000 Subject: [PATCH 18/26] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d476d8e49..a1a952779 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.9 + 2.0.10-SNAPSHOT jar https://github.com/hap-java/HAP-Java @@ -26,7 +26,7 @@ scm:git:https://github.com/hap-java/HAP-Java.git scm:git:https://github.com/hap-java/HAP-Java.git https://github.com/hap-java/HAP-Java.git - hap-2.0.9 + HEAD From 25e9efe3b6e198aae42a5bb11c0abd9544e72773 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:54:39 +0200 Subject: [PATCH 19/26] Only run deploy in release:perform maven-release-plugin defaults release:perform to 'deploy site-deploy' because the pom has a distributionManagement/site element. The site-deploy runs in the release checkout without git credentials and fails with "could not read Username", marking every release run as failed after the artifacts were already published. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 380426925..53c2c8b9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,7 @@ jobs: git config --global user.name "github-actions[bot]" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Maven release - run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} + # Only deploy; the site is published by the deploy workflow on pushes to master. + run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -Dgoals=deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 79ee9a1341af3f47cc8cf78f122618461951178c Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 23:18:58 +0200 Subject: [PATCH 20/26] Remove redundant comment from release workflow Addresses review feedback on #206. --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53c2c8b9e..29bfe0e59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,6 @@ jobs: git config --global user.name "github-actions[bot]" git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - name: Maven release - # Only deploy; the site is published by the deploy workflow on pushes to master. run: mvn release:prepare release:perform -B -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -Dgoals=deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 3389210fa18420d5d75c821fb5a1548ed6748250 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 23:31:33 +0200 Subject: [PATCH 21/26] Add custom min/max constructors to remaining range characteristics (#162) Add DEFAULT_MIN_VALUE/DEFAULT_MAX_VALUE (and DEFAULT_STEP for float characteristics) and a constructor taking min/max (and step) to 19 characteristics that only supported fixed ranges, following the pattern of RotationSpeedCharacteristic. Existing constructors delegate with the previous values, so their serialized output is unchanged. --- CHANGES.md | 3 + .../impl/audio/VolumeCharacteristic.java | 16 +- .../battery/BatteryLevelCharacteristic.java | 15 +- .../impl/common/WaterLavelCharacteristic.java | 19 +- .../FilterLifeLevelCharacteristic.java | 19 +- ...tyDehumidifierThresholdCharacteristic.java | 27 +- ...dityHumidifierThresholdCharacteristic.java | 27 +- ...CurrentRelativeHumidityCharacteristic.java | 19 +- .../TargetRelativeHumidityCharacteristic.java | 27 +- .../lightbulb/BrightnessCharacteristic.java | 16 +- .../impl/lightbulb/HueCharacteristic.java | 28 +- .../lightbulb/SaturationCharacteristic.java | 27 +- .../slat/CurrentTiltAngleCharacteristic.java | 15 +- .../slat/TargetTiltAngleCharacteristic.java | 16 +- ...rentHorizontalTiltAngleCharacteristic.java | 15 +- .../CurrentPositionCharacteristic.java | 15 +- ...urrentVerticalTiltAngleCharacteristic.java | 15 +- ...rgetHorizontalTiltAngleCharacteristic.java | 16 +- .../TargetPositionCharacteristic.java | 16 +- ...TargetVerticalTiltAngleCharacteristic.java | 16 +- .../impl/CustomRangeCharacteristicsTest.java | 253 ++++++++++++++++++ 21 files changed, 574 insertions(+), 46 deletions(-) create mode 100644 src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java diff --git a/CHANGES.md b/CHANGES.md index a50fc7fae..6dfc34b6d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.10 +* Add constructors with custom min/max (and step for float values) to the remaining range characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) + # 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) * Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level diff --git a/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java index 2d1caedd7..2a4223d5c 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java @@ -11,8 +11,12 @@ /** Volume characteristic to control audio volume. */ public class VolumeCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = 0; + public static final int DEFAULT_MAX_VALUE = 100; public VolumeCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -20,12 +24,20 @@ public VolumeCharacteristic( super( "00000119-0000-1000-8000-0026BB765291", "volume", - 0, - 100, + minValue, + maxValue, "percentage", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public VolumeCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java index 0fa56ff40..ae7097ccd 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java @@ -11,20 +11,31 @@ /** This characteristic describes the current level of the battery. */ public class BatteryLevelCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = 0; + public static final int DEFAULT_MAX_VALUE = 100; public BatteryLevelCharacteristic( + int minValue, + int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "00000068-0000-1000-8000-0026BB765291", "battery level", - 0, - 100, + minValue, + maxValue, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public BatteryLevelCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java index dda11b087..d9d29bac7 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java @@ -11,21 +11,34 @@ /** This characteristic describes the water level. */ public class WaterLavelCharacteristic extends FloatCharacteristic implements EventableCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public WaterLavelCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000B5-0000-1000-8000-0026BB765291", "water level", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public WaterLavelCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java index 29cdf8d6e..6213769c9 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java @@ -10,21 +10,34 @@ public class FilterLifeLevelCharacteristic extends FloatCharacteristic implements EventableCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public FilterLifeLevelCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000AB-0000-1000-8000-0026BB765291", "Filter Life Level", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public FilterLifeLevelCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java index b8a4ca569..84a31fe24 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java @@ -13,8 +13,14 @@ * turns on. */ public class HumidityDehumidifierThresholdCharacteristic extends FloatCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public HumidityDehumidifierThresholdCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -22,13 +28,28 @@ public HumidityDehumidifierThresholdCharacteristic( super( "000000C9-0000-1000-8000-0026BB765291", "humidity threshold dehumidifier", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public HumidityDehumidifierThresholdCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this( + DEFAULT_MIN_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_STEP, + getter, + setter, + subscriber, + unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java index 2ee8adbf2..14b78bde1 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java @@ -13,8 +13,14 @@ * turns on. */ public class HumidityHumidifierThresholdCharacteristic extends FloatCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public HumidityHumidifierThresholdCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -22,13 +28,28 @@ public HumidityHumidifierThresholdCharacteristic( super( "000000CA-0000-1000-8000-0026BB765291", "humidity threshold humidifier", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public HumidityHumidifierThresholdCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this( + DEFAULT_MIN_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_STEP, + getter, + setter, + subscriber, + unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java index be77125b6..7ce18fb46 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java @@ -9,21 +9,34 @@ /** This characteristic describes the current relative humidity. */ public class CurrentRelativeHumidityCharacteristic extends FloatCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public CurrentRelativeHumidityCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "00000010-0000-1000-8000-0026BB765291", "Current Relative Humidity", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public CurrentRelativeHumidityCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java index ab8ad7e29..600ca0f7c 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java @@ -10,8 +10,14 @@ /** This characteristic describes the target relative humidity. */ public class TargetRelativeHumidityCharacteristic extends FloatCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public TargetRelativeHumidityCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -19,13 +25,28 @@ public TargetRelativeHumidityCharacteristic( super( "00000034-0000-1000-8000-0026BB765291", "Target Relative Humidity", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public TargetRelativeHumidityCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this( + DEFAULT_MIN_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_STEP, + getter, + setter, + subscriber, + unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java index 693e42efc..b68cb914b 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java @@ -15,8 +15,12 @@ */ public class BrightnessCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = 0; + public static final int DEFAULT_MAX_VALUE = 100; public BrightnessCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -24,12 +28,20 @@ public BrightnessCharacteristic( super( "00000008-0000-1000-8000-0026BB765291", "level of brightness", - 0, - 100, + minValue, + maxValue, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public BrightnessCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java index 7a00816bc..f41a92319 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java @@ -10,7 +10,14 @@ /** This characteristic describes hue or color. */ public class HueCharacteristic extends FloatCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 360; + public static final double DEFAULT_STEP = 1; + public HueCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -18,13 +25,28 @@ public HueCharacteristic( super( "00000013-0000-1000-8000-0026BB765291", "hue or color", - 0, - 360, - 1, + minValue, + maxValue, + minStep, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public HueCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this( + DEFAULT_MIN_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_STEP, + getter, + setter, + subscriber, + unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java index 90694ef98..5d2f7033b 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java @@ -12,8 +12,14 @@ /** This characteristic describes color saturation. */ public class SaturationCharacteristic extends FloatCharacteristic implements EventableCharacteristic { + public static final double DEFAULT_MIN_VALUE = 0; + public static final double DEFAULT_MAX_VALUE = 100; + public static final double DEFAULT_STEP = 1; public SaturationCharacteristic( + double minValue, + double maxValue, + double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -21,13 +27,28 @@ public SaturationCharacteristic( super( "0000002F-0000-1000-8000-0026BB765291", "color saturation", - 0, - 100, - 1, + minValue, + maxValue, + minStep, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public SaturationCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this( + DEFAULT_MIN_VALUE, + DEFAULT_MAX_VALUE, + DEFAULT_STEP, + getter, + setter, + subscriber, + unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java index 3a4868b2c..0f186281a 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java @@ -11,20 +11,31 @@ /** This characteristic describe current tilt angle (between -90 and 90) */ public class CurrentTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public CurrentTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000C1-0000-1000-8000-0026BB765291", "current tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public CurrentTiltAngleCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java index 2a327522d..44824f350 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java @@ -12,8 +12,12 @@ /** This characteristic describes the target angle of slats. */ public class TargetTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public TargetTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -21,12 +25,20 @@ public TargetTiltAngleCharacteristic( super( "000000C2-0000-1000-8000-0026BB765291", "target tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public TargetTiltAngleCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentHorizontalTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentHorizontalTiltAngleCharacteristic.java index 1255d7eff..272a822f5 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentHorizontalTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentHorizontalTiltAngleCharacteristic.java @@ -11,20 +11,31 @@ /** This characteristic describes the current angle of horizontal slats. */ public class CurrentHorizontalTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public CurrentHorizontalTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "0000006C-0000-1000-8000-0026BB765291", "current horizontal tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public CurrentHorizontalTiltAngleCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java index d51614615..e9ed063c8 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java @@ -11,20 +11,31 @@ /** This characteristic describes the current position of accessories. */ public class CurrentPositionCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = 0; + public static final int DEFAULT_MAX_VALUE = 100; public CurrentPositionCharacteristic( + int minValue, + int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "0000006D-0000-1000-8000-0026BB765291", "current position", - 0, - 100, + minValue, + maxValue, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public CurrentPositionCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentVerticalTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentVerticalTiltAngleCharacteristic.java index c6258a2db..7d7168973 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentVerticalTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentVerticalTiltAngleCharacteristic.java @@ -11,20 +11,31 @@ /** This characteristic describes the current angle of vertical slats for accessory. */ public class CurrentVerticalTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public CurrentVerticalTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "0000006E-0000-1000-8000-0026BB765291", "current vertical tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public CurrentVerticalTiltAngleCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetHorizontalTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetHorizontalTiltAngleCharacteristic.java index 38c075760..17639ba6e 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetHorizontalTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetHorizontalTiltAngleCharacteristic.java @@ -12,8 +12,12 @@ /** This characteristic describes the target angle of horizontal slats for accessories. */ public class TargetHorizontalTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public TargetHorizontalTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -21,12 +25,20 @@ public TargetHorizontalTiltAngleCharacteristic( super( "0000007B-0000-1000-8000-0026BB765291", "target horizontal tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public TargetHorizontalTiltAngleCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java index 6398a0b37..82d06c4b2 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java @@ -12,8 +12,12 @@ /** This characteristic describes the target position of accessories like door, window, blinds. */ public class TargetPositionCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = 0; + public static final int DEFAULT_MAX_VALUE = 100; public TargetPositionCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -21,12 +25,20 @@ public TargetPositionCharacteristic( super( "0000007C-0000-1000-8000-0026BB765291", "target position", - 0, - 100, + minValue, + maxValue, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public TargetPositionCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetVerticalTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetVerticalTiltAngleCharacteristic.java index 4d7c3ad71..03fd7a3f5 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetVerticalTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetVerticalTiltAngleCharacteristic.java @@ -12,8 +12,12 @@ /** This characteristic describes the target angle of vertical slats. */ public class TargetVerticalTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { + public static final int DEFAULT_MIN_VALUE = -90; + public static final int DEFAULT_MAX_VALUE = 90; public TargetVerticalTiltAngleCharacteristic( + int minValue, + int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -21,12 +25,20 @@ public TargetVerticalTiltAngleCharacteristic( super( "0000007D-0000-1000-8000-0026BB765291", "target vertical tilt angle", - -90, - 90, + minValue, + maxValue, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } + + public TargetVerticalTiltAngleCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); + } } diff --git a/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java b/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java new file mode 100644 index 000000000..800719d6d --- /dev/null +++ b/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java @@ -0,0 +1,253 @@ +package io.github.hapjava.characteristics.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.hapjava.characteristics.Characteristic; +import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic; +import io.github.hapjava.characteristics.impl.battery.BatteryLevelCharacteristic; +import io.github.hapjava.characteristics.impl.common.WaterLavelCharacteristic; +import io.github.hapjava.characteristics.impl.filtermaintenance.FilterLifeLevelCharacteristic; +import io.github.hapjava.characteristics.impl.humidifier.HumidityDehumidifierThresholdCharacteristic; +import io.github.hapjava.characteristics.impl.humidifier.HumidityHumidifierThresholdCharacteristic; +import io.github.hapjava.characteristics.impl.humiditysensor.CurrentRelativeHumidityCharacteristic; +import io.github.hapjava.characteristics.impl.humiditysensor.TargetRelativeHumidityCharacteristic; +import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic; +import io.github.hapjava.characteristics.impl.lightbulb.HueCharacteristic; +import io.github.hapjava.characteristics.impl.lightbulb.SaturationCharacteristic; +import io.github.hapjava.characteristics.impl.slat.CurrentTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.slat.TargetTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.CurrentHorizontalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.CurrentPositionCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.CurrentVerticalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.TargetHorizontalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.TargetPositionCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.TargetVerticalTiltAngleCharacteristic; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.json.JsonObject; +import org.junit.Test; + +/** Custom min/max support, see https://github.com/hap-java/HAP-Java/issues/162 */ +public class CustomRangeCharacteristicsTest { + + /** Serialized output of the default constructors, captured before custom ranges were added. */ + private static final List DEFAULT_CONSTRUCTORS = + Arrays.asList( + Case.of( + "BatteryLevelCharacteristic", + new BatteryLevelCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"68\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), + Case.of( + "BrightnessCharacteristic", + new BrightnessCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"8\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), + Case.of( + "CurrentPositionCharacteristic", + new CurrentPositionCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"6D\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), + Case.of( + "TargetPositionCharacteristic", + new TargetPositionCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"7C\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), + Case.of( + "VolumeCharacteristic", + new VolumeCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"119\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"percentage\"}"), + Case.of( + "CurrentTiltAngleCharacteristic", + new CurrentTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"C1\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "TargetTiltAngleCharacteristic", + new TargetTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"C2\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "CurrentHorizontalTiltAngleCharacteristic", + new CurrentHorizontalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"6C\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "TargetHorizontalTiltAngleCharacteristic", + new TargetHorizontalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"7B\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "CurrentVerticalTiltAngleCharacteristic", + new CurrentVerticalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"6E\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "TargetVerticalTiltAngleCharacteristic", + new TargetVerticalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"7D\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + "CurrentRelativeHumidityCharacteristic", + new CurrentRelativeHumidityCharacteristic( + () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"10\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "TargetRelativeHumidityCharacteristic", + new TargetRelativeHumidityCharacteristic( + () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"34\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "FilterLifeLevelCharacteristic", + new FilterLifeLevelCharacteristic( + () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"AB\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "HueCharacteristic", + new HueCharacteristic( + () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"13\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":360.0,\"minStep\":1.0,\"unit\":\"arcdegrees\"}"), + Case.of( + "SaturationCharacteristic", + new SaturationCharacteristic( + () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"2F\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "HumidityDehumidifierThresholdCharacteristic", + new HumidityDehumidifierThresholdCharacteristic( + () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"C9\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "HumidityHumidifierThresholdCharacteristic", + new HumidityHumidifierThresholdCharacteristic( + () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"CA\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), + Case.of( + "WaterLavelCharacteristic", + new WaterLavelCharacteristic( + () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"B5\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}")); + + @Test + public void defaultConstructorsKeepTheirSerializedOutput() { + for (Case c : DEFAULT_CONSTRUCTORS) { + assertThat(c.characteristic.toJson(7).join().toString()).as(c.name).isEqualTo(c.expectedJson); + } + } + + @Test + public void integerCharacteristicsAdvertiseCustomRange() { + for (Characteristic c : + Arrays.asList( + new BatteryLevelCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new BrightnessCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentPositionCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetPositionCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new VolumeCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentTiltAngleCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetTiltAngleCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentHorizontalTiltAngleCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetHorizontalTiltAngleCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentVerticalTiltAngleCharacteristic( + 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetVerticalTiltAngleCharacteristic( + 10, + 20, + () -> CompletableFuture.completedFuture(50), + v -> {}, + cb -> {}, + () -> {}))) { + JsonObject json = c.toJson(7).join(); + String name = c.getClass().getSimpleName(); + assertThat(json.getInt("minValue")).as(name).isEqualTo(10); + assertThat(json.getInt("maxValue")).as(name).isEqualTo(20); + assertThat(json.getInt("minStep")).as(name).isEqualTo(1); + } + } + + @Test + public void floatCharacteristicsAdvertiseCustomRangeAndClampToIt() { + for (Characteristic c : + Arrays.asList( + new CurrentRelativeHumidityCharacteristic( + 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), + new TargetRelativeHumidityCharacteristic( + 10, + 20, + 0.5, + () -> CompletableFuture.completedFuture(50.0), + v -> {}, + cb -> {}, + () -> {}), + new FilterLifeLevelCharacteristic( + 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), + new HueCharacteristic( + 10, + 20, + 0.5, + () -> CompletableFuture.completedFuture(50.0), + v -> {}, + cb -> {}, + () -> {}), + new SaturationCharacteristic( + 10, + 20, + 0.5, + () -> CompletableFuture.completedFuture(50.0), + v -> {}, + cb -> {}, + () -> {}), + new HumidityDehumidifierThresholdCharacteristic( + 10, + 20, + 0.5, + () -> CompletableFuture.completedFuture(50.0), + v -> {}, + cb -> {}, + () -> {}), + new HumidityHumidifierThresholdCharacteristic( + 10, + 20, + 0.5, + () -> CompletableFuture.completedFuture(50.0), + v -> {}, + cb -> {}, + () -> {}), + new WaterLavelCharacteristic( + 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}))) { + JsonObject json = c.toJson(7).join(); + String name = c.getClass().getSimpleName(); + assertThat(json.getJsonNumber("minValue").doubleValue()).as(name).isEqualTo(10.0); + assertThat(json.getJsonNumber("maxValue").doubleValue()).as(name).isEqualTo(20.0); + assertThat(json.getJsonNumber("minStep").doubleValue()).as(name).isEqualTo(0.5); + assertThat(json.getJsonNumber("value").doubleValue()).as(name).isEqualTo(20.0); + } + } + + private static class Case { + final String name; + final Characteristic characteristic; + final String expectedJson; + + private Case(String name, Characteristic characteristic, String expectedJson) { + this.name = name; + this.characteristic = characteristic; + this.expectedJson = expectedJson; + } + + static Case of(String name, Characteristic characteristic, String expectedJson) { + return new Case(name, characteristic, expectedJson); + } + } +} From 298f6dd525ce78247ffa9ae15c19559c233eb443 Mon Sep 17 00:00:00 2001 From: udaw Date: Mon, 19 Oct 2026 10:35:23 +0200 Subject: [PATCH 22/26] Add optional Mute characteristic to Doorbell service Mute is an optional characteristic of the Doorbell service. The 2.0.1 changelog lists it for the Doorbell service, but it was only wired into DoorService. DoorService is left unchanged to stay compatible. --- CHANGES.md | 3 ++ .../services/impl/DoorbellService.java | 14 +++++ .../services/impl/DoorbellServiceTest.java | 51 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/test/java/io/github/hapjava/services/impl/DoorbellServiceTest.java diff --git a/CHANGES.md b/CHANGES.md index a50fc7fae..2f8e24550 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.10 +* Add optional Mute characteristic to Doorbell service + # 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) * Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level diff --git a/src/main/java/io/github/hapjava/services/impl/DoorbellService.java b/src/main/java/io/github/hapjava/services/impl/DoorbellService.java index fd8fc7004..06ee325d4 100644 --- a/src/main/java/io/github/hapjava/services/impl/DoorbellService.java +++ b/src/main/java/io/github/hapjava/services/impl/DoorbellService.java @@ -2,8 +2,10 @@ import io.github.hapjava.accessories.DoorbellAccessory; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithBrightness; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithMute; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolume; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic; import io.github.hapjava.characteristics.impl.common.NameCharacteristic; import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic; @@ -42,6 +44,14 @@ public DoorbellService(DoorbellAccessory accessory) { ((AccessoryWithVolume) accessory)::subscribeVolume, ((AccessoryWithVolume) accessory)::unsubscribeVolume)); } + if (accessory instanceof AccessoryWithMute) { + addOptionalCharacteristic( + new MuteCharacteristic( + ((AccessoryWithMute) accessory)::isMuted, + ((AccessoryWithMute) accessory)::setMute, + ((AccessoryWithMute) accessory)::subscribeMuteState, + ((AccessoryWithMute) accessory)::unsubscribeMuteState)); + } } public void addOptionalCharacteristic(NameCharacteristic name) { @@ -55,4 +65,8 @@ public void addOptionalCharacteristic(BrightnessCharacteristic brightness) { public void addOptionalCharacteristic(VolumeCharacteristic volume) { addCharacteristic(volume); } + + public void addOptionalCharacteristic(MuteCharacteristic mute) { + addCharacteristic(mute); + } } diff --git a/src/test/java/io/github/hapjava/services/impl/DoorbellServiceTest.java b/src/test/java/io/github/hapjava/services/impl/DoorbellServiceTest.java new file mode 100644 index 000000000..feb67b94c --- /dev/null +++ b/src/test/java/io/github/hapjava/services/impl/DoorbellServiceTest.java @@ -0,0 +1,51 @@ +package io.github.hapjava.services.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import io.github.hapjava.accessories.DoorbellAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithMute; +import io.github.hapjava.characteristics.Characteristic; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; +import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic; +import java.util.concurrent.CompletableFuture; +import javax.json.JsonObject; +import javax.json.JsonValue; +import org.junit.Test; + +public class DoorbellServiceTest { + + @Test + public void doorbellWithoutOptionalsOnlyHasSwitchEvent() { + DoorbellService service = new DoorbellService(mock(DoorbellAccessory.class)); + + assertThat(service.getType()).isEqualTo("00000121-0000-1000-8000-0026BB765291"); + assertThat(service.getCharacteristics()) + .hasExactlyElementsOfTypes(ProgrammableSwitchEventCharacteristic.class); + } + + @Test + public void doorbellWithMuteHasMuteCharacteristic() throws Exception { + DoorbellAccessory accessory = + mock(DoorbellAccessory.class, withSettings().extraInterfaces(AccessoryWithMute.class)); + when(((AccessoryWithMute) accessory).isMuted()) + .thenReturn(CompletableFuture.completedFuture(true)); + + DoorbellService service = new DoorbellService(accessory); + + assertThat(service.getCharacteristics()) + .hasExactlyElementsOfTypes( + ProgrammableSwitchEventCharacteristic.class, MuteCharacteristic.class); + + Characteristic mute = service.getCharacteristics().get(1); + JsonObject json = mute.toJson(2).join(); + assertThat(json.getString("type")).isEqualTo("11A"); + assertThat(json.getBoolean("value")).isTrue(); + + mute.setValue(JsonValue.FALSE); + verify((AccessoryWithMute) accessory).setMute(false); + } +} From a2a6bffa2b9a636ddb7c0164d8801208a83410a1 Mon Sep 17 00:00:00 2001 From: udaw Date: Mon, 19 Oct 2026 17:03:53 +0200 Subject: [PATCH 23/26] Limit custom min/max to tilt angle characteristics Percentage characteristics and hue have fixed ranges by definition, so only the tilt angles keep the min/max constructors. --- CHANGES.md | 2 +- .../impl/audio/VolumeCharacteristic.java | 16 +- .../battery/BatteryLevelCharacteristic.java | 15 +- .../impl/common/WaterLavelCharacteristic.java | 19 +- .../FilterLifeLevelCharacteristic.java | 19 +- ...tyDehumidifierThresholdCharacteristic.java | 27 +- ...dityHumidifierThresholdCharacteristic.java | 27 +- ...CurrentRelativeHumidityCharacteristic.java | 19 +- .../TargetRelativeHumidityCharacteristic.java | 27 +- .../lightbulb/BrightnessCharacteristic.java | 16 +- .../impl/lightbulb/HueCharacteristic.java | 28 +- .../lightbulb/SaturationCharacteristic.java | 27 +- .../CurrentPositionCharacteristic.java | 15 +- .../TargetPositionCharacteristic.java | 16 +- .../impl/CustomRangeCharacteristicsTest.java | 253 ------------------ .../impl/CustomTiltAngleRangeTest.java | 92 +++++++ 16 files changed, 127 insertions(+), 491 deletions(-) delete mode 100644 src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java create mode 100644 src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java diff --git a/CHANGES.md b/CHANGES.md index 6dfc34b6d..f6be4cdb6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,5 @@ # HAP-Java 2.0.10 -* Add constructors with custom min/max (and step for float values) to the remaining range characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) +* Add constructors with custom min/max to the tilt angle characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) # 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) diff --git a/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java index 2a4223d5c..2d1caedd7 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/audio/VolumeCharacteristic.java @@ -11,12 +11,8 @@ /** Volume characteristic to control audio volume. */ public class VolumeCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = 0; - public static final int DEFAULT_MAX_VALUE = 100; public VolumeCharacteristic( - int minValue, - int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -24,20 +20,12 @@ public VolumeCharacteristic( super( "00000119-0000-1000-8000-0026BB765291", "volume", - minValue, - maxValue, + 0, + 100, "percentage", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public VolumeCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java index ae7097ccd..0fa56ff40 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/battery/BatteryLevelCharacteristic.java @@ -11,31 +11,20 @@ /** This characteristic describes the current level of the battery. */ public class BatteryLevelCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = 0; - public static final int DEFAULT_MAX_VALUE = 100; public BatteryLevelCharacteristic( - int minValue, - int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "00000068-0000-1000-8000-0026BB765291", "battery level", - minValue, - maxValue, + 0, + 100, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public BatteryLevelCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java index d9d29bac7..dda11b087 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/WaterLavelCharacteristic.java @@ -11,34 +11,21 @@ /** This characteristic describes the water level. */ public class WaterLavelCharacteristic extends FloatCharacteristic implements EventableCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public WaterLavelCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000B5-0000-1000-8000-0026BB765291", "water level", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public WaterLavelCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java index 6213769c9..29cdf8d6e 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/filtermaintenance/FilterLifeLevelCharacteristic.java @@ -10,34 +10,21 @@ public class FilterLifeLevelCharacteristic extends FloatCharacteristic implements EventableCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public FilterLifeLevelCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000AB-0000-1000-8000-0026BB765291", "Filter Life Level", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public FilterLifeLevelCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java index 84a31fe24..b8a4ca569 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityDehumidifierThresholdCharacteristic.java @@ -13,14 +13,8 @@ * turns on. */ public class HumidityDehumidifierThresholdCharacteristic extends FloatCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public HumidityDehumidifierThresholdCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -28,28 +22,13 @@ public HumidityDehumidifierThresholdCharacteristic( super( "000000C9-0000-1000-8000-0026BB765291", "humidity threshold dehumidifier", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public HumidityDehumidifierThresholdCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this( - DEFAULT_MIN_VALUE, - DEFAULT_MAX_VALUE, - DEFAULT_STEP, - getter, - setter, - subscriber, - unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java index 14b78bde1..2ee8adbf2 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humidifier/HumidityHumidifierThresholdCharacteristic.java @@ -13,14 +13,8 @@ * turns on. */ public class HumidityHumidifierThresholdCharacteristic extends FloatCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public HumidityHumidifierThresholdCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -28,28 +22,13 @@ public HumidityHumidifierThresholdCharacteristic( super( "000000CA-0000-1000-8000-0026BB765291", "humidity threshold humidifier", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public HumidityHumidifierThresholdCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this( - DEFAULT_MIN_VALUE, - DEFAULT_MAX_VALUE, - DEFAULT_STEP, - getter, - setter, - subscriber, - unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java index 7ce18fb46..be77125b6 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/CurrentRelativeHumidityCharacteristic.java @@ -9,34 +9,21 @@ /** This characteristic describes the current relative humidity. */ public class CurrentRelativeHumidityCharacteristic extends FloatCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public CurrentRelativeHumidityCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "00000010-0000-1000-8000-0026BB765291", "Current Relative Humidity", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public CurrentRelativeHumidityCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, DEFAULT_STEP, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java index 600ca0f7c..ab8ad7e29 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/humiditysensor/TargetRelativeHumidityCharacteristic.java @@ -10,14 +10,8 @@ /** This characteristic describes the target relative humidity. */ public class TargetRelativeHumidityCharacteristic extends FloatCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public TargetRelativeHumidityCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -25,28 +19,13 @@ public TargetRelativeHumidityCharacteristic( super( "00000034-0000-1000-8000-0026BB765291", "Target Relative Humidity", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public TargetRelativeHumidityCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this( - DEFAULT_MIN_VALUE, - DEFAULT_MAX_VALUE, - DEFAULT_STEP, - getter, - setter, - subscriber, - unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java index b68cb914b..693e42efc 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/BrightnessCharacteristic.java @@ -15,12 +15,8 @@ */ public class BrightnessCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = 0; - public static final int DEFAULT_MAX_VALUE = 100; public BrightnessCharacteristic( - int minValue, - int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -28,20 +24,12 @@ public BrightnessCharacteristic( super( "00000008-0000-1000-8000-0026BB765291", "level of brightness", - minValue, - maxValue, + 0, + 100, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public BrightnessCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java index f41a92319..7a00816bc 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/HueCharacteristic.java @@ -10,14 +10,7 @@ /** This characteristic describes hue or color. */ public class HueCharacteristic extends FloatCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 360; - public static final double DEFAULT_STEP = 1; - public HueCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -25,28 +18,13 @@ public HueCharacteristic( super( "00000013-0000-1000-8000-0026BB765291", "hue or color", - minValue, - maxValue, - minStep, + 0, + 360, + 1, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public HueCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this( - DEFAULT_MIN_VALUE, - DEFAULT_MAX_VALUE, - DEFAULT_STEP, - getter, - setter, - subscriber, - unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java index 5d2f7033b..90694ef98 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/SaturationCharacteristic.java @@ -12,14 +12,8 @@ /** This characteristic describes color saturation. */ public class SaturationCharacteristic extends FloatCharacteristic implements EventableCharacteristic { - public static final double DEFAULT_MIN_VALUE = 0; - public static final double DEFAULT_MAX_VALUE = 100; - public static final double DEFAULT_STEP = 1; public SaturationCharacteristic( - double minValue, - double maxValue, - double minStep, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -27,28 +21,13 @@ public SaturationCharacteristic( super( "0000002F-0000-1000-8000-0026BB765291", "color saturation", - minValue, - maxValue, - minStep, + 0, + 100, + 1, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public SaturationCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this( - DEFAULT_MIN_VALUE, - DEFAULT_MAX_VALUE, - DEFAULT_STEP, - getter, - setter, - subscriber, - unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java index e9ed063c8..d51614615 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/CurrentPositionCharacteristic.java @@ -11,31 +11,20 @@ /** This characteristic describes the current position of accessories. */ public class CurrentPositionCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = 0; - public static final int DEFAULT_MAX_VALUE = 100; public CurrentPositionCharacteristic( - int minValue, - int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "0000006D-0000-1000-8000-0026BB765291", "current position", - minValue, - maxValue, + 0, + 100, "%", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public CurrentPositionCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java index 82d06c4b2..6398a0b37 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/windowcovering/TargetPositionCharacteristic.java @@ -12,12 +12,8 @@ /** This characteristic describes the target position of accessories like door, window, blinds. */ public class TargetPositionCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = 0; - public static final int DEFAULT_MAX_VALUE = 100; public TargetPositionCharacteristic( - int minValue, - int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -25,20 +21,12 @@ public TargetPositionCharacteristic( super( "0000007C-0000-1000-8000-0026BB765291", "target position", - minValue, - maxValue, + 0, + 100, "%", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public TargetPositionCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); - } } diff --git a/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java b/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java deleted file mode 100644 index 800719d6d..000000000 --- a/src/test/java/io/github/hapjava/characteristics/impl/CustomRangeCharacteristicsTest.java +++ /dev/null @@ -1,253 +0,0 @@ -package io.github.hapjava.characteristics.impl; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.github.hapjava.characteristics.Characteristic; -import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic; -import io.github.hapjava.characteristics.impl.battery.BatteryLevelCharacteristic; -import io.github.hapjava.characteristics.impl.common.WaterLavelCharacteristic; -import io.github.hapjava.characteristics.impl.filtermaintenance.FilterLifeLevelCharacteristic; -import io.github.hapjava.characteristics.impl.humidifier.HumidityDehumidifierThresholdCharacteristic; -import io.github.hapjava.characteristics.impl.humidifier.HumidityHumidifierThresholdCharacteristic; -import io.github.hapjava.characteristics.impl.humiditysensor.CurrentRelativeHumidityCharacteristic; -import io.github.hapjava.characteristics.impl.humiditysensor.TargetRelativeHumidityCharacteristic; -import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic; -import io.github.hapjava.characteristics.impl.lightbulb.HueCharacteristic; -import io.github.hapjava.characteristics.impl.lightbulb.SaturationCharacteristic; -import io.github.hapjava.characteristics.impl.slat.CurrentTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.slat.TargetTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.CurrentHorizontalTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.CurrentPositionCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.CurrentVerticalTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.TargetHorizontalTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.TargetPositionCharacteristic; -import io.github.hapjava.characteristics.impl.windowcovering.TargetVerticalTiltAngleCharacteristic; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import javax.json.JsonObject; -import org.junit.Test; - -/** Custom min/max support, see https://github.com/hap-java/HAP-Java/issues/162 */ -public class CustomRangeCharacteristicsTest { - - /** Serialized output of the default constructors, captured before custom ranges were added. */ - private static final List DEFAULT_CONSTRUCTORS = - Arrays.asList( - Case.of( - "BatteryLevelCharacteristic", - new BatteryLevelCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"68\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), - Case.of( - "BrightnessCharacteristic", - new BrightnessCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"8\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), - Case.of( - "CurrentPositionCharacteristic", - new CurrentPositionCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"6D\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), - Case.of( - "TargetPositionCharacteristic", - new TargetPositionCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"7C\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"%\"}"), - Case.of( - "VolumeCharacteristic", - new VolumeCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"119\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":0,\"maxValue\":100,\"minStep\":1,\"unit\":\"percentage\"}"), - Case.of( - "CurrentTiltAngleCharacteristic", - new CurrentTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"C1\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "TargetTiltAngleCharacteristic", - new TargetTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"C2\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "CurrentHorizontalTiltAngleCharacteristic", - new CurrentHorizontalTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"6C\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "TargetHorizontalTiltAngleCharacteristic", - new TargetHorizontalTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"7B\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "CurrentVerticalTiltAngleCharacteristic", - new CurrentVerticalTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"6E\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "TargetVerticalTiltAngleCharacteristic", - new TargetVerticalTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"7D\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - "CurrentRelativeHumidityCharacteristic", - new CurrentRelativeHumidityCharacteristic( - () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"10\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "TargetRelativeHumidityCharacteristic", - new TargetRelativeHumidityCharacteristic( - () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"34\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "FilterLifeLevelCharacteristic", - new FilterLifeLevelCharacteristic( - () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"AB\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "HueCharacteristic", - new HueCharacteristic( - () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"13\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":360.0,\"minStep\":1.0,\"unit\":\"arcdegrees\"}"), - Case.of( - "SaturationCharacteristic", - new SaturationCharacteristic( - () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"2F\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "HumidityDehumidifierThresholdCharacteristic", - new HumidityDehumidifierThresholdCharacteristic( - () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"C9\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "HumidityHumidifierThresholdCharacteristic", - new HumidityHumidifierThresholdCharacteristic( - () -> CompletableFuture.completedFuture(50.0), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"CA\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}"), - Case.of( - "WaterLavelCharacteristic", - new WaterLavelCharacteristic( - () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"B5\",\"perms\":[\"pr\",\"ev\"],\"format\":\"float\",\"value\":50.0,\"minValue\":0.0,\"maxValue\":100.0,\"minStep\":1.0,\"unit\":\"%\"}")); - - @Test - public void defaultConstructorsKeepTheirSerializedOutput() { - for (Case c : DEFAULT_CONSTRUCTORS) { - assertThat(c.characteristic.toJson(7).join().toString()).as(c.name).isEqualTo(c.expectedJson); - } - } - - @Test - public void integerCharacteristicsAdvertiseCustomRange() { - for (Characteristic c : - Arrays.asList( - new BatteryLevelCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new BrightnessCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - new CurrentPositionCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new TargetPositionCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - new VolumeCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - new CurrentTiltAngleCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new TargetTiltAngleCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - new CurrentHorizontalTiltAngleCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new TargetHorizontalTiltAngleCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - new CurrentVerticalTiltAngleCharacteristic( - 10, 20, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new TargetVerticalTiltAngleCharacteristic( - 10, - 20, - () -> CompletableFuture.completedFuture(50), - v -> {}, - cb -> {}, - () -> {}))) { - JsonObject json = c.toJson(7).join(); - String name = c.getClass().getSimpleName(); - assertThat(json.getInt("minValue")).as(name).isEqualTo(10); - assertThat(json.getInt("maxValue")).as(name).isEqualTo(20); - assertThat(json.getInt("minStep")).as(name).isEqualTo(1); - } - } - - @Test - public void floatCharacteristicsAdvertiseCustomRangeAndClampToIt() { - for (Characteristic c : - Arrays.asList( - new CurrentRelativeHumidityCharacteristic( - 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), - new TargetRelativeHumidityCharacteristic( - 10, - 20, - 0.5, - () -> CompletableFuture.completedFuture(50.0), - v -> {}, - cb -> {}, - () -> {}), - new FilterLifeLevelCharacteristic( - 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}), - new HueCharacteristic( - 10, - 20, - 0.5, - () -> CompletableFuture.completedFuture(50.0), - v -> {}, - cb -> {}, - () -> {}), - new SaturationCharacteristic( - 10, - 20, - 0.5, - () -> CompletableFuture.completedFuture(50.0), - v -> {}, - cb -> {}, - () -> {}), - new HumidityDehumidifierThresholdCharacteristic( - 10, - 20, - 0.5, - () -> CompletableFuture.completedFuture(50.0), - v -> {}, - cb -> {}, - () -> {}), - new HumidityHumidifierThresholdCharacteristic( - 10, - 20, - 0.5, - () -> CompletableFuture.completedFuture(50.0), - v -> {}, - cb -> {}, - () -> {}), - new WaterLavelCharacteristic( - 10, 20, 0.5, () -> CompletableFuture.completedFuture(50.0), cb -> {}, () -> {}))) { - JsonObject json = c.toJson(7).join(); - String name = c.getClass().getSimpleName(); - assertThat(json.getJsonNumber("minValue").doubleValue()).as(name).isEqualTo(10.0); - assertThat(json.getJsonNumber("maxValue").doubleValue()).as(name).isEqualTo(20.0); - assertThat(json.getJsonNumber("minStep").doubleValue()).as(name).isEqualTo(0.5); - assertThat(json.getJsonNumber("value").doubleValue()).as(name).isEqualTo(20.0); - } - } - - private static class Case { - final String name; - final Characteristic characteristic; - final String expectedJson; - - private Case(String name, Characteristic characteristic, String expectedJson) { - this.name = name; - this.characteristic = characteristic; - this.expectedJson = expectedJson; - } - - static Case of(String name, Characteristic characteristic, String expectedJson) { - return new Case(name, characteristic, expectedJson); - } - } -} diff --git a/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java new file mode 100644 index 000000000..dcff74fc3 --- /dev/null +++ b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java @@ -0,0 +1,92 @@ +package io.github.hapjava.characteristics.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.hapjava.characteristics.Characteristic; +import io.github.hapjava.characteristics.impl.slat.CurrentTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.slat.TargetTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.CurrentHorizontalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.CurrentVerticalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.TargetHorizontalTiltAngleCharacteristic; +import io.github.hapjava.characteristics.impl.windowcovering.TargetVerticalTiltAngleCharacteristic; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import javax.json.JsonObject; +import org.junit.Test; + +/** Custom tilt angle ranges, see https://github.com/hap-java/HAP-Java/issues/162 */ +public class CustomTiltAngleRangeTest { + + @Test + public void defaultConstructorsKeepTheirSerializedOutput() { + // Serialized output captured before custom ranges were added. + for (Case c : + Arrays.asList( + Case.of( + new CurrentTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"C1\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + new TargetTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"C2\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + new CurrentHorizontalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"6C\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + new TargetHorizontalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"7B\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + new CurrentVerticalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"6E\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), + Case.of( + new TargetVerticalTiltAngleCharacteristic( + () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + "{\"iid\":7,\"type\":\"7D\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"))) { + assertThat(c.characteristic.toJson(7).join().toString()) + .as(c.characteristic.getClass().getSimpleName()) + .isEqualTo(c.expected); + } + } + + @Test + public void customRangeIsAdvertised() { + for (Characteristic c : + Arrays.asList( + new CurrentTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentHorizontalTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetHorizontalTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), + new CurrentVerticalTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), + new TargetVerticalTiltAngleCharacteristic( + 0, 90, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}))) { + JsonObject json = c.toJson(7).join(); + String name = c.getClass().getSimpleName(); + assertThat(json.getInt("minValue")).as(name).isEqualTo(0); + assertThat(json.getInt("maxValue")).as(name).isEqualTo(90); + assertThat(json.getInt("minStep")).as(name).isEqualTo(1); + } + } + + private static class Case { + final Characteristic characteristic; + final String expected; + + private Case(Characteristic characteristic, String expected) { + this.characteristic = characteristic; + this.expected = expected; + } + + static Case of(Characteristic characteristic, String expected) { + return new Case(characteristic, expected); + } + } +} From 3f844c2d3f81136507674a55166aa49a78e1e3ed Mon Sep 17 00:00:00 2001 From: Eugen Date: Tue, 3 Nov 2026 10:11:14 +0100 Subject: [PATCH 24/26] Drop slat tilt angle characteristics from custom min/max The Home app shows no control for the Slat service, so a custom range cannot be verified there. --- CHANGES.md | 2 +- .../slat/CurrentTiltAngleCharacteristic.java | 15 ++------------- .../impl/slat/TargetTiltAngleCharacteristic.java | 16 ++-------------- .../impl/CustomTiltAngleRangeTest.java | 14 -------------- 4 files changed, 5 insertions(+), 42 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f6be4cdb6..5bc1183a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,5 @@ # HAP-Java 2.0.10 -* Add constructors with custom min/max to the tilt angle characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) +* Add constructors with custom min/max to the window covering tilt angle characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) # 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) diff --git a/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java index 0f186281a..3a4868b2c 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/slat/CurrentTiltAngleCharacteristic.java @@ -11,31 +11,20 @@ /** This characteristic describe current tilt angle (between -90 and 90) */ public class CurrentTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = -90; - public static final int DEFAULT_MAX_VALUE = 90; public CurrentTiltAngleCharacteristic( - int minValue, - int maxValue, Supplier> getter, Consumer subscriber, Runnable unsubscriber) { super( "000000C1-0000-1000-8000-0026BB765291", "current tilt angle", - minValue, - maxValue, + -90, + 90, "arcdegrees", Optional.of(getter), Optional.empty(), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public CurrentTiltAngleCharacteristic( - Supplier> getter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, subscriber, unsubscriber); - } } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java index 44824f350..2a327522d 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/slat/TargetTiltAngleCharacteristic.java @@ -12,12 +12,8 @@ /** This characteristic describes the target angle of slats. */ public class TargetTiltAngleCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { - public static final int DEFAULT_MIN_VALUE = -90; - public static final int DEFAULT_MAX_VALUE = 90; public TargetTiltAngleCharacteristic( - int minValue, - int maxValue, Supplier> getter, ExceptionalConsumer setter, Consumer subscriber, @@ -25,20 +21,12 @@ public TargetTiltAngleCharacteristic( super( "000000C2-0000-1000-8000-0026BB765291", "target tilt angle", - minValue, - maxValue, + -90, + 90, "arcdegrees", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), Optional.of(unsubscriber)); } - - public TargetTiltAngleCharacteristic( - Supplier> getter, - ExceptionalConsumer setter, - Consumer subscriber, - Runnable unsubscriber) { - this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE, getter, setter, subscriber, unsubscriber); - } } diff --git a/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java index dcff74fc3..d3cd51756 100644 --- a/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java +++ b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java @@ -3,8 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import io.github.hapjava.characteristics.Characteristic; -import io.github.hapjava.characteristics.impl.slat.CurrentTiltAngleCharacteristic; -import io.github.hapjava.characteristics.impl.slat.TargetTiltAngleCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.CurrentHorizontalTiltAngleCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.CurrentVerticalTiltAngleCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.TargetHorizontalTiltAngleCharacteristic; @@ -22,14 +20,6 @@ public void defaultConstructorsKeepTheirSerializedOutput() { // Serialized output captured before custom ranges were added. for (Case c : Arrays.asList( - Case.of( - new CurrentTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"C1\",\"perms\":[\"pr\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), - Case.of( - new TargetTiltAngleCharacteristic( - () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), - "{\"iid\":7,\"type\":\"C2\",\"perms\":[\"pr\",\"pw\",\"ev\"],\"format\":\"int\",\"value\":50,\"minValue\":-90,\"maxValue\":90,\"minStep\":1,\"unit\":\"arcdegrees\"}"), Case.of( new CurrentHorizontalTiltAngleCharacteristic( () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), @@ -56,10 +46,6 @@ public void defaultConstructorsKeepTheirSerializedOutput() { public void customRangeIsAdvertised() { for (Characteristic c : Arrays.asList( - new CurrentTiltAngleCharacteristic( - 0, 90, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), - new TargetTiltAngleCharacteristic( - 0, 90, () -> CompletableFuture.completedFuture(50), v -> {}, cb -> {}, () -> {}), new CurrentHorizontalTiltAngleCharacteristic( 0, 90, () -> CompletableFuture.completedFuture(50), cb -> {}, () -> {}), new TargetHorizontalTiltAngleCharacteristic( From 11b8813cc3042a655827169a00803fe663ba5b3b Mon Sep 17 00:00:00 2001 From: Eugen Date: Sat, 7 Nov 2026 13:33:56 +0100 Subject: [PATCH 25/26] Close owned mDNS instance and remove shutdown hook on stop JmdnsHomekitAdvertiser created its own JmDNS when given an InetAddress but never closed it, and registered a shutdown hook on every advertise() that was never removed. Each stopped HomekitRoot therefore left a running JmDNS instance behind, kept reachable by its shutdown hook. stop() now removes the shutdown hook, closes the JmDNS only if the advertiser created it, and resets the advertising state so later changes to discoverability, MAC or configuration index do not re-register the service. --- CHANGES.md | 1 + .../impl/jmdns/JmdnsHomekitAdvertiser.java | 40 ++++++++++++++----- .../jmdns/JmdnsHomekitAdvertiserTest.java | 25 ++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2f8e24550..5fc06bed9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # HAP-Java 2.0.10 * Add optional Mute characteristic to Doorbell service +* Fix leak of mDNS instances and shutdown hooks when stopping `HomekitRoot` # 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) diff --git a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java index 9086acb3e..1073f9b86 100644 --- a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java +++ b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java @@ -17,6 +17,8 @@ public class JmdnsHomekitAdvertiser { private static final String SERVICE_TYPE = "_hap._tcp.local."; private final JmDNS jmdns; + private final boolean ownsJmdns; + private Thread shutdownHook; private boolean discoverable = true; private static final Logger logger = LoggerFactory.getLogger(JmdnsHomekitAdvertiser.class); private boolean isAdvertising = false; @@ -30,11 +32,16 @@ public class JmdnsHomekitAdvertiser { private int category; public JmdnsHomekitAdvertiser(JmDNS jmdns) { - this.jmdns = jmdns; + this(jmdns, false); } public JmdnsHomekitAdvertiser(InetAddress localAddress) throws UnknownHostException, IOException { - jmdns = JmDNS.create(localAddress); + this(JmDNS.create(localAddress), true); + } + + JmdnsHomekitAdvertiser(JmDNS jmdns, boolean ownsJmdns) { + this.jmdns = jmdns; + this.ownsJmdns = ownsJmdns; } public synchronized void advertise( @@ -54,18 +61,33 @@ public synchronized void advertise( registerService(); - Runtime.getRuntime() - .addShutdownHook( - new Thread( - () -> { - logger.trace("Stopping advertising in response to shutdown."); - jmdns.unregisterAllServices(); - })); + shutdownHook = + new Thread( + () -> { + logger.trace("Stopping advertising in response to shutdown."); + jmdns.unregisterAllServices(); + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); isAdvertising = true; } public synchronized void stop() { unregisterService(); + isAdvertising = false; + if (shutdownHook != null) { + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException e) { + // already shutting down + } + } + if (ownsJmdns) { + try { + jmdns.close(); + } catch (IOException e) { + logger.warn("Failed to close mDNS", e); + } + } } public synchronized void setDiscoverable(boolean discoverable) throws IOException { diff --git a/src/test/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiserTest.java b/src/test/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiserTest.java index 870bd739b..731be498f 100644 --- a/src/test/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiserTest.java +++ b/src/test/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiserTest.java @@ -2,7 +2,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import java.io.IOException; @@ -54,6 +56,29 @@ public void testSetConfigurationIndex() throws Exception { assertThat(getArgumentFromUnregister().getPropertyString("c#")).isEqualTo("1"); } + @Test + public void testStopClosesOwnedJmdns() throws Exception { + subject = new JmdnsHomekitAdvertiser(jmdns, true); + advertise(); + subject.stop(); + verify(jmdns).close(); + } + + @Test + public void testStopDoesNotCloseSuppliedJmdns() throws Exception { + advertise(); + subject.stop(); + verify(jmdns, never()).close(); + } + + @Test + public void testSetDiscoverableAfterStopDoesNotRegister() throws Exception { + advertise(); + subject.stop(); + subject.setDiscoverable(false); + verify(jmdns).registerService(any(ServiceInfo.class)); + } + private ServiceInfo getArgumentFromUnregister() { ArgumentCaptor serviceInfoCaptor = ArgumentCaptor.forClass(ServiceInfo.class); verify(jmdns).unregisterService(serviceInfoCaptor.capture()); From a162ea552137a9a7ca7b5caa7bc3f5ec78369a55 Mon Sep 17 00:00:00 2001 From: eugen Date: Sat, 14 Nov 2026 01:03:47 +0100 Subject: [PATCH 26/26] Update netty to 4.1.139.Final (#215) Co-authored-by: Eugen --- CHANGES.md | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1bbbbcf73..ab4bb1202 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Add constructors with custom min/max to the window covering tilt angle characteristics [#162](https://github.com/hap-java/HAP-Java/issues/162) * Add optional Mute characteristic to Doorbell service * Fix leak of mDNS instances and shutdown hooks when stopping `HomekitRoot` +* Updated netty to 4.1.139.Final # 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) diff --git a/pom.xml b/pom.xml index a1a952779..7bbbb9021 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 - 4.1.126.Final + 4.1.139.Final