diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 61753eab7..3206d8a70 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -7,6 +7,10 @@ on:
push:
branches: [ master ]
+permissions:
+ contents: write
+ packages: write
+
jobs:
build:
@@ -14,26 +18,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..29bfe0e59 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 }} -Dgoals=deploy
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/.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=
diff --git a/CHANGES.md b/CHANGES.md
index ab4bc7e4f..ab4bb1202 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,21 @@
+# HAP-Java 2.0.10
+* 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)
+* 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
+* Updated netty to 4.1.126.Final
+
+# 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/pom.xml b/pom.xml
index ffd5798a3..7bbbb9021 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,13 +5,13 @@
hap
hap-java
Homekit Accessory Protocol for Java
- 2.0.8-SNAPSHOT
+ 2.0.10-SNAPSHOT
jar
https://github.com/hap-java/HAP-Java
UTF-8
- 4.1.72.Final
+ 4.1.139.Final
@@ -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
@@ -156,7 +162,7 @@
org.assertj
assertj-core
- 3.19.0
+ 3.27.7
test
@@ -272,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
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..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;
@@ -77,7 +124,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/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/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/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/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/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/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/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/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/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/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/ExchangeHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
old mode 100644
new mode 100755
index 2310975fa..6bae3ab84
--- a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
@@ -44,8 +44,17 @@ public HttpResponse handle(PairSetupRequest req) 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());
+ 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 +63,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() {
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;
+ }
}
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/characteristics/impl/CustomTiltAngleRangeTest.java b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java
new file mode 100644
index 000000000..d3cd51756
--- /dev/null
+++ b/src/test/java/io/github/hapjava/characteristics/impl/CustomTiltAngleRangeTest.java
@@ -0,0 +1,78 @@
+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.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 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 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);
+ }
+ }
+}
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");
+ }
+}
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);
+ }
+}
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());
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);
+ }
+}