From 17f87f1ff5e1d5a43672950ac4f6aa3a2f935d15 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 7 Oct 2022 12:47:34 -0600 Subject: [PATCH 01/50] add back fanv1 service the Home app doesn't let you customize the icon for a FanV2 (i.e. to show a ceiling fan). --- .../accessories/BasicFanAccessory.java | 46 +++++++++++++ .../services/impl/BasicFanService.java | 65 +++++++++++++++++++ .../hapjava/services/impl/FanService.java | 7 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/github/hapjava/accessories/BasicFanAccessory.java create mode 100644 src/main/java/io/github/hapjava/services/impl/BasicFanService.java diff --git a/src/main/java/io/github/hapjava/accessories/BasicFanAccessory.java b/src/main/java/io/github/hapjava/accessories/BasicFanAccessory.java new file mode 100644 index 000000000..98c4bf497 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/BasicFanAccessory.java @@ -0,0 +1,46 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.BasicFanService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** + * A fan with mandatory characteristics. + * + * @author Andy Lintner + */ +public interface BasicFanAccessory extends HomekitAccessory { + /** + * Mandatory: Retrieves the current power state of the fan. + * + * @return a future that will contain the binary state + */ + CompletableFuture isOn(); + + /** + * Sets the active state of the fan + * + * @param state the binary state to set + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setOn(boolean state) throws Exception; + + /** + * Subscribes to changes in the active state of the fan. + * + * @param callback the function to call when the active state changes. + */ + void subscribeOn(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active state of the fan. */ + void unsubscribeOn(); + + @Override + default Collection getServices() { + return Collections.singleton(new BasicFanService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/BasicFanService.java b/src/main/java/io/github/hapjava/services/impl/BasicFanService.java new file mode 100644 index 000000000..9a033dd0c --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/BasicFanService.java @@ -0,0 +1,65 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.BasicFanAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithRotationDirection; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithRotationSpeed; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.common.OnCharacteristic; +import io.github.hapjava.characteristics.impl.fan.RotationDirectionCharacteristic; +import io.github.hapjava.characteristics.impl.fan.RotationSpeedCharacteristic; + +/** + * This service describes a fan. + * + *

In the R1 release of the HAP specification, this is simply described as Fan. It is no longer + * present in the R2 release. + */ +public class BasicFanService extends AbstractServiceImpl { + + public BasicFanService(OnCharacteristic on) { + super("00000040-0000-1000-8000-0026BB765291"); + addCharacteristic(on); + } + + public BasicFanService(BasicFanAccessory accessory) { + this( + new OnCharacteristic( + () -> accessory.isOn(), + (v) -> accessory.setOn(v), + accessory::subscribeOn, + accessory::unsubscribeOn)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + + if (accessory instanceof AccessoryWithRotationDirection) { + addOptionalCharacteristic( + new RotationDirectionCharacteristic( + ((AccessoryWithRotationDirection) accessory)::getRotationDirection, + ((AccessoryWithRotationDirection) accessory)::setRotationDirection, + ((AccessoryWithRotationDirection) accessory)::subscribeRotationDirection, + ((AccessoryWithRotationDirection) accessory)::unsubscribeRotationDirection)); + } + if (accessory instanceof AccessoryWithRotationSpeed) { + addOptionalCharacteristic( + new RotationSpeedCharacteristic( + ((AccessoryWithRotationSpeed) accessory)::getRotationSpeed, + ((AccessoryWithRotationSpeed) accessory)::setRotationSpeed, + ((AccessoryWithRotationSpeed) accessory)::subscribeRotationSpeed, + ((AccessoryWithRotationSpeed) accessory)::unsubscribeRotationSpeed)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(RotationDirectionCharacteristic direction) { + addCharacteristic(direction); + } + + public void addOptionalCharacteristic(RotationSpeedCharacteristic speed) { + addCharacteristic(speed); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/FanService.java b/src/main/java/io/github/hapjava/services/impl/FanService.java index 24d2bc052..a8f3a7103 100644 --- a/src/main/java/io/github/hapjava/services/impl/FanService.java +++ b/src/main/java/io/github/hapjava/services/impl/FanService.java @@ -17,7 +17,12 @@ import io.github.hapjava.characteristics.impl.fan.SwingModeCharacteristic; import io.github.hapjava.characteristics.impl.fan.TargetFanStateCharacteristic; -/** This service describes a fan. */ +/** + * This service describes a fan. + * + *

In the R1 release of the HAP specification, this is described as Fan v2. In the R1 release of + * the HAP specification, this is simply described as Fan. + */ public class FanService extends AbstractServiceImpl { public FanService(ActiveCharacteristic active) { From 81ecb5cf402d10c11a6854ceba924dc9f04bab4b Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Tue, 18 Oct 2022 09:45:26 -0600 Subject: [PATCH 02/50] exclude characteristic description for well-known types they're optional, and they just take up extra data --- .../hapjava/characteristics/impl/base/BaseCharacteristic.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 77b0bc452..734a83cdc 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 @@ -112,8 +112,8 @@ protected CompletableFuture makeBuilder(int instanceId) { .add("iid", instanceId) .add("type", shortType) .add("perms", perms.build()) - .add("format", format) - .add("description", description); + .add("format", format); + if (shortType.length() == type.length()) builder.add("description", description); if (isReadable) setJsonValue(builder, value); return builder; }); From 91b5a3caac574d6b50f100bd6f541e660f256494 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 14 Oct 2022 13:14:19 -0600 Subject: [PATCH 03/50] don't reset connections just cause an accessory was added or removed the caller is expected to updated the configuration index, at which point iOS will shortly request the new accessory information. --- CHANGES.md | 3 +++ .../io/github/hapjava/server/impl/HomekitRoot.java | 2 -- .../io/github/hapjava/server/impl/HomekitRootTest.java | 10 ++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index debc2dfbf..67f610bed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.3 +* Avoid unnecessary forced disconnects. Library users should be updating the configuration index anyway. + # HAP-Java 2.0.2 * Various minor stability issues for after an accessory has been removed. diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java index ca182f812..df77afd07 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java @@ -112,7 +112,6 @@ void addAccessorySkipRangeCheck(HomekitAccessory accessory) { } if (started) { registry.reset(); - webHandler.resetConnections(); } } @@ -130,7 +129,6 @@ public void removeAccessory(HomekitAccessory accessory) { } if (started) { registry.reset(); - webHandler.resetConnections(); } } diff --git a/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java b/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java index 3ac40288a..81d2666e5 100644 --- a/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java +++ b/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java @@ -91,20 +91,18 @@ public void testAdvertiserStops() throws Exception { } @Test - public void testAddAccessoryResetsWeb() { + public void testAddAccessoryDoesntResetWeb() { root.start(); - verify(webHandler, never()).resetConnections(); root.addAccessory(accessory); - verify(webHandler).resetConnections(); + verify(webHandler, never()).resetConnections(); } @Test - public void testRemoveAccessoryResetsWeb() { + public void testRemoveAccessoryDoesntResetWeb() { root.addAccessory(accessory); root.start(); - verify(webHandler, never()).resetConnections(); root.removeAccessory(accessory); - verify(webHandler).resetConnections(); + verify(webHandler, never()).resetConnections(); } @Test(expected = IndexOutOfBoundsException.class) From 0226861d9e71eddde518ee530e90b08a9f3353e6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 16:03:10 +0000 Subject: [PATCH 04/50] [maven-release-plugin] prepare release hap-2.0.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b682cf656..d58839503 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.3-SNAPSHOT + 2.0.3 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.3 From de16264232b68dabc8646cc9d619a435ee759488 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 16:03:11 +0000 Subject: [PATCH 05/50] [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 d58839503..7e7851c32 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.3 + 2.0.4-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.3 + HEAD From 0e4db0b6532fae8871684c7be8b821b75601394d Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 24 Oct 2022 13:47:10 -0600 Subject: [PATCH 06/50] keep subscriptions up to date when accessories are replaced but the connection is _not_ torn down also add the ability to modify accessories in a batch without having to regenerate all AIDs and IIDs (and other temporary data structures) at each intermediate step. You must use this mechanism if you plan on replacing an existing accessory with an equivalent one, but want to keep subscriptions intact. --- .../characteristics/Characteristic.java | 2 + .../impl/base/BaseCharacteristic.java | 6 + .../hapjava/server/impl/HomekitRegistry.java | 10 +- .../hapjava/server/impl/HomekitRoot.java | 44 ++-- .../impl/connections/SubscriptionManager.java | 231 +++++++++++++----- .../server/impl/json/EventController.java | 4 +- 6 files changed, 213 insertions(+), 84 deletions(-) diff --git a/src/main/java/io/github/hapjava/characteristics/Characteristic.java b/src/main/java/io/github/hapjava/characteristics/Characteristic.java index f73bf345b..d2e87030a 100644 --- a/src/main/java/io/github/hapjava/characteristics/Characteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/Characteristic.java @@ -18,6 +18,8 @@ * @author Andy Lintner */ public interface Characteristic { + /** @return The UUID type for this characteristic. */ + String getType(); /** * Adds an attribute to the passed JsonObjectBuilder named "value" with the current value of the 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 734a83cdc..1d2d55c5b 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 @@ -70,6 +70,12 @@ public BaseCharacteristic( this.unsubscriber = unsubscriber; } + @Override + /** {@inheritDoc} */ + public String getType() { + return type; + } + @Override /** {@inheritDoc} */ public final CompletableFuture toJson(int iid) { diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java index 39ec31ff9..cc6cd352e 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java @@ -2,6 +2,7 @@ import io.github.hapjava.accessories.HomekitAccessory; import io.github.hapjava.characteristics.Characteristic; +import io.github.hapjava.server.impl.connections.SubscriptionManager; import io.github.hapjava.services.Service; import io.github.hapjava.services.impl.AccessoryInformationService; import java.util.ArrayList; @@ -19,14 +20,16 @@ public class HomekitRegistry { private static final Logger logger = LoggerFactory.getLogger(HomekitRegistry.class); private final String label; + private final SubscriptionManager subscriptions; private final Map accessories; private final Map> services = new HashMap<>(); private final Map> characteristics = new HashMap<>(); private boolean isAllowUnauthenticatedRequests = false; - public HomekitRegistry(String label) { + public HomekitRegistry(String label, SubscriptionManager subscriptions) { this.label = label; + this.subscriptions = subscriptions; this.accessories = new ConcurrentHashMap<>(); reset(); } @@ -61,6 +64,7 @@ public synchronized void reset() { services.put(accessory, newServicesByInterfaceId); characteristics.put(accessory, newCharacteristicsByInterfaceId); } + subscriptions.resync(this); } public String getLabel() { @@ -87,8 +91,8 @@ public void add(HomekitAccessory accessory) { accessories.put(accessory.getId(), accessory); } - public void remove(HomekitAccessory accessory) { - accessories.remove(accessory.getId()); + public boolean remove(HomekitAccessory accessory) { + return accessories.remove(accessory.getId()) != null; } public boolean isAllowUnauthenticatedRequests() { diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java index df77afd07..a80f0723e 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java @@ -36,6 +36,8 @@ public class HomekitRoot { private final SubscriptionManager subscriptions = new SubscriptionManager(); private boolean started = false; private int configurationIndex = 1; + private int nestedBatches = 0; + private boolean madeChanges = false; HomekitRoot( String label, HomekitWebHandler webHandler, InetAddress host, HomekitAuthInfo authInfo) @@ -65,7 +67,7 @@ public class HomekitRoot { this.authInfo = authInfo; this.label = label; this.category = category; - this.registry = new HomekitRegistry(label); + this.registry = new HomekitRegistry(label, subscriptions); } HomekitRoot( @@ -83,11 +85,19 @@ public class HomekitRoot { this( label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns)); } + + public synchronized void batchUpdate() { + if (this.nestedBatches == 0) madeChanges = false; + ++this.nestedBatches; + } + + public synchronized void completeUpdateBatch() { + if (--this.nestedBatches == 0 && madeChanges) registry.reset(); + } + /** - * Add an accessory to be handled and advertised by this root. Any existing HomeKit connections - * will be terminated to allow the clients to reconnect and see the updated accessory list. When - * using this for a bridge, the ID of the accessory must be greater than 1, as that ID is reserved - * for the Bridge itself. + * Add an accessory to be handled and advertised by this root. When using this for a bridge, the + * ID of the accessory must be greater than 1, as that ID is reserved for the Bridge itself. * * @param accessory to advertise and handle. */ @@ -110,25 +120,28 @@ void addAccessorySkipRangeCheck(HomekitAccessory accessory) { if (logger.isTraceEnabled()) { accessory.getName().thenAccept(name -> logger.trace("Added accessory {}", name)); } - if (started) { + madeChanges = true; + if (started && nestedBatches == 0) { registry.reset(); } } /** - * Removes an accessory from being handled or advertised by this root. Any existing HomeKit - * connections will be terminated to allow the clients to reconnect and see the updated accessory - * list. + * Removes an accessory from being handled or advertised by this root. * * @param accessory accessory to cease advertising and handling */ public void removeAccessory(HomekitAccessory accessory) { - this.registry.remove(accessory); - if (logger.isTraceEnabled()) { - accessory.getName().thenAccept(name -> logger.trace("Removed accessory {}", name)); - } - if (started) { - registry.reset(); + if (this.registry.remove(accessory)) { + if (logger.isTraceEnabled()) { + accessory.getName().thenAccept(name -> logger.trace("Removed accessory {}", name)); + } + madeChanges = true; + if (started && nestedBatches == 0) { + registry.reset(); + } + } else { + accessory.getName().thenAccept(name -> logger.warn("Could not remove accessory {}", name)); } } @@ -140,6 +153,7 @@ public void removeAccessory(HomekitAccessory accessory) { */ public void start() { started = true; + madeChanges = false; registry.reset(); webHandler .start( diff --git a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java index 25f68526c..72ac27ba8 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java @@ -1,15 +1,18 @@ package io.github.hapjava.server.impl.connections; +import io.github.hapjava.characteristics.Characteristic; import io.github.hapjava.characteristics.EventableCharacteristic; +import io.github.hapjava.server.impl.HomekitRegistry; import io.github.hapjava.server.impl.http.HomekitClientConnection; import io.github.hapjava.server.impl.http.HttpResponse; import io.github.hapjava.server.impl.json.EventController; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,12 +20,22 @@ public class SubscriptionManager { private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionManager.class); - private final ConcurrentMap> subscriptions = - new ConcurrentHashMap<>(); - private final ConcurrentMap> reverse = - new ConcurrentHashMap<>(); - private final ConcurrentMap> - pendingNotifications = new ConcurrentHashMap<>(); + private static class ConnectionsWithIds { + Set connections; + int aid, iid; + + ConnectionsWithIds(int aid, int iid) { + this.aid = aid; + this.iid = iid; + this.connections = new HashSet<>(); + } + } + + private final Map subscriptions = new HashMap<>(); + private final Map> reverse = + new HashMap<>(); + private final Map> pendingNotifications = + new HashMap<>(); private int nestedBatches = 0; public synchronized void addSubscription( @@ -31,64 +44,75 @@ public synchronized void addSubscription( EventableCharacteristic characteristic, HomekitClientConnection connection) { synchronized (this) { - if (!subscriptions.containsKey(characteristic)) { - subscriptions.putIfAbsent(characteristic, newSet()); - } - subscriptions.get(characteristic).add(connection); - if (subscriptions.get(characteristic).size() == 1) { - characteristic.subscribe( - () -> { - publish(aid, iid, characteristic); - }); + ConnectionsWithIds subscribers; + if (subscriptions.containsKey(characteristic)) { + subscribers = subscriptions.get(characteristic); + } else { + subscribers = new ConnectionsWithIds(aid, iid); + subscriptions.put(characteristic, subscribers); + subscribe(aid, iid, characteristic); } + subscribers.connections.add(connection); if (!reverse.containsKey(connection)) { - reverse.putIfAbsent(connection, newSet()); + reverse.put(connection, new HashSet<>()); } reverse.get(connection).add(characteristic); - LOGGER.trace( - "Added subscription to " + characteristic.getClass() + " for " + connection.hashCode()); + LOGGER.debug( + "Added subscription to {}:{} ({}) for {}", + aid, + iid, + characteristic.getClass().getSimpleName(), + connection.hashCode()); } } public synchronized void removeSubscription( EventableCharacteristic characteristic, HomekitClientConnection connection) { - Set subscriptions = this.subscriptions.get(characteristic); - if (subscriptions != null) { - subscriptions.remove(connection); - if (subscriptions.size() == 0) { + ConnectionsWithIds subscribers = subscriptions.get(characteristic); + if (subscribers != null) { + subscribers.connections.remove(connection); + if (subscribers.connections.isEmpty()) { + LOGGER.debug("Unsubscribing from characteristic as all subscriptions are closed"); characteristic.unsubscribe(); + subscriptions.remove(characteristic); + } + + // Remove pending notifications for this no-longer-subscribed characteristic + List connectionNotifications = pendingNotifications.get(connection); + if (connectionNotifications != null) { + connectionNotifications.removeIf(n -> n.aid == subscribers.aid && n.iid == subscribers.iid); + if (connectionNotifications.isEmpty()) pendingNotifications.remove(connection); } + + LOGGER.debug( + "Removed subscription from {}:{} ({}) for {}", + subscribers.aid, + subscribers.iid, + characteristic.getClass().getSimpleName(), + connection.hashCode()); } Set reverse = this.reverse.get(connection); if (reverse != null) { reverse.remove(characteristic); + if (reverse.isEmpty()) this.reverse.remove(connection); } - LOGGER.trace( - "Removed subscription to " + characteristic.getClass() + " for " + connection.hashCode()); } public synchronized void removeConnection(HomekitClientConnection connection) { - Set characteristics = reverse.remove(connection); + removeConnection(connection, reverse.remove(connection)); + } + + private void removeConnection( + HomekitClientConnection connection, Set characteristics) { pendingNotifications.remove(connection); if (characteristics != null) { for (EventableCharacteristic characteristic : characteristics) { - Set characteristicSubscriptions = - subscriptions.get(characteristic); - characteristicSubscriptions.remove(connection); - if (characteristicSubscriptions.isEmpty()) { - LOGGER.trace("Unsubscribing from characteristic as all subscriptions are closed"); - characteristic.unsubscribe(); - subscriptions.remove(characteristic); - } + removeSubscription(characteristic, connection); } } - LOGGER.trace("Removed connection {}", connection.hashCode()); - } - - private Set newSet() { - return Collections.newSetFromMap(new ConcurrentHashMap()); + LOGGER.debug("Removed connection {}", connection.hashCode()); } public synchronized void batchUpdate() { @@ -96,31 +120,35 @@ public synchronized void batchUpdate() { } public synchronized void completeUpdateBatch() { - if (--this.nestedBatches == 0 && !pendingNotifications.isEmpty()) { - LOGGER.trace("Publishing batched changes"); - for (ConcurrentMap.Entry> entry : - pendingNotifications.entrySet()) { - try { - HttpResponse message = new EventController().getMessage(entry.getValue()); - entry.getKey().outOfBand(message); - } catch (Exception e) { - LOGGER.warn("Failed to create new event message", e); - } + if (--this.nestedBatches == 0) flushUpdateBatch(); + } + + private void flushUpdateBatch() { + if (pendingNotifications.isEmpty()) return; + + LOGGER.trace("Publishing batched changes"); + for (Map.Entry> entry : + pendingNotifications.entrySet()) { + try { + HttpResponse message = new EventController().getMessage(entry.getValue()); + entry.getKey().outOfBand(message); + } catch (Exception e) { + LOGGER.warn("Failed to create new event message", e); } - pendingNotifications.clear(); } + pendingNotifications.clear(); } public synchronized void publish(int accessoryId, int iid, EventableCharacteristic changed) { - final Set subscribers = subscriptions.get(changed); - if ((subscribers == null) || (subscribers.isEmpty())) { - LOGGER.debug("No subscribers to characteristic {} at accessory {} ", changed, accessoryId); + final ConnectionsWithIds subscribers = subscriptions.get(changed); + if (subscribers == null || subscribers.connections.isEmpty()) { + LOGGER.trace("No subscribers to characteristic {} at accessory {} ", changed, accessoryId); return; // no subscribers } if (nestedBatches != 0) { LOGGER.trace("Batching change for accessory {} and characteristic {} " + accessoryId, iid); PendingNotification notification = new PendingNotification(accessoryId, iid, changed); - for (HomekitClientConnection connection : subscribers) { + for (HomekitClientConnection connection : subscribers.connections) { if (!pendingNotifications.containsKey(connection)) { pendingNotifications.put(connection, new ArrayList()); } @@ -132,7 +160,7 @@ public synchronized void publish(int accessoryId, int iid, EventableCharacterist try { HttpResponse message = new EventController().getMessage(accessoryId, iid, changed); LOGGER.trace("Publishing change for " + accessoryId); - for (HomekitClientConnection connection : subscribers) { + for (HomekitClientConnection connection : subscribers.connections) { connection.outOfBand(message); } } catch (Exception e) { @@ -140,15 +168,90 @@ public synchronized void publish(int accessoryId, int iid, EventableCharacterist } } + /** + * The accessory registry has changed; go through all subscriptions and link to any new/changed + * characteristics + */ + public synchronized void resync(HomekitRegistry registry) { + LOGGER.debug("Resyncing subscriptions"); + flushUpdateBatch(); + + Map newSubscriptions = new HashMap<>(); + Iterator> i = + subscriptions.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry entry = i.next(); + EventableCharacteristic oldCharacteristic = entry.getKey(); + ConnectionsWithIds subscribers = entry.getValue(); + Characteristic newCharacteristic = + registry.getCharacteristics(subscribers.aid).get(subscribers.iid); + if (newCharacteristic == null || newCharacteristic.getType() != oldCharacteristic.getType()) { + // characteristic is gone or has completely changed; drop all subscriptions for it + LOGGER.debug( + "{}:{} ({}) has gone missing; dropping subscriptions.", + subscribers.aid, + subscribers.iid, + oldCharacteristic.getClass().getSimpleName()); + i.remove(); + for (HomekitClientConnection conn : subscribers.connections) { + removeSubscription(oldCharacteristic, conn); + } + } else if (newCharacteristic != oldCharacteristic) { + EventableCharacteristic newEventableCharacteristic = + (EventableCharacteristic) newCharacteristic; + LOGGER.debug( + "{}:{} has been replaced by a compatible characteristic; re-connecting subscriptions", + subscribers.aid, + subscribers.iid); + // characteristic has been replaced by another instance of the same thing; + // re-connect subscriptions + i.remove(); + oldCharacteristic.unsubscribe(); + subscribe(subscribers.aid, subscribers.iid, newEventableCharacteristic); + // we can't replace the key, and we can't add to the map while we're iterating, + // so save it off to a temporary map that we'll add them all at the end + newSubscriptions.put(newEventableCharacteristic, subscribers); + + for (HomekitClientConnection conn : subscribers.connections) { + Set subscribedCharacteristics = reverse.get(conn); + subscribedCharacteristics.remove(oldCharacteristic); + subscribedCharacteristics.add(newEventableCharacteristic); + + // and also update references for any pending notifications, so they'll get the proper + // value + List connectionPendingNotifications = pendingNotifications.get(conn); + if (connectionPendingNotifications != null) { + for (PendingNotification notification : connectionPendingNotifications) { + if (notification.characteristic == oldCharacteristic) { + notification.characteristic = newEventableCharacteristic; + } + } + } + } + } + } + subscriptions.putAll(newSubscriptions); + } + + private void subscribe(int aid, int iid, EventableCharacteristic characteristic) { + characteristic.subscribe( + () -> { + publish(aid, iid, characteristic); + }); + } + /** Remove all existing subscriptions */ - public void removeAll() { - LOGGER.trace("Removing {} reverse connections from subscription manager", reverse.size()); - Iterator i = reverse.keySet().iterator(); + public synchronized void removeAll() { + LOGGER.debug("Removing {} reverse connections from subscription manager", reverse.size()); + Iterator>> i = + reverse.entrySet().iterator(); while (i.hasNext()) { - HomekitClientConnection connection = i.next(); - LOGGER.trace("Removing connection {}", connection.hashCode()); - removeConnection(connection); + Map.Entry> entry = i.next(); + HomekitClientConnection connection = entry.getKey(); + LOGGER.debug("Removing connection {}", connection.hashCode()); + i.remove(); + removeConnection(connection, entry.getValue()); } - LOGGER.trace("Subscription sizes are {} and {}", reverse.size(), subscriptions.size()); + LOGGER.debug("Subscription sizes are {} and {}", reverse.size(), subscriptions.size()); } } diff --git a/src/main/java/io/github/hapjava/server/impl/json/EventController.java b/src/main/java/io/github/hapjava/server/impl/json/EventController.java index 41473f716..0c7c2bc7c 100644 --- a/src/main/java/io/github/hapjava/server/impl/json/EventController.java +++ b/src/main/java/io/github/hapjava/server/impl/json/EventController.java @@ -4,7 +4,7 @@ import io.github.hapjava.server.impl.connections.PendingNotification; import io.github.hapjava.server.impl.http.HttpResponse; import java.io.ByteArrayOutputStream; -import java.util.ArrayList; +import java.util.List; import javax.json.Json; import javax.json.JsonArrayBuilder; import javax.json.JsonObject; @@ -34,7 +34,7 @@ public HttpResponse getMessage(int accessoryId, int iid, EventableCharacteristic } } - public HttpResponse getMessage(ArrayList notifications) throws Exception { + public HttpResponse getMessage(List notifications) throws Exception { JsonArrayBuilder characteristics = Json.createArrayBuilder(); for (PendingNotification notification : notifications) { From a781e8518fb257bb9794b0ce47b5d00fb7cae64d Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 17 Nov 2022 14:04:46 -0700 Subject: [PATCH 07/50] address review comments --- .../hapjava/server/impl/HomekitRoot.java | 8 ++++++++ .../impl/connections/SubscriptionManager.java | 20 +++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java index a80f0723e..1750a8d3e 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java @@ -86,11 +86,19 @@ public class HomekitRoot { label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns)); } + /** + * Begin a batch update of accessories. + * + *

After calling this, you can call addAccessory() and removeAccessory() multiple times without + * causing HAP-Java to re-publishing the metadata to HomeKit. You'll need to call + * completeUpdateBatch in order to publish all accumulated changes. + */ public synchronized void batchUpdate() { if (this.nestedBatches == 0) madeChanges = false; ++this.nestedBatches; } + /** Publish accumulated accessory changes since batchUpdate() was called. */ public synchronized void completeUpdateBatch() { if (--this.nestedBatches == 0 && madeChanges) registry.reset(); } diff --git a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java index 72ac27ba8..9399295fb 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/SubscriptionManager.java @@ -58,7 +58,7 @@ public synchronized void addSubscription( reverse.put(connection, new HashSet<>()); } reverse.get(connection).add(characteristic); - LOGGER.debug( + LOGGER.trace( "Added subscription to {}:{} ({}) for {}", aid, iid, @@ -73,7 +73,7 @@ public synchronized void removeSubscription( if (subscribers != null) { subscribers.connections.remove(connection); if (subscribers.connections.isEmpty()) { - LOGGER.debug("Unsubscribing from characteristic as all subscriptions are closed"); + LOGGER.trace("Unsubscribing from characteristic as all subscriptions are closed"); characteristic.unsubscribe(); subscriptions.remove(characteristic); } @@ -85,7 +85,7 @@ public synchronized void removeSubscription( if (connectionNotifications.isEmpty()) pendingNotifications.remove(connection); } - LOGGER.debug( + LOGGER.trace( "Removed subscription from {}:{} ({}) for {}", subscribers.aid, subscribers.iid, @@ -112,7 +112,7 @@ private void removeConnection( removeSubscription(characteristic, connection); } } - LOGGER.debug("Removed connection {}", connection.hashCode()); + LOGGER.trace("Removed connection {}", connection.hashCode()); } public synchronized void batchUpdate() { @@ -173,7 +173,7 @@ public synchronized void publish(int accessoryId, int iid, EventableCharacterist * characteristics */ public synchronized void resync(HomekitRegistry registry) { - LOGGER.debug("Resyncing subscriptions"); + LOGGER.trace("Resyncing subscriptions"); flushUpdateBatch(); Map newSubscriptions = new HashMap<>(); @@ -187,7 +187,7 @@ public synchronized void resync(HomekitRegistry registry) { registry.getCharacteristics(subscribers.aid).get(subscribers.iid); if (newCharacteristic == null || newCharacteristic.getType() != oldCharacteristic.getType()) { // characteristic is gone or has completely changed; drop all subscriptions for it - LOGGER.debug( + LOGGER.trace( "{}:{} ({}) has gone missing; dropping subscriptions.", subscribers.aid, subscribers.iid, @@ -199,7 +199,7 @@ public synchronized void resync(HomekitRegistry registry) { } else if (newCharacteristic != oldCharacteristic) { EventableCharacteristic newEventableCharacteristic = (EventableCharacteristic) newCharacteristic; - LOGGER.debug( + LOGGER.trace( "{}:{} has been replaced by a compatible characteristic; re-connecting subscriptions", subscribers.aid, subscribers.iid); @@ -242,16 +242,16 @@ private void subscribe(int aid, int iid, EventableCharacteristic characteristic) /** Remove all existing subscriptions */ public synchronized void removeAll() { - LOGGER.debug("Removing {} reverse connections from subscription manager", reverse.size()); + LOGGER.trace("Removing {} reverse connections from subscription manager", reverse.size()); Iterator>> i = reverse.entrySet().iterator(); while (i.hasNext()) { Map.Entry> entry = i.next(); HomekitClientConnection connection = entry.getKey(); - LOGGER.debug("Removing connection {}", connection.hashCode()); + LOGGER.trace("Removing connection {}", connection.hashCode()); i.remove(); removeConnection(connection, entry.getValue()); } - LOGGER.debug("Subscription sizes are {} and {}", reverse.size(), subscriptions.size()); + LOGGER.trace("Subscription sizes are {} and {}", reverse.size(), subscriptions.size()); } } From a559f7c5408aacc0f35f8d0515a9df733dec3797 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Nov 2022 21:19:51 +0000 Subject: [PATCH 08/50] [maven-release-plugin] prepare release hap-2.0.4 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7e7851c32..e37ea6efc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.4-SNAPSHOT + 2.0.4 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.4 From 4038b55d5a38c450b7afacebc3ab9950b768363c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Nov 2022 21:19:53 +0000 Subject: [PATCH 09/50] [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 e37ea6efc..81102f179 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.4 + 2.0.5-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.4 + HEAD From 2656dfdec98b67aa85a274571c1516345ec00a30 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Wed, 23 Nov 2022 14:39:22 -0700 Subject: [PATCH 10/50] update github actions to setup-java@v3 no breaking changes --- .github/workflows/deploy.yml | 2 +- .github/workflows/maven.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba402983e..61753eab7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up JDK 11 for Maven Central - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c7f0c8de8..6f597043c 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e1ae1875..804e4b2bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' From f04a4d6345b6dd94f7ed19874f98b539742c9126 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 16 Dec 2022 14:22:29 -0700 Subject: [PATCH 11/50] implement List Pairings request also keep track of admin permissions for users note that those permissions aren't actually enforced yet --- CHANGES.md | 3 ++ .../hapjava/server/HomekitAuthInfo.java | 38 ++++++++++++++++++- .../server/impl/connections/HttpSession.java | 2 +- .../server/impl/pairing/FinalPairHandler.java | 3 +- .../server/impl/pairing/MessageType.java | 7 +++- .../server/impl/pairing/PairingMethod.java | 24 ++++++++++++ .../impl/pairing/PairingUpdateController.java | 36 ++++++++++++++++-- .../impl/pairing/TypeLengthValueUtils.java | 10 +++++ 8 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java diff --git a/CHANGES.md b/CHANGES.md index 67f610bed..402d3fcec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.5 +* Implement List-Pairings method. Compatibility with new Home infrastructure from iOS 16.2? + # HAP-Java 2.0.3 * Avoid unnecessary forced disconnects. Library users should be updating the configuration index anyway. diff --git a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java index 069bf7270..6441962d7 100644 --- a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java +++ b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java @@ -3,6 +3,8 @@ import io.github.hapjava.server.impl.HomekitServer; import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils; import java.math.BigInteger; +import java.util.Collection; +import java.util.List; /** * Authentication info that must be provided when constructing a new {@link HomekitServer}. You will @@ -65,8 +67,23 @@ default String getSetupId() { * @param username the iOS device's username. The value will not be meaningful to anything but * iOS. * @param publicKey the iOS device's public key. + * @param isAdmin if the user is an admin, authorized to and/remove other users */ - void createUser(String username, byte[] publicKey); + default void createUser(String username, byte[] publicKey, boolean isAdmin) { + createUser(username, publicKey); + } + + /** + * Deprecated method to add a user, assuming all users are admins. + * + *

At least one of the createUser methods must be implemented. + * + * @param username the iOS device's username. + * @param publicKey the iOS device's public key. + */ + default void createUser(String username, byte[] publicKey) { + createUser(username, publicKey, true); + } /** * Called when an iOS device needs to remove an existing pairing. Subsequent calls to {@link @@ -76,6 +93,15 @@ default String getSetupId() { */ void removeUser(String username); + /** + * List all users which have been authenticated. + * + * @return the previously stored list of users. + */ + default Collection listUsers() { + return List.of(); + } + /** * Called when an already paired iOS device is re-connecting. The public key returned by this * method will be compared with the signature of the pair verification request to validate the @@ -86,6 +112,16 @@ default String getSetupId() { */ byte[] getUserPublicKey(String username); + /** + * Determine if the specified user is an admin. + * + * @param username the username of the iOS device to retrieve permissions for. + * @return the previously stored permissions. + */ + default boolean userIsAdmin(String username) { + return true; + } + /** * Called to check if a user has been created. The homekit accessory advertises whether the * accessory has already been paired. At this time, it's unclear whether multiple users can be diff --git a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java index 36ac04085..08a49f64a 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java @@ -67,7 +67,7 @@ public HttpResponse handleRequest(HttpRequest request) throws IOException { public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOException { advertiser.setDiscoverable( - false); // brigde is already bound and should not be discoverable anymore + false); // bridge is already bound and should not be discoverable anymore try { switch (request.getUri()) { case "/accessories": diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java index e5d9e371c..9d464147a 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java @@ -65,7 +65,8 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro if (!new EdsaVerifier(ltpk).verify(completeData, proof)) { throw new Exception("Invalid signature"); } - authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk); + authInfo.createUser( + authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk, true); return createResponse(); } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java b/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java index ffdd0ba33..84991d130 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java @@ -9,7 +9,12 @@ public enum MessageType { ENCRYPTED_DATA(5), STATE(6), ERROR(7), - SIGNATURE(10); + SIGNATURE(0x0a), + PERMISSIONS(0x0b), + FRAGMENT_DATA(0x0c), + FRAGMENT_LAST(0x0d), + FLAGS(0x13), + SEPARATOR(0xff); private final short key; diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java new file mode 100644 index 000000000..07decf02d --- /dev/null +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java @@ -0,0 +1,24 @@ +package io.github.hapjava.server.impl.pairing; + +public enum PairingMethod { + PAIR_SETUP(0), + PAIR_SETUP_WITH_AUTH(1), + PAIR_VERIFY(2), + ADD_PAIRING(3), + REMOVE_PAIRING(4), + LIST_PAIRINGS(5); + + private final byte value; + + PairingMethod(byte value) { + this.value = value; + } + + PairingMethod(int value) { + this.value = (byte) value; + } + + public byte getValue() { + return value; + } +} diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java index 3d52fe907..abc73dfc1 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java @@ -7,6 +7,8 @@ import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; public class PairingUpdateController { @@ -22,19 +24,45 @@ public HttpResponse handle(HttpRequest request) throws IOException { DecodeResult d = TypeLengthValueUtils.decode(request.getBody()); int method = d.getByte(MessageType.METHOD); - if (method == 3) { // Add pairing + if (method == PairingMethod.ADD_PAIRING.getValue()) { byte[] username = d.getBytes(MessageType.USERNAME); byte[] ltpk = d.getBytes(MessageType.PUBLIC_KEY); - authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk); - } else if (method == 4) { // Remove pairing + byte permissions = d.getByte(MessageType.PERMISSIONS); + authInfo.createUser( + authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk, permissions == 1); + } else if (method == PairingMethod.REMOVE_PAIRING.getValue()) { byte[] username = d.getBytes(MessageType.USERNAME); authInfo.removeUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8)); if (!authInfo.hasUser()) { advertiser.setDiscoverable(true); } + } else if (method == PairingMethod.LIST_PAIRINGS.getValue()) { + TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder(); + + Collection usernames = authInfo.listUsers(); + boolean first = true; + Iterator iterator = usernames.iterator(); + while (iterator.hasNext()) { + String username = iterator.next(); + if (first) { + e.add(MessageType.STATE, (byte) 2); + first = false; + } else { + e.add(MessageType.SEPARATOR); + } + e.add(MessageType.USERNAME, username); + e.add(MessageType.PUBLIC_KEY, authInfo.getUserPublicKey(username)); + e.add(MessageType.PERMISSIONS, (short) (authInfo.userIsAdmin(username) ? 1 : 0)); + } + ; + + return new PairingResponse(e.toByteArray()); } else { throw new RuntimeException("Unrecognized method: " + method); } - return new PairingResponse(new byte[] {0x06, 0x01, 0x02}); + + TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder(); + e.add(MessageType.STATE, (byte) 2); + return new PairingResponse(e.toByteArray()); } } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java index 396829d34..e36f81af8 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -37,6 +38,11 @@ private Encoder() { baos = new ByteArrayOutputStream(); } + public void add(MessageType type) { + baos.write(type.getKey()); + baos.write(0); + } + public void add(MessageType type, BigInteger i) throws IOException { add(type, ByteUtils.toByteArray(i)); } @@ -58,6 +64,10 @@ public void add(MessageType type, byte[] bytes) throws IOException { } } + public void add(MessageType type, String string) throws IOException { + add(type, string.getBytes(StandardCharsets.UTF_8)); + } + public byte[] toByteArray() { return baos.toByteArray(); } From 9c6358873e30fff25296d728cf242a842089c5c1 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 17 Dec 2022 20:12:32 -0700 Subject: [PATCH 12/50] be sure to remove MAC from username before returning to iOS --- .../server/impl/pairing/PairingUpdateController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java index abc73dfc1..d9cdf2f05 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java @@ -42,6 +42,8 @@ public HttpResponse handle(HttpRequest request) throws IOException { Collection usernames = authInfo.listUsers(); boolean first = true; Iterator iterator = usernames.iterator(); + String mac = authInfo.getMac(); + while (iterator.hasNext()) { String username = iterator.next(); if (first) { @@ -50,9 +52,14 @@ public HttpResponse handle(HttpRequest request) throws IOException { } else { e.add(MessageType.SEPARATOR); } + byte[] publicKey = authInfo.getUserPublicKey(username); + boolean isAdmin = authInfo.userIsAdmin(username); + if (username.startsWith(mac)) { + username = username.substring(mac.length()); + } e.add(MessageType.USERNAME, username); - e.add(MessageType.PUBLIC_KEY, authInfo.getUserPublicKey(username)); - e.add(MessageType.PERMISSIONS, (short) (authInfo.userIsAdmin(username) ? 1 : 0)); + e.add(MessageType.PUBLIC_KEY, publicKey); + e.add(MessageType.PERMISSIONS, (short) (isAdmin ? 1 : 0)); } ; From d1d6fb09b4175cbabad181d2ee1f762b4111aaa9 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 19 Dec 2022 13:35:40 -0700 Subject: [PATCH 13/50] refactor pairing to match HAP spec more closely mostly renames, but significant change is that when we don't recognize a user, return an error code instead of throwing an exception and returning a 500 with no content. several responses are now generated with PairingResponse, rather than as a raw byte array, so that the reader can more easily follow what the response is. several more trace loggings showing the details of the pairing process have been added, for debug use also ensure that we check if the MAC has changed when refreshing auth info --- .../hapjava/server/impl/HomekitRoot.java | 1 + .../server/impl/connections/HttpSession.java | 28 ++++---- .../impl/jmdns/JmdnsHomekitAdvertiser.java | 11 +++ .../server/impl/pairing/ErrorCode.java | 26 +++++++ ...lPairHandler.java => ExchangeHandler.java} | 21 +++--- ...ringManager.java => PairSetupManager.java} | 27 ++++---- .../server/impl/pairing/PairSetupRequest.java | 67 +++++++++++++------ ...ionManager.java => PairVerifyManager.java} | 35 +++++----- ...ionRequest.java => PairVerifyRequest.java} | 35 +++++----- .../server/impl/pairing/PairingResponse.java | 21 ++++++ ...teController.java => PairingsManager.java} | 14 ++-- .../server/impl/pairing/SrpHandler.java | 16 ++--- .../hapjava/server/impl/pairing/Stage.java | 7 -- .../impl/pairing/TypeLengthValueUtils.java | 14 ++++ 14 files changed, 209 insertions(+), 114 deletions(-) create mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java rename src/main/java/io/github/hapjava/server/impl/pairing/{FinalPairHandler.java => ExchangeHandler.java} (84%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairingManager.java => PairSetupManager.java} (64%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairVerificationManager.java => PairVerifyManager.java} (82%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairVerificationRequest.java => PairVerifyRequest.java} (61%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairingUpdateController.java => PairingsManager.java} (89%) delete mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/Stage.java diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java index 1750a8d3e..36bfeefa2 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java @@ -197,6 +197,7 @@ public void stop() { * @throws IOException if there is an error in the underlying protocol, such as a TCP error */ public void refreshAuthInfo() throws IOException { + advertiser.setMac(authInfo.getMac()); advertiser.setDiscoverable(!authInfo.hasUser()); } diff --git a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java index 08a49f64a..f1c97c62c 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java @@ -9,9 +9,9 @@ import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser; import io.github.hapjava.server.impl.json.AccessoryController; import io.github.hapjava.server.impl.json.CharacteristicsController; -import io.github.hapjava.server.impl.pairing.PairVerificationManager; -import io.github.hapjava.server.impl.pairing.PairingManager; -import io.github.hapjava.server.impl.pairing.PairingUpdateController; +import io.github.hapjava.server.impl.pairing.PairSetupManager; +import io.github.hapjava.server.impl.pairing.PairVerifyManager; +import io.github.hapjava.server.impl.pairing.PairingsManager; import io.github.hapjava.server.impl.responses.InternalServerErrorResponse; import io.github.hapjava.server.impl.responses.NotFoundResponse; import java.io.IOException; @@ -21,8 +21,8 @@ class HttpSession { - private volatile PairingManager pairingManager; - private volatile PairVerificationManager pairVerificationManager; + private volatile PairSetupManager pairSetupManager; + private volatile PairVerifyManager pairVerifyManager; private volatile AccessoryController accessoryController; private volatile CharacteristicsController characteristicsController; @@ -84,7 +84,7 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc } case "/pairings": - return new PairingUpdateController(authInfo, advertiser).handle(request); + return new PairingsManager(authInfo, advertiser).handle(request); default: if (request.getUri().startsWith("/characteristics?")) { @@ -100,15 +100,15 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc } private HttpResponse handlePairSetup(HttpRequest request) { - if (pairingManager == null) { + if (pairSetupManager == null) { synchronized (HttpSession.class) { - if (pairingManager == null) { - pairingManager = new PairingManager(authInfo, registry); + if (pairSetupManager == null) { + pairSetupManager = new PairSetupManager(authInfo, registry); } } } try { - return pairingManager.handle(request); + return pairSetupManager.handle(request); } catch (Exception e) { logger.warn("Exception encountered during pairing", e); return new InternalServerErrorResponse(e); @@ -116,15 +116,15 @@ private HttpResponse handlePairSetup(HttpRequest request) { } private HttpResponse handlePairVerify(HttpRequest request) { - if (pairVerificationManager == null) { + if (pairVerifyManager == null) { synchronized (HttpSession.class) { - if (pairVerificationManager == null) { - pairVerificationManager = new PairVerificationManager(authInfo, registry); + if (pairVerifyManager == null) { + pairVerifyManager = new PairVerifyManager(authInfo, registry); } } } try { - return pairVerificationManager.handle(request); + return pairVerifyManager.handle(request); } catch (Exception e) { logger.warn("Exception encountered while verifying pairing", e); return new InternalServerErrorResponse(e); 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 1535a490a..9086acb3e 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 @@ -79,6 +79,17 @@ public synchronized void setDiscoverable(boolean discoverable) throws IOExceptio } } + public synchronized void setMac(String mac) throws IOException { + if (this.mac != mac) { + this.mac = mac; + if (isAdvertising) { + logger.trace("Re-creating service due to change in mac to " + mac); + unregisterService(); + registerService(); + } + } + } + public synchronized void setConfigurationIndex(int revision) throws IOException { if (this.configurationIndex != revision) { this.configurationIndex = revision; diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java b/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java new file mode 100644 index 000000000..90112fc46 --- /dev/null +++ b/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java @@ -0,0 +1,26 @@ +package io.github.hapjava.server.impl.pairing; + +public enum ErrorCode { + OK(0), + UNKNOWN(1), + AUTHENTICATION(2), + BACKOFF(3), + MAX_PEERS(4), + MAX_TRIES(5), + UNAVAILABLE(6), + BUSY(7); + + private final short key; + + ErrorCode(short key) { + this.key = key; + } + + ErrorCode(int key) { + this.key = (short) key; + } + + public short getKey() { + return key; + } +} diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java similarity index 84% rename from src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java rename to src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java index 9d464147a..2310975fa 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java @@ -6,22 +6,26 @@ import io.github.hapjava.server.impl.crypto.EdsaSigner; import io.github.hapjava.server.impl.crypto.EdsaVerifier; import io.github.hapjava.server.impl.http.HttpResponse; -import io.github.hapjava.server.impl.pairing.PairSetupRequest.Stage3Request; +import io.github.hapjava.server.impl.pairing.PairSetupRequest.ExchangeRequest; import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult; import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder; import java.nio.charset.StandardCharsets; import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.generators.HKDFBytesGenerator; import org.bouncycastle.crypto.params.HKDFParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -class FinalPairHandler { +class ExchangeHandler { private final byte[] k; private final HomekitAuthInfo authInfo; private byte[] hkdf_enc_key; - public FinalPairHandler(byte[] k, HomekitAuthInfo authInfo) { + private static final Logger LOGGER = LoggerFactory.getLogger(ExchangeHandler.class); + + public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) { this.k = k; this.authInfo = authInfo; } @@ -36,10 +40,10 @@ public HttpResponse handle(PairSetupRequest req) throws Exception { byte[] okm = hkdf_enc_key = new byte[32]; hkdf.generateBytes(okm, 0, 32); - return decrypt((Stage3Request) req, okm); + return decrypt((ExchangeRequest) req, okm); } - private HttpResponse decrypt(Stage3Request req, byte[] key) throws Exception { + 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()); @@ -63,10 +67,11 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro byte[] completeData = ByteUtils.joinBytes(okm, username, ltpk); if (!new EdsaVerifier(ltpk).verify(completeData, proof)) { - throw new Exception("Invalid signature"); + return new PairingResponse(6, ErrorCode.AUTHENTICATION); } - authInfo.createUser( - authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk, true); + String stringUsername = new String(username, StandardCharsets.UTF_8); + LOGGER.trace("Creating initial user {}", stringUsername); + authInfo.createUser(authInfo.getMac() + stringUsername, ltpk, true); return createResponse(); } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java similarity index 64% rename from src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java rename to src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java index af6a6e01f..05b37b8ab 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java @@ -9,48 +9,49 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PairingManager { +public class PairSetupManager { - private static final Logger logger = LoggerFactory.getLogger(PairingManager.class); + private static final Logger logger = LoggerFactory.getLogger(PairSetupManager.class); private final HomekitAuthInfo authInfo; private final HomekitRegistry registry; private SrpHandler srpHandler; - public PairingManager(HomekitAuthInfo authInfo, HomekitRegistry registry) { + public PairSetupManager(HomekitAuthInfo authInfo, HomekitRegistry registry) { this.authInfo = authInfo; this.registry = registry; } public HttpResponse handle(HttpRequest httpRequest) throws Exception { PairSetupRequest req = PairSetupRequest.of(httpRequest.getBody()); + logger.trace("Handling pair-setup request {}", req); - if (req.getStage() == Stage.ONE) { - logger.trace("Starting pair for " + registry.getLabel()); + if (req.getState() == 1) { + logger.trace("Received SRP Start Request " + registry.getLabel()); srpHandler = new SrpHandler(authInfo.getPin(), authInfo.getSalt()); return srpHandler.handle(req); - } else if (req.getStage() == Stage.TWO) { - logger.trace("Entering second stage of pair for " + registry.getLabel()); + } else if (req.getState() == 3) { + logger.trace("Receive SRP Verify Request for " + registry.getLabel()); if (srpHandler == null) { - logger.warn("Received unexpected stage 2 request for " + registry.getLabel()); + logger.warn("Received unexpected SRP Verify Request for " + registry.getLabel()); return new UnauthorizedResponse(); } else { try { return srpHandler.handle(req); } catch (Exception e) { srpHandler = null; // You don't get to try again - need a new key - logger.warn("Exception encountered while processing pairing request", e); + logger.warn("Exception encountered while processing SRP Verify Request", e); return new UnauthorizedResponse(); } } - } else if (req.getStage() == Stage.THREE) { - logger.trace("Entering third stage of pair for " + registry.getLabel()); + } else if (req.getState() == 5) { + logger.trace("Received Exchange Request for " + registry.getLabel()); if (srpHandler == null) { - logger.warn("Received unexpected stage 3 request for " + registry.getLabel()); + logger.warn("Received unexpected Exchanged Request for " + registry.getLabel()); return new UnauthorizedResponse(); } else { - FinalPairHandler handler = new FinalPairHandler(srpHandler.getK(), authInfo); + ExchangeHandler handler = new ExchangeHandler(srpHandler.getK(), authInfo); try { return handler.handle(req); } catch (Exception e) { 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 index bd0da4c81..4e7260fed 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java @@ -2,46 +2,61 @@ import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult; import java.math.BigInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; abstract class PairSetupRequest { - - private static final short VALUE_STAGE_1 = 1; - private static final short VALUE_STAGE_2 = 3; - private static final short VALUE_STAGE_3 = 5; + private static final Logger logger = LoggerFactory.getLogger(PairSetupRequest.class); public static PairSetupRequest of(byte[] content) throws Exception { DecodeResult d = TypeLengthValueUtils.decode(content); + logger.trace("Decoded pair setup request: {}", d); short stage = d.getByte(MessageType.STATE); switch (stage) { - case VALUE_STAGE_1: - return new Stage1Request(); + case 1: + return new SRPStartRequest(d); - case VALUE_STAGE_2: - return new Stage2Request(d); + case 3: + return new SRPVerifyRequest(d); - case VALUE_STAGE_3: - return new Stage3Request(d); + case 5: + return new ExchangeRequest(d); default: throw new Exception("Unknown pair process stage: " + stage); } } - public abstract Stage getStage(); + // Raw integer. + // State of the pairing process. 1=M1, 2=M2, etc. + public abstract int getState(); + + public static class SRPStartRequest extends PairSetupRequest { + int flags; + + public SRPStartRequest(DecodeResult d) { + flags = 0; + if (d.hasMessage(MessageType.FLAGS)) { + flags = d.getInt(MessageType.FLAGS); + } + } - public static class Stage1Request extends PairSetupRequest { @Override - public Stage getStage() { - return Stage.ONE; + public int getState() { + return 1; + } + + public String toString() { + return ""; } } - public static class Stage2Request extends PairSetupRequest { + public static class SRPVerifyRequest extends PairSetupRequest { private final BigInteger a; private final BigInteger m1; - public Stage2Request(DecodeResult d) { + public SRPVerifyRequest(DecodeResult d) { a = d.getBigInt(MessageType.PUBLIC_KEY); m1 = d.getBigInt(MessageType.PROOF); } @@ -55,17 +70,21 @@ public BigInteger getM1() { } @Override - public Stage getStage() { - return Stage.TWO; + public int getState() { + return 3; + } + + public String toString() { + return ""; } } - static class Stage3Request extends PairSetupRequest { + static class ExchangeRequest extends PairSetupRequest { private final byte[] messageData; private final byte[] authTagData; - public Stage3Request(DecodeResult d) { + public ExchangeRequest(DecodeResult d) { messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16]; authTagData = new byte[16]; d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0); @@ -81,8 +100,12 @@ public byte[] getAuthTagData() { } @Override - public Stage getStage() { - return Stage.THREE; + public int getState() { + return 5; + } + + public String toString() { + return ""; } } } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java similarity index 82% rename from src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java rename to src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java index d43b2d210..44f25c6d1 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java @@ -9,12 +9,11 @@ import io.github.hapjava.server.impl.crypto.EdsaVerifier; import io.github.hapjava.server.impl.http.HttpRequest; import io.github.hapjava.server.impl.http.HttpResponse; -import io.github.hapjava.server.impl.pairing.PairVerificationRequest.Stage1Request; -import io.github.hapjava.server.impl.pairing.PairVerificationRequest.Stage2Request; +import io.github.hapjava.server.impl.pairing.PairVerifyRequest.VerifyFinishRequest; +import io.github.hapjava.server.impl.pairing.PairVerifyRequest.VerifyStartRequest; import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult; import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder; import io.github.hapjava.server.impl.responses.NotFoundResponse; -import io.github.hapjava.server.impl.responses.OkResponse; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import org.bouncycastle.crypto.digests.SHA512Digest; @@ -23,9 +22,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PairVerificationManager { +public class PairVerifyManager { - private static final Logger logger = LoggerFactory.getLogger(PairVerificationManager.class); + private static final Logger logger = LoggerFactory.getLogger(PairVerifyManager.class); private static volatile SecureRandom secureRandom; private final HomekitAuthInfo authInfo; @@ -36,26 +35,26 @@ public class PairVerificationManager { private byte[] publicKey; private byte[] sharedSecret; - public PairVerificationManager(HomekitAuthInfo authInfo, HomekitRegistry registry) { + public PairVerifyManager(HomekitAuthInfo authInfo, HomekitRegistry registry) { this.authInfo = authInfo; this.registry = registry; } public HttpResponse handle(HttpRequest rawRequest) throws Exception { - PairVerificationRequest request = PairVerificationRequest.of(rawRequest.getBody()); - switch (request.getStage()) { - case ONE: - return stage1((Stage1Request) request); + PairVerifyRequest request = PairVerifyRequest.of(rawRequest.getBody()); + switch (request.getState()) { + case 1: + return handleVerifyStartRequest((VerifyStartRequest) request); - case TWO: - return stage2((Stage2Request) request); + case 3: + return handleVerifyFinishRequest((VerifyFinishRequest) request); default: return new NotFoundResponse(); } } - private HttpResponse stage1(Stage1Request request) throws Exception { + private HttpResponse handleVerifyStartRequest(VerifyStartRequest request) throws Exception { logger.trace("Starting pair verification for " + registry.getLabel()); clientPublicKey = request.getClientPublicKey(); publicKey = new byte[32]; @@ -96,7 +95,7 @@ private HttpResponse stage1(Stage1Request request) throws Exception { return new PairingResponse(encoder.toByteArray()); } - private HttpResponse stage2(Stage2Request request) throws Exception { + private HttpResponse handleVerifyFinishRequest(VerifyFinishRequest request) throws Exception { ChachaDecoder chacha = new ChachaDecoder(hkdfKey, "PV-Msg03".getBytes(StandardCharsets.UTF_8)); byte[] plaintext = chacha.decodeCiphertext(request.getAuthTagData(), request.getMessageData()); @@ -110,7 +109,8 @@ private HttpResponse stage2(Stage2Request request) throws Exception { authInfo.getUserPublicKey( authInfo.getMac() + new String(clientUsername, StandardCharsets.UTF_8)); if (clientLtpk == null) { - throw new Exception("Unknown user: " + new String(clientUsername, StandardCharsets.UTF_8)); + logger.warn("Unknown user: {}", new String(clientUsername, StandardCharsets.UTF_8)); + return new PairingResponse(4, ErrorCode.AUTHENTICATION); } Encoder encoder = TypeLengthValueUtils.getEncoder(); @@ -122,9 +122,8 @@ private HttpResponse stage2(Stage2Request request) throws Exception { createKey("Control-Write-Encryption-Key"), createKey("Control-Read-Encryption-Key")); } else { - encoder.add(MessageType.ERROR, (short) 4); logger.warn("Invalid signature. Could not pair " + registry.getLabel()); - return new OkResponse(encoder.toByteArray()); + return new PairingResponse(4, ErrorCode.AUTHENTICATION); } } @@ -142,7 +141,7 @@ private byte[] createKey(String info) { private static SecureRandom getSecureRandom() { if (secureRandom == null) { - synchronized (PairVerificationManager.class) { + synchronized (PairVerifyManager.class) { if (secureRandom == null) { secureRandom = new SecureRandom(); } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java similarity index 61% rename from src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java rename to src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java index 3bb6627d9..7cf31585b 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java @@ -2,33 +2,32 @@ import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult; -abstract class PairVerificationRequest { +abstract class PairVerifyRequest { - private static final short VALUE_STAGE_1 = 1; - private static final short VALUE_STAGE_2 = 3; - - static PairVerificationRequest of(byte[] content) throws Exception { + static PairVerifyRequest of(byte[] content) throws Exception { DecodeResult d = TypeLengthValueUtils.decode(content); short stage = d.getByte(MessageType.STATE); switch (stage) { - case VALUE_STAGE_1: - return new Stage1Request(d); + case 1: + return new VerifyStartRequest(d); - case VALUE_STAGE_2: - return new Stage2Request(d); + case 3: + return new VerifyFinishRequest(d); default: throw new Exception("Unknown pair process stage: " + stage); } } - abstract Stage getStage(); + // Raw integer. + // State of the pairing process. 1=M1, 2=M2, etc. + abstract int getState(); - static class Stage1Request extends PairVerificationRequest { + static class VerifyStartRequest extends PairVerifyRequest { private final byte[] clientPublicKey; - public Stage1Request(DecodeResult d) { + public VerifyStartRequest(DecodeResult d) { clientPublicKey = d.getBytes(MessageType.PUBLIC_KEY); } @@ -37,17 +36,17 @@ public byte[] getClientPublicKey() { } @Override - Stage getStage() { - return Stage.ONE; + int getState() { + return 1; } } - static class Stage2Request extends PairVerificationRequest { + static class VerifyFinishRequest extends PairVerifyRequest { private final byte[] messageData; private final byte[] authTagData; - public Stage2Request(DecodeResult d) { + public VerifyFinishRequest(DecodeResult d) { messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16]; authTagData = new byte[16]; d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0); @@ -63,8 +62,8 @@ public byte[] getAuthTagData() { } @Override - public Stage getStage() { - return Stage.TWO; + public int getState() { + return 3; } } } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java index af678a86c..defc5798f 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java @@ -21,6 +21,27 @@ public PairingResponse(byte[] body) { super(body); } + public PairingResponse(int state) { + this(encodeSuccess(state)); + } + + public PairingResponse(int state, ErrorCode errorCode) { + this(encodeError(state, errorCode)); + } + + private static byte[] encodeSuccess(int state) { + TypeLengthValueUtils.Encoder encoder = TypeLengthValueUtils.getEncoder(); + encoder.add(MessageType.STATE, (byte) state); + return encoder.toByteArray(); + } + + private static byte[] encodeError(int state, ErrorCode errorCode) { + TypeLengthValueUtils.Encoder encoder = TypeLengthValueUtils.getEncoder(); + encoder.add(MessageType.STATE, (byte) state); + encoder.add(MessageType.ERROR, (byte) errorCode.getKey()); + return encoder.toByteArray(); + } + @Override public Map getHeaders() { return headers; diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java similarity index 89% rename from src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java rename to src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java index d9cdf2f05..11eb4a709 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java @@ -9,13 +9,17 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Iterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class PairingUpdateController { +public class PairingsManager { private final HomekitAuthInfo authInfo; private final JmdnsHomekitAdvertiser advertiser; - public PairingUpdateController(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) { + private static final Logger LOGGER = LoggerFactory.getLogger(PairingsManager.class); + + public PairingsManager(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) { this.authInfo = authInfo; this.advertiser = advertiser; } @@ -24,6 +28,7 @@ public HttpResponse handle(HttpRequest request) throws IOException { DecodeResult d = TypeLengthValueUtils.decode(request.getBody()); int method = d.getByte(MessageType.METHOD); + if (method == PairingMethod.ADD_PAIRING.getValue()) { byte[] username = d.getBytes(MessageType.USERNAME); byte[] ltpk = d.getBytes(MessageType.PUBLIC_KEY); @@ -61,15 +66,12 @@ public HttpResponse handle(HttpRequest request) throws IOException { e.add(MessageType.PUBLIC_KEY, publicKey); e.add(MessageType.PERMISSIONS, (short) (isAdmin ? 1 : 0)); } - ; return new PairingResponse(e.toByteArray()); } else { throw new RuntimeException("Unrecognized method: " + method); } - TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder(); - e.add(MessageType.STATE, (byte) 2); - return new PairingResponse(e.toByteArray()); + return new PairingResponse(2); } } diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java index e02ccdb90..5c1c23353 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java @@ -3,7 +3,7 @@ import com.nimbusds.srp6.*; import io.github.hapjava.server.impl.http.HttpResponse; import io.github.hapjava.server.impl.pairing.HomekitSRP6ServerSession.State; -import io.github.hapjava.server.impl.pairing.PairSetupRequest.Stage2Request; +import io.github.hapjava.server.impl.pairing.PairSetupRequest.SRPVerifyRequest; import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder; import io.github.hapjava.server.impl.responses.ConflictResponse; import io.github.hapjava.server.impl.responses.NotFoundResponse; @@ -39,19 +39,19 @@ public SrpHandler(String pin, BigInteger salt) { } public HttpResponse handle(PairSetupRequest request) throws Exception { - switch (request.getStage()) { - case ONE: - return step1(); + switch (request.getState()) { + case 1: + return handleSrpStartRequest(); - case TWO: - return step2((Stage2Request) request); + case 3: + return handleSrpVerifyRequest((SRPVerifyRequest) request); default: return new NotFoundResponse(); } } - private HttpResponse step1() throws Exception { + private HttpResponse handleSrpStartRequest() throws Exception { if (session.getState() != State.INIT) { logger.warn("Session is not in state INIT when receiving step1"); return new ConflictResponse(); @@ -68,7 +68,7 @@ private HttpResponse step1() throws Exception { return new PairingResponse(encoder.toByteArray()); } - private HttpResponse step2(Stage2Request request) throws Exception { + private HttpResponse handleSrpVerifyRequest(SRPVerifyRequest request) throws Exception { if (session.getState() != State.STEP_1) { logger.warn("Session is not in state Stage 1 when receiving step2"); return new ConflictResponse(); diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java b/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java deleted file mode 100644 index 19bac9a14..000000000 --- a/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.hapjava.server.impl.pairing; - -public enum Stage { - ONE, - TWO, - THREE -} diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java index e36f81af8..cbd76f118 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -78,10 +79,23 @@ public static final class DecodeResult { private DecodeResult() {} + public String toString() { + return result.toString(); + } + + public boolean hasMessage(MessageType type) { + return result.containsKey(type.getKey()); + } + public byte getByte(MessageType type) { return result.get(type.getKey())[0]; } + public int getInt(MessageType type) { + ByteBuffer wrapped = ByteBuffer.wrap(result.get(type.getKey())); + return wrapped.getInt(); + } + public BigInteger getBigInt(MessageType type) { return new BigInteger(1, result.get(type.getKey())); } From 036e05a82cd7c50b52c339200ec06af81c30d262 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 19 Dec 2022 15:19:42 -0700 Subject: [PATCH 14/50] log HTTP method with requests --- .../github/hapjava/server/impl/connections/ConnectionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ab2ff4444..4869712e1 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 @@ -59,7 +59,7 @@ private HttpResponse doHandleRequest(HttpRequest request) throws IOException { readKey = ((UpgradeResponse) response).getReadKey().array(); writeKey = ((UpgradeResponse) response).getWriteKey().array(); } - LOGGER.trace(response.getStatusCode() + " " + request.getUri()); + LOGGER.trace("{} {} {}", response.getStatusCode(), request.getMethod(), request.getUri()); return response; } From e7cd4835a503b039e08737200c35468d3316d5f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:56:06 +0000 Subject: [PATCH 15/50] [maven-release-plugin] prepare release hap-2.0.5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 81102f179..473e00750 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.5-SNAPSHOT + 2.0.5 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.5 From 331f75b77bea738ccf1836982b658d8cb18a432f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:56:09 +0000 Subject: [PATCH 16/50] [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 473e00750..42afd6b12 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.5 + 2.0.6-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.5 + HEAD From ec4cd274f6a9eab14d71ccad71457af0cbed1541 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 30 Dec 2022 10:26:17 -0700 Subject: [PATCH 17/50] allow library users to get the value of characteristics themselves so they can also generically get the value, regardless of how they decided to fulfill it, without having to also keep track themselves --- .../impl/base/BaseCharacteristic.java | 2 +- .../impl/base/BooleanCharacteristic.java | 2 +- .../impl/base/EnumCharacteristic.java | 13 ++++++++++++- .../impl/base/FloatCharacteristic.java | 2 +- .../impl/base/IntegerCharacteristic.java | 2 +- .../impl/base/StaticStringCharacteristic.java | 2 +- .../impl/base/StringCharacteristic.java | 2 +- 7 files changed, 18 insertions(+), 7 deletions(-) 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 1d2d55c5b..9e57de098 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 @@ -190,7 +190,7 @@ public void unsubscribe() { * * @return a future that will complete with the current value. */ - protected abstract CompletableFuture getValue(); + public abstract CompletableFuture getValue(); /** * Supplies a default value for the characteristic to send to connected clients when the real 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 3e07936a8..5e7f16ad7 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 @@ -60,7 +60,7 @@ protected Boolean convert(JsonValue jsonValue) { } @Override - protected CompletableFuture getValue() { + public CompletableFuture getValue() { return getter.isPresent() ? getter.map(booleanGetter -> booleanGetter.get()).get() : null; } 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 df3287de5..881ef91e2 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 @@ -84,8 +84,19 @@ protected Integer convert(JsonValue jsonValue) { } } + /** + * @return the current value of this characteristic, or null if it has no value or can't be + * fetched + */ + public CompletableFuture getEnumValue() { + if (!getter.isPresent()) { + return null; + } + return getter.get().get(); + } + @Override - protected CompletableFuture getValue() { + public CompletableFuture getValue() { if (!getter.isPresent()) { return null; } 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 a8a98dc27..e38d310c7 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 @@ -94,7 +94,7 @@ protected Double convert(JsonValue jsonValue) { * the constructor. */ @Override - protected final CompletableFuture getValue() { + public final CompletableFuture getValue() { if (!getter.isPresent()) { return null; } 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 22a6dce83..0c05fb4f7 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 @@ -71,7 +71,7 @@ protected CompletableFuture makeBuilder(int iid) { } @Override - protected CompletableFuture getValue() { + public CompletableFuture getValue() { return getter.map(integerGetter -> integerGetter.get()).orElse(null); } diff --git a/src/main/java/io/github/hapjava/characteristics/impl/base/StaticStringCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/base/StaticStringCharacteristic.java index 576df7e0e..fc5961d4d 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/base/StaticStringCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/StaticStringCharacteristic.java @@ -59,7 +59,7 @@ public void setValue(String value) throws Exception { /** {@inheritDoc} */ @Override - protected CompletableFuture getValue() { + public CompletableFuture getValue() { return getter.map(stringGetter -> stringGetter.get()).orElse(null); } 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 65cf45a71..53196efb9 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 @@ -68,7 +68,7 @@ public void setValue(String value) throws Exception { /** {@inheritDoc} */ @Override - protected CompletableFuture getValue() { + public CompletableFuture getValue() { return getter.map(stringGetter -> stringGetter.get()).orElse(null); } From 2a5e1c013f17f59e261cc8662c241148338baff7 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 30 Dec 2022 10:31:46 -0700 Subject: [PATCH 18/50] don't add an extra AccessoryInformationService if the accessory already provided one --- .../github/hapjava/accessories/HomekitAccessory.java | 12 ++++++++++-- .../github/hapjava/server/impl/HomekitRegistry.java | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java b/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java index 5e2bbe3b6..64c406492 100644 --- a/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java +++ b/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java @@ -4,6 +4,7 @@ import io.github.hapjava.services.impl.AccessoryInformationService; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.concurrent.CompletableFuture; /** @@ -81,11 +82,18 @@ default Collection getServices() { }; /** - * default implementation which is sufficient if accessory has only one services + * default implementation which is sufficient if accessory has only one service * * @return primary service */ default Service getPrimaryService() { - return getServices().isEmpty() ? null : getServices().iterator().next(); + Iterator iterator = getServices().iterator(); + while (iterator.hasNext()) { + Service service = iterator.next(); + if (!(service instanceof AccessoryInformationService)) { + return service; + } + } + return null; }; } diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java index cc6cd352e..3e3be9b6b 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java @@ -42,8 +42,11 @@ public synchronized void reset() { List newServices; try { newServices = new ArrayList<>(2); - newServices.add(new AccessoryInformationService(accessory)); - for (Service service : accessory.getServices()) { + Collection services = accessory.getServices(); + if (!services.stream().anyMatch(s -> s instanceof AccessoryInformationService)) { + newServices.add(new AccessoryInformationService(accessory)); + } + for (Service service : services) { newServices.add(service); newServices.addAll(service.getLinkedServices()); } From b40277150fb21378a92138aa24c915c0200d1285 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 30 Dec 2022 11:46:04 -0700 Subject: [PATCH 19/50] allow setting values directly via characteristics --- .../impl/base/BaseCharacteristic.java | 2 +- .../impl/base/BooleanCharacteristic.java | 2 +- .../impl/base/EnumCharacteristic.java | 12 ++++++++++-- .../impl/base/FloatCharacteristic.java | 2 +- .../impl/base/IntegerCharacteristic.java | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) 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 9e57de098..38d1d8e6d 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 @@ -183,7 +183,7 @@ public void unsubscribe() { * @param value the new value to set. * @throws Exception if the value cannot be set. */ - protected abstract void setValue(T value) throws Exception; + public abstract void setValue(T value) throws Exception; /** * 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 5e7f16ad7..4b484eb85 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 @@ -65,7 +65,7 @@ public CompletableFuture getValue() { } @Override - protected void setValue(Boolean value) throws Exception { + public void setValue(Boolean value) throws Exception { if (setter.isPresent()) setter.get().accept(value); } 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 881ef91e2..65e82747f 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 @@ -103,8 +103,16 @@ public CompletableFuture getValue() { return getter.get().get().thenApply(T::getCode); } + public void setValue(T value) throws Exception { + if (!setter.isPresent()) { + return; + } + + setter.get().accept(value); + } + @Override - protected void setValue(Integer value) throws Exception { + public void setValue(Integer value) throws Exception { if (!setter.isPresent()) { return; } @@ -113,7 +121,7 @@ protected void setValue(Integer value) throws Exception { if (validValues != null && value != null) { for (T valid : validValues) { if (valid.getCode() == value) { - setter.get().accept(valid); + setValue(valid); 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 e38d310c7..f504322a0 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 @@ -129,7 +129,7 @@ public final CompletableFuture getValue() { } @Override - protected void setValue(Double value) throws Exception { + public void setValue(Double value) throws Exception { if (setter.isPresent()) setter.get().accept(value); } 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 0c05fb4f7..6d1b7df6d 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 @@ -76,7 +76,7 @@ public CompletableFuture getValue() { } @Override - protected void setValue(Integer value) throws Exception { + public void setValue(Integer value) throws Exception { if (setter.isPresent()) setter.get().accept(value); } From ae4a8ecc43b2c72bb48ade78caa01e09394326fd Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Thu, 5 Jan 2023 18:08:41 -0700 Subject: [PATCH 20/50] check for AccessoryInformationService by UUID, not by class in case someone is using some form of decorator or replacement class, but is still exposing the proper type --- .../java/io/github/hapjava/server/impl/HomekitRegistry.java | 3 ++- .../hapjava/services/impl/AccessoryInformationService.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java index 3e3be9b6b..3c70cfb9e 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRegistry.java @@ -43,7 +43,8 @@ public synchronized void reset() { try { newServices = new ArrayList<>(2); Collection services = accessory.getServices(); - if (!services.stream().anyMatch(s -> s instanceof AccessoryInformationService)) { + if (!services.stream() + .anyMatch(s -> s.getType().equals(AccessoryInformationService.TYPE))) { newServices.add(new AccessoryInformationService(accessory)); } for (Service service : services) { diff --git a/src/main/java/io/github/hapjava/services/impl/AccessoryInformationService.java b/src/main/java/io/github/hapjava/services/impl/AccessoryInformationService.java index 8aa7f3635..1fdb30d2a 100644 --- a/src/main/java/io/github/hapjava/services/impl/AccessoryInformationService.java +++ b/src/main/java/io/github/hapjava/services/impl/AccessoryInformationService.java @@ -14,6 +14,7 @@ /** Accessory Information service. */ public class AccessoryInformationService extends AbstractServiceImpl { + public static final String TYPE = "0000003E-0000-1000-8000-0026BB765291"; public AccessoryInformationService( IdentifyCharacteristic identify, @@ -22,7 +23,7 @@ public AccessoryInformationService( NameCharacteristic name, SerialNumberCharacteristic serialNumber, FirmwareRevisionCharacteristic firmwareRevision) { - super("0000003E-0000-1000-8000-0026BB765291"); + super(TYPE); addCharacteristic(identify); addCharacteristic(manufacturer); addCharacteristic(model); From 77db50c29945843f9c559553093755acb5ddf5a3 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 9 Jan 2023 12:25:28 -0700 Subject: [PATCH 21/50] improve ProgrammableSwitchEvent * fix typo ProgrammableSwitchEnum * allow restricting valid values --- CHANGES.md | 5 +++++ .../impl/base/EnumCharacteristic.java | 13 +++++++++++-- .../impl/common/ProgrammableSwitchEnum.java | 2 +- .../ProgrammableSwitchEventCharacteristic.java | 10 +++++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 402d3fcec..ab4bc7e4f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +# HAP-Java 2.0.6 +* Several methods allowing library users to manipulate characteristics themselves +* Allow library users to provider their own implementation of AccessoryInformationService +* Fix ProgrammableSwitchEventEnum that has an incorrect value + # HAP-Java 2.0.5 * Implement List-Pairings method. Compatibility with new Home infrastructure from iOS 16.2? 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 65e82747f..0032b4301 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 @@ -26,7 +26,7 @@ public abstract class EnumCharacteristic extends BaseCharacteristic { private final T[] validValues; - Optional>> getter; + protected Optional>> getter; protected Optional> setter; /** @@ -100,7 +100,16 @@ public CompletableFuture getValue() { if (!getter.isPresent()) { return null; } - return getter.get().get().thenApply(T::getCode); + return getter + .get() + .get() + .thenApply( + e -> { + if (e == null) { + return null; + } + return e.getCode(); + }); } public void setValue(T value) throws Exception { diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEnum.java index 12f600f92..988472a0c 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEnum.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEnum.java @@ -9,7 +9,7 @@ public enum ProgrammableSwitchEnum implements CharacteristicEnum { SINGLE_PRESS(0), DOUBLE_PRESS(1), - LONG_PRESS(3); + LONG_PRESS(2); private static final Map reverse; diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEventCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEventCharacteristic.java index 83dcbb2dc..b19430100 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEventCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ProgrammableSwitchEventCharacteristic.java @@ -21,10 +21,18 @@ public ProgrammableSwitchEventCharacteristic( Supplier> getter, Consumer subscriber, Runnable unsubscriber) { + this(ProgrammableSwitchEnum.values(), getter, subscriber, unsubscriber); + } + + public ProgrammableSwitchEventCharacteristic( + ProgrammableSwitchEnum[] validValues, + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { super( "00000073-0000-1000-8000-0026BB765291", "Switch Event", - ProgrammableSwitchEnum.values(), + validValues, Optional.of(getter), Optional.empty(), Optional.of(subscriber), From 0039adb97b4f8240aaf8912ef1eb17d9d459259a Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 9 Jan 2023 12:25:09 -0700 Subject: [PATCH 22/50] more direct access to characteristic data --- .../impl/base/EnumCharacteristic.java | 4 ++++ .../impl/base/FloatCharacteristic.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) 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 65e82747f..e3ba9ae46 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 @@ -137,4 +137,8 @@ public Integer getDefault() { } return 0; } + + public T[] getValidValues() { + return validValues; + } } 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 f504322a0..727cea645 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 @@ -138,4 +138,16 @@ public void setValue(Double value) throws Exception { public Double getDefault() { return minValue; } + + public double getMinValue() { + return minValue; + } + + public double getMaxValue() { + return maxValue; + } + + public double getMinStep() { + return minStep; + } } From 975485f2be3db04dac4302a374719802daa712e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 19:53:37 +0000 Subject: [PATCH 23/50] [maven-release-plugin] prepare release hap-2.0.6 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 42afd6b12..7a1762ffe 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.6-SNAPSHOT + 2.0.6 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.6 From 2e75f47a3a3c9ba97e88c327062c1c0809b093db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 19:53:39 +0000 Subject: [PATCH 24/50] [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 7a1762ffe..29db614f1 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.6 + 2.0.7-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.6 + HEAD 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 25/50] 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 26/50] 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 27/50] [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 28/50] [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 29/50] 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 30/50] 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 31/50] 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 32/50] 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 33/50] 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 34/50] [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 35/50] [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 36/50] 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 37/50] 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 38/50] 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 39/50] 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 40/50] 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 41/50] [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 42/50] [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 43/50] 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 44/50] 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 45/50] 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 46/50] 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 47/50] 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 48/50] 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 49/50] 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 50/50] 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