From d0d4f270fb969b20bd5bde0a4a510a59e58a8344 Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 3 Feb 2022 22:42:25 +0100 Subject: [PATCH 01/73] add support for television service Signed-off-by: Eugen Freiter --- CHANGES.md | 25 ++++ .../accessories/InputSourceAccessory.java | 104 +++++++++++++ .../accessories/TelevisionAccessory.java | 123 ++++++++++++++++ .../TelevisionSpeakerAccessory.java | 43 ++++++ .../AccessoryWithActive.java | 38 +++++ .../AccessoryWithClosedCaptions.java | 37 +++++ .../AccessoryWithCurrentMediaState.java | 30 ++++ .../AccessoryWithIdentifier.java | 17 +++ .../AccessoryWithInputDeviceType.java | 29 ++++ .../AccessoryWithPictureMode.java | 39 +++++ .../AccessoryWithPowerMode.java | 16 ++ .../AccessoryWithTargetMediaState.java | 37 +++++ .../AccessoryWithTargetVisibilityState.java | 36 +++++ .../AccessoryWithVolumeControlType.java | 30 ++++ .../AccessoryWithVolumeSelector.java | 20 +++ .../impl/base/StringCharacteristic.java | 80 ++++++++++ .../ActiveIdentifierCharacteristic.java | 29 ++++ .../common/ConfiguredNameCharacteristic.java | 27 ++++ .../impl/common/IdentifierCharacteristic.java | 22 +++ .../impl/common/VersionCharacteristic.java | 2 +- .../CurrentVisibilityStateCharacteristic.java | 27 ++++ .../CurrentVisibilityStateEnum.java | 36 +++++ .../InputDeviceTypeCharacteristic.java | 26 ++++ .../impl/inputsource/InputDeviceTypeEnum.java | 38 +++++ .../InputSourceTypeCharacteristic.java | 26 ++++ .../impl/inputsource/InputSourceTypeEnum.java | 43 ++++++ .../TargetVisibilityStateCharacteristic.java | 29 ++++ .../TargetVisibilityStateEnum.java | 34 +++++ .../ClosedCaptionsCharacteristic.java | 31 ++++ .../impl/television/ClosedCaptionsEnum.java | 35 +++++ .../CurrentMediaStateCharacteristic.java | 28 ++++ .../television/CurrentMediaStateEnum.java | 37 +++++ .../television/PictureModeCharacteristic.java | 30 ++++ .../impl/television/PictureModeEnum.java | 44 ++++++ .../television/PowerModeCharacteristic.java | 20 +++ .../impl/television/PowerModeEnum.java | 35 +++++ .../television/RemoteKeyCharacteristic.java | 22 +++ .../impl/television/RemoteKeyEnum.java | 45 ++++++ .../SleepDiscoveryModeCharacteristic.java | 28 ++++ .../television/SleepDiscoveryModeEnum.java | 35 +++++ .../TargetMediaStateCharacteristic.java | 27 ++++ .../impl/television/TargetMediaStateEnum.java | 36 +++++ .../VolumeControlTypeCharacteristic.java | 28 ++++ .../VolumeControlTypeEnum.java | 36 +++++ .../VolumeSelectorCharacteristic.java | 22 +++ .../televisionspeaker/VolumeSelectorEnum.java | 34 +++++ .../services/impl/AbstractServiceImpl.java | 7 + .../services/impl/InputSourceService.java | 91 ++++++++++++ .../services/impl/TelevisionService.java | 139 ++++++++++++++++++ .../impl/TelevisionSpeakerService.java | 75 ++++++++++ 50 files changed, 1927 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java create mode 100644 src/main/java/io/github/hapjava/services/impl/InputSourceService.java create mode 100644 src/main/java/io/github/hapjava/services/impl/TelevisionService.java create mode 100644 src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java diff --git a/CHANGES.md b/CHANGES.md index a3a55a290..bb5668235 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,31 @@ ## Fixes * Log accessory names instead of futures. [#150](https://github.com/hap-java/HAP-Java/issues/150) * Fix rotation speed data type (BREAKING API CHANGE). According to HAP specification it must be float +* Fix UUID of HAP Version characteristic + +## New +* New characteristics: + * Identifier + * Input Device Type + * Input Source Type + * Configured Name + * Current Visibility State + * Target Visibility State + * Sleep Discovery Mode + * Active Identifier + * Closed Captions + * Current Media State + * Target Media State + * Picture Mode + * Power Mode + * Remote Key + * Volume Control Type + * Volume Selector + +* New services + * Input Source + * Television + * Television Speaker # HAP-Java 2.0.0 * major refactoring to support optional characteristics diff --git a/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java b/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java new file mode 100644 index 000000000..cfa5ae3d6 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java @@ -0,0 +1,104 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum; +import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateEnum; +import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.InputSourceService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Input Source accessory. */ +public interface InputSourceAccessory extends HomekitAccessory { + + /** + * Retrieves configured name of input source. + * + * @return configured name of input source + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the configured name. + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in configured name. + * + * @param callback the function to call when the configured name changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the configured name. */ + void unsubscribeConfiguredName(); + + /** + * Retrieves the flag whether input source is configured. + * + * @return a future that will contain the flag . + */ + CompletableFuture isConfigured(); + /** + * set the flag whether input source is configured. + * + * @param state is configured state + * @return a future that completes when the change is made + */ + CompletableFuture setIsConfigured(IsConfiguredEnum state); + + /** + * Subscribes to changes in isConfigured. + * + * @param callback the function to call when the state changes. + */ + void subscribeIsConfigured(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in isConfigured. */ + void unsubscribeIsConfigured(); + + /** + * Retrieves the input source type. + * + * @return a future that will contain the input source type. + */ + CompletableFuture getInputSourceType(); + + /** + * Subscribes to changes in input source type. + * + * @param callback the function to call when the type changes. + */ + void subscribeInputSourceType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the input source type. */ + void unsubscribeInputSourceType(); + + /** + * Retrieves the current visibility state. + * + * @return a future that will contain the current visibility state. + */ + CompletableFuture getCurrentVisibilityState(); + + /** + * Subscribes to changes in current visibility state. + * + * @param callback the function to call when the state changes. + */ + void subscribeCurrentVisibilityState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current visibility state. */ + void unsubscribeCurrentVisibilityState(); + + @Override + default Collection getServices() { + return Collections.singleton(new InputSourceService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java new file mode 100644 index 000000000..3a1a1c515 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java @@ -0,0 +1,123 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.RemoteKeyEnum; +import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.TelevisionService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Television accessory. */ +public interface TelevisionAccessory extends HomekitAccessory { + + /** + * Retrieves the current active state of the TV. + * + * @return a future that will contain the state + */ + CompletableFuture isActive(); + + /** + * Sets the active state of the TV + * + * @param state the state to set + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setActive(boolean state) throws Exception; + + /** + * Subscribes to changes in the active state of the TV . + * + * @param callback the function to call when the active state changes. + */ + void subscribeActive(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active state of the TV. */ + void unsubscribeActive(); + + /** + * Retrieves the active identifier + * + * @return a future that will contain the active identifier. + */ + CompletableFuture getActiveIdentifier(); + + /** + * Sets the active identifier + * + * @param identifier the active identifier + * @return a future that completes when the active identifier is changed + * @throws Exception when the active identifier cannot be set + */ + CompletableFuture setActiveIdentifier(Integer identifier) throws Exception; + + /** + * Subscribes to changes in the active identifier. + * + * @param callback the function to call when the active identifier changes. + */ + void subscribeActiveIdentifier(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active identifier. */ + void unsubscribeActiveIdentifier(); + + /** + * Retrieves configured name. + * + * @return configured name + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the mute status + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeConfiguredName(); + + /** + * Sends the remote key. + * + * @param key remote key + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setRemoteKey(RemoteKeyEnum key) throws Exception; + + /** + * Retrieves the sleep discovery mode. + * + * @return a future that will contain the sleep discovery mode . + */ + CompletableFuture getSleepDiscoveryMode(); + + /** + * Subscribes to changes in sleep discovery mode. + * + * @param callback the function to call when the sleep discovery mode changes. + */ + void subscribeSleepDiscoveryMode(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the sleep discovery mode. */ + void unsubscribeSleepDiscoveryMode(); + + @Override + default Collection getServices() { + return Collections.singleton(new TelevisionService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java new file mode 100644 index 000000000..28df1437d --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java @@ -0,0 +1,43 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.TelevisionSpeakerService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Television speaker accessory. */ +public interface TelevisionSpeakerAccessory extends HomekitAccessory { + + /** + * Retrieves mute status. + * + * @return true if accessory is muted + */ + CompletableFuture isMuted(); + + /** + * Sets the mute status + * + * @param mute true if accessory should be muted + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setMute(boolean mute) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeMuteState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeMuteState(); + + @Override + default Collection getServices() { + return Collections.singleton(new TelevisionSpeakerService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java new file mode 100644 index 000000000..4ad11be63 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java @@ -0,0 +1,38 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.common.ActiveEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with active characteristic {@link + * io.github.hapjava.characteristics.impl.common.ActiveCharacteristic}. + */ +public interface AccessoryWithActive { + + /** + * Retrieves the active state (see {@link + * io.github.hapjava.characteristics.impl.common.ActiveEnum} for supported values). + * + * @return a future that will contain the active state + */ + CompletableFuture getActive(); + + /** + * Set the active state (see {@link ActiveEnum} for supported values). + * + * @param active active state + * @return a future that completes when the change is made + */ + CompletableFuture setActive(ActiveEnum active); + + /** + * Subscribes to changes in the active state. + * + * @param callback the function to call when the active state changes. + */ + void subscribeActive(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active state. */ + void unsubscribeActive(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java new file mode 100644 index 000000000..c13da3289 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.ClosedCaptionsEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with closed captions characteristic {@link + * io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic}. + */ +public interface AccessoryWithClosedCaptions { + + /** + * Retrieves the closed captions state (see {@link ClosedCaptionsEnum} for supported values). + * + * @return a future that will contain the closed captions + */ + CompletableFuture getClosedCaptions(); + + /** + * Set the closed captions state (see {@link ClosedCaptionsEnum} for supported values). + * + * @param closedCaptions closed captions + * @return a future that completes when the change is made + */ + CompletableFuture setClosedCaptions(ClosedCaptionsEnum closedCaptions); + + /** + * Subscribes to changes in the closed captions. + * + * @param callback the function to call when the closed captions changes. + */ + void subscribeClosedCaptions(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the closed captions. */ + void unsubscribeClosedCaptions(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java new file mode 100644 index 000000000..22350d9b4 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java @@ -0,0 +1,30 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with current media state characteristic {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic}. + */ +public interface AccessoryWithCurrentMediaState { + + /** + * Retrieves the current media state (see {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum} for supported values). + * + * @return a future that will contain the current media state + */ + CompletableFuture getCurrentMediaState(); + + /** + * Subscribes to changes in the current media state. + * + * @param callback the function to call when the current media state changes. + */ + void subscribeCurrentMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current media state. */ + void unsubscribeCurrentMediaState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java new file mode 100644 index 000000000..b181d4d99 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java @@ -0,0 +1,17 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with identifier characteristic {@link + * io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic}. + */ +public interface AccessoryWithIdentifier { + + /** + * Retrieves the identifier of service. + * + * @return a future with the identifier + */ + CompletableFuture getIdentifier(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java new file mode 100644 index 000000000..fca5f5df1 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java @@ -0,0 +1,29 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with input devices, e.g. television. {@link + * io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic} + */ +public interface AccessoryWithInputDeviceType { + + /** + * Retrieves the input device type. See {@link InputDeviceTypeEnum} for supported values. + * + * @return a future with the value + */ + CompletableFuture getInputDeviceType(); + + /** + * Subscribes to changes in input device type. + * + * @param callback the function when the input device type changes + */ + void subscribeInputDeviceType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes */ + void unsubscribeInputDeviceType(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java new file mode 100644 index 000000000..b369b98bf --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java @@ -0,0 +1,39 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.PictureModeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with picture mode characteristic {@link + * io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic}. + */ +public interface AccessoryWithPictureMode { + + /** + * Retrieves the picture mode (see {@link + * io.github.hapjava.characteristics.impl.television.PictureModeEnum} for supported values). + * + * @return a future that will contain the picture mode + */ + CompletableFuture getPictureMode(); + + /** + * Set the picture mode (see {@link + * io.github.hapjava.characteristics.impl.television.PictureModeEnum} for supported values). + * + * @param pictureMode picture mode + * @return a future that completes when the change is made + */ + CompletableFuture setPictureMode(PictureModeEnum pictureMode); + + /** + * Subscribes to changes in the picture mode. + * + * @param callback the function to call when the picture mode changes. + */ + void subscribePictureMode(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the picture mode. */ + void unsubscribePictureMode(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java new file mode 100644 index 000000000..5abb7e6f1 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java @@ -0,0 +1,16 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.impl.television.PowerModeEnum; +import java.util.concurrent.CompletableFuture; + +/** An accessory with power mode. */ +public interface AccessoryWithPowerMode { + + /** + * Set the power mode. See {@link PowerModeEnum} for supported values + * + * @param mode target power mode + * @return a future that completes when the change is made + */ + CompletableFuture setPowerMode(PowerModeEnum mode); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java new file mode 100644 index 000000000..e1f1a34bd --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with target media state characteristic {@link + * io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic}. + */ +public interface AccessoryWithTargetMediaState { + + /** + * Retrieves the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @return a future that will contain the target media state + */ + CompletableFuture getTargetMediaState(); + + /** + * Set the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @param targetMediaState target media state + * @return a future that completes when the change is made + */ + CompletableFuture setTargetMediaState(TargetMediaStateEnum targetMediaState); + + /** + * Subscribes to changes in the target media state. + * + * @param callback the function to call when the target media state changes. + */ + void subscribeTargetMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target media state. */ + void unsubscribeTargetMediaState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java new file mode 100644 index 000000000..dcadd1cc5 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java @@ -0,0 +1,36 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateEnum; +import java.util.concurrent.CompletableFuture; + +/** accessory with target visibility state characteristics. */ +public interface AccessoryWithTargetVisibilityState { + + /** + * Retrieves the target visibility state. + * + * @return a future that will contain the target visibility state + */ + CompletableFuture getTargetVisibilityState(); + + /** + * Sets the target visibility state + * + * @param state the target visibility state to set + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setTargetVisibilityState(TargetVisibilityStateEnum state) + throws Exception; + + /** + * Subscribes to changes in t target visibility state. + * + * @param callback the function to call when the target visibility state changes. + */ + void subscribeTargetVisibilityState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target visibility state. */ + void unsubscribeTargetVisibilityState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java new file mode 100644 index 000000000..21a6f6c66 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java @@ -0,0 +1,30 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with volume control type {@link + * io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic} + */ +public interface AccessoryWithVolumeControlType { + + /** + * Retrieves the current volume control type. see {@link VolumeControlTypeEnum} for possible + * values + * + * @return a future that will contain the type. + */ + CompletableFuture getVolumeControlType(); + + /** + * Subscribes to changes in the volume. + * + * @param callback the function to call when the state changes. + */ + void subscribeVolumeControlType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the volume. */ + void unsubscribeVolumeControlType(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java new file mode 100644 index 000000000..0c2a2890f --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java @@ -0,0 +1,20 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with volume selector {@link + * io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic} + */ +public interface AccessoryWithVolumeSelector { + + /** + * Sets the volume selector + * + * @param value the volume selector + * @return a future that completes when the volume selector is changed + * @throws Exception when the volume selector cannot be set + */ + CompletableFuture setVolumeSelector(VolumeSelectorEnum value) throws Exception; +} 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 new file mode 100644 index 000000000..4373b06c9 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java @@ -0,0 +1,80 @@ +package io.github.hapjava.characteristics.impl.base; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; + +/** + * A characteristic that provides an string value. + * + * @author Eugen Freiter + */ +public class StringCharacteristic extends BaseCharacteristic { + private final Optional>> getter; + private final Optional> setter; + /** + * Default constructor + * + * @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 description a description of the characteristic to be passed to the consuming device. + * @param getter getter to retrieve the value + * @param setter setter for value + * @param subscriber subscriber to subscribe to changes + * @param unsubscriber unsubscriber to unsubscribe from chnages + */ + public StringCharacteristic( + String type, + String description, + Optional>> getter, + Optional> setter, + Optional> subscriber, + Optional unsubscriber) { + super( + type, + "string", + description, + getter.isPresent(), + setter.isPresent(), + subscriber, + unsubscriber); + this.getter = getter; + this.setter = setter; + } + + /** {@inheritDoc} */ + @Override + protected CompletableFuture makeBuilder(int iid) { + return super.makeBuilder(iid); + } + + /** {@inheritDoc} */ + @Override + public String convert(JsonValue jsonValue) { + return ((JsonString) jsonValue).getString(); + } + + /** {@inheritDoc} */ + @Override + public void setValue(String value) throws Exception { + setter.get().accept(value); + } + + /** {@inheritDoc} */ + @Override + protected CompletableFuture getValue() { + return getter.map(stringGetter -> stringGetter.get()).get(); + } + + /** {@inheritDoc} */ + @Override + protected String getDefault() { + return "Unknown"; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java new file mode 100644 index 000000000..587c1b666 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java @@ -0,0 +1,29 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class ActiveIdentifierCharacteristic extends IntegerCharacteristic { + + public ActiveIdentifierCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E7-0000-1000-8000-0026BB765291", + "active identifier", + 0, + 1000, + "", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java new file mode 100644 index 000000000..471d5a368 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.StringCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes a configured name. */ +public class ConfiguredNameCharacteristic extends StringCharacteristic { + + public ConfiguredNameCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E3-0000-1000-8000-0026BB765291", + "configured name", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java new file mode 100644 index 000000000..4b8f7613c --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class IdentifierCharacteristic extends IntegerCharacteristic { + + public IdentifierCharacteristic(Supplier> getter) { + super( + "000000E6-0000-1000-8000-0026BB765291", + "identifier", + 0, + 1000, + "", + Optional.of(getter), + Optional.empty(), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java index 038c46c5c..75c8aaf9e 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java @@ -10,7 +10,7 @@ public class VersionCharacteristic extends StaticStringCharacteristic { public VersionCharacteristic(Supplier> getter) { super( - "00000023-0000-1000-8000-0026BB765291", + "00000037-0000-1000-8000-0026BB765291", "HAP version", Optional.of(getter), Optional.empty(), diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java new file mode 100644 index 000000000..65c358a3f --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes current visibility state. */ +public class CurrentVisibilityStateCharacteristic + extends EnumCharacteristic { + + public CurrentVisibilityStateCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000135-0000-1000-8000-0026BB765291", + "current visibility state", + CurrentVisibilityStateEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java new file mode 100644 index 000000000..2159393d8 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum CurrentVisibilityStateEnum implements CharacteristicEnum { + SHOWN(0), + HIDDEN(1), + UNKNOWN_1(2), + UNKNOWN_2(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(CurrentVisibilityStateEnum.values()) + .collect(Collectors.toMap(CurrentVisibilityStateEnum::getCode, t -> t)); + } + + public static CurrentVisibilityStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + CurrentVisibilityStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java new file mode 100644 index 000000000..51a6fbde5 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java @@ -0,0 +1,26 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes input device type. */ +public class InputDeviceTypeCharacteristic extends EnumCharacteristic { + + public InputDeviceTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DC-0000-1000-8000-0026BB765291", + "input device type", + InputDeviceTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java new file mode 100644 index 000000000..5c7337174 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java @@ -0,0 +1,38 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum InputDeviceTypeEnum implements CharacteristicEnum { + OTHER(0), + TV(1), + RECORDING(2), + TUNER(3), + PLAYBACK(4), + AUDIO_SYSTEM(5); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(InputDeviceTypeEnum.values()) + .collect(Collectors.toMap(InputDeviceTypeEnum::getCode, t -> t)); + } + + public static InputDeviceTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + InputDeviceTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java new file mode 100644 index 000000000..91134891b --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java @@ -0,0 +1,26 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes input source type. */ +public class InputSourceTypeCharacteristic extends EnumCharacteristic { + + public InputSourceTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DB-0000-1000-8000-0026BB765291", + "input source type", + InputSourceTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java new file mode 100644 index 000000000..94545ec18 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java @@ -0,0 +1,43 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum InputSourceTypeEnum implements CharacteristicEnum { + OTHER(0), + HOME_SCREEN(1), + TUNER(2), + HDMI(3), + COMPOSITE_VIDEO(4), + S_VIDEO(5), + COMPONENT_VIDEO(6), + DVI(7), + AIRPLAY(8), + USB(9), + APPLICATION(10); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(InputSourceTypeEnum.values()) + .collect(Collectors.toMap(InputSourceTypeEnum::getCode, t -> t)); + } + + public static InputSourceTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + InputSourceTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java new file mode 100644 index 000000000..ea466d182 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java @@ -0,0 +1,29 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes target visibility state. */ +public class TargetVisibilityStateCharacteristic + extends EnumCharacteristic { + + public TargetVisibilityStateCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000134-0000-1000-8000-0026BB765291", + "target visibility state", + TargetVisibilityStateEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java new file mode 100644 index 000000000..6a75b0c30 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java @@ -0,0 +1,34 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum TargetVisibilityStateEnum implements CharacteristicEnum { + SHOWN(0), + HIDDEN(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(TargetVisibilityStateEnum.values()) + .collect(Collectors.toMap(TargetVisibilityStateEnum::getCode, t -> t)); + } + + public static TargetVisibilityStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + TargetVisibilityStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java new file mode 100644 index 000000000..a963ca222 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java @@ -0,0 +1,31 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Characteristic indicates whether the service provides closed captions. See {@link + * ClosedCaptionsEnum} for possible values. + */ +public class ClosedCaptionsCharacteristic extends EnumCharacteristic { + + public ClosedCaptionsCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DD-0000-1000-8000-0026BB765291", + "closed captions", + ClosedCaptionsEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java new file mode 100644 index 000000000..2eb994149 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Disabled” 1 ”Enabled” 2-255 ”Reserved” */ +public enum ClosedCaptionsEnum implements CharacteristicEnum { + DISABLED(0), + ENABLED(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(ClosedCaptionsEnum.values()) + .collect(Collectors.toMap(ClosedCaptionsEnum::getCode, t -> t)); + } + + public static ClosedCaptionsEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + ClosedCaptionsEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java new file mode 100644 index 000000000..ca72c4ee9 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates current media state. See {@link CurrentMediaStateEnum} for possible + * values. + */ +public class CurrentMediaStateCharacteristic extends EnumCharacteristic { + public CurrentMediaStateCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E0-0000-1000-8000-0026BB765291", + "current media state", + CurrentMediaStateEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java new file mode 100644 index 000000000..83836c4c6 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java @@ -0,0 +1,37 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Play” 1 ”Pause” 2 "Stop" 3 "Unknown" 4-255 ”Reserved” */ +public enum CurrentMediaStateEnum implements CharacteristicEnum { + PLAY(0), + PAUSE(1), + STOP(2), + UNKNOWN(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(CurrentMediaStateEnum.values()) + .collect(Collectors.toMap(CurrentMediaStateEnum::getCode, t -> t)); + } + + public static CurrentMediaStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + CurrentMediaStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java new file mode 100644 index 000000000..7efefa23a --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java @@ -0,0 +1,30 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic retrieves / sets picture mode. See {@link PictureModeEnum} for possible + * values. + */ +public class PictureModeCharacteristic extends EnumCharacteristic { + public PictureModeCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E2-0000-1000-8000-0026BB765291", + "picture mode", + PictureModeEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java new file mode 100644 index 000000000..ac387cf65 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java @@ -0,0 +1,44 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 0 ”Other” 1 ”Standard” 2 "Calibrated" 3 "CalibratedDark" 4 "Vivid" 5 "Game" 6 "Computer" 7 + * "Custom" 8-255 ”Reserved” + */ +public enum PictureModeEnum implements CharacteristicEnum { + OTHER(0), + STANDARD(1), + CALIBRATED(2), + CALIBRATED_DARK(3), + VIVID(4), + GAME(5), + COMPUTER(6), + CUSTOM(7); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(PictureModeEnum.values()) + .collect(Collectors.toMap(PictureModeEnum::getCode, t -> t)); + } + + public static PictureModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + PictureModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java new file mode 100644 index 000000000..cebbe4124 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java @@ -0,0 +1,20 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** characteristic sets power mode. See {@link PowerModeEnum} for possible values. */ +public class PowerModeCharacteristic extends EnumCharacteristic { + + public PowerModeCharacteristic(ExceptionalConsumer setter) { + super( + "000000DF-0000-1000-8000-0026BB765291", + "power mode selection", + PowerModeEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java new file mode 100644 index 000000000..a171ba135 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Show” 1 ”Hide” 2-255 ”Reserved” */ +public enum PowerModeEnum implements CharacteristicEnum { + SHOW(0), + HIDE(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(PowerModeEnum.values()) + .collect(Collectors.toMap(PowerModeEnum::getCode, t -> t)); + } + + public static PowerModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + PowerModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java new file mode 100644 index 000000000..fe028d10d --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** + * This characteristic sends information about pressed key on tv remote. See {@link RemoteKeyEnum} + * for possible values. + */ +public class RemoteKeyCharacteristic extends EnumCharacteristic { + public RemoteKeyCharacteristic(ExceptionalConsumer setter) { + super( + "000000E1-0000-1000-8000-0026BB765291", + "remote key", + RemoteKeyEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} 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 new file mode 100644 index 000000000..443c23644 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java @@ -0,0 +1,45 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum RemoteKeyEnum implements CharacteristicEnum { + REWIND(0), + FAST_FORWARD(1), + NEXT_TRACK(2), + PREV_TRACK(3), + ARROW_UP(4), + ARROW_DOWN(5), + ARROW_LEFT(6), + ARROW_RIGHT(7), + SELECT(8), + BACK(9), + EXIT(10), + PLAY_PAUSE(11), + INFO(115); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(RemoteKeyEnum.values()) + .collect(Collectors.toMap(RemoteKeyEnum::getCode, t -> t)); + } + + public static RemoteKeyEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + RemoteKeyEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java new file mode 100644 index 000000000..2a7f41829 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates whether the service can be discovered. See {@link + * SleepDiscoveryModeEnum} for possible values. + */ +public class SleepDiscoveryModeCharacteristic extends EnumCharacteristic { + public SleepDiscoveryModeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E8-0000-1000-8000-0026BB765291", + "sleep discovery mode", + SleepDiscoveryModeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java new file mode 100644 index 000000000..da1377b9a --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Not discoverable” 1 ”Always discoverable” 2-255 ”Reserved” */ +public enum SleepDiscoveryModeEnum implements CharacteristicEnum { + NOT_DISCOVERABLE(0), + ALWAYS_DISCOVERABLE(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(SleepDiscoveryModeEnum.values()) + .collect(Collectors.toMap(SleepDiscoveryModeEnum::getCode, t -> t)); + } + + public static SleepDiscoveryModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + SleepDiscoveryModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java new file mode 100644 index 000000000..8909bb1ea --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic sets media state. See {@link TargetMediaStateEnum} for possible values. */ +public class TargetMediaStateCharacteristic extends EnumCharacteristic { + public TargetMediaStateCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000137-0000-1000-8000-0026BB765291", + "current media state", + TargetMediaStateEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java new file mode 100644 index 000000000..b6f6633ff --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Play” 1 ”Pause” 2 "Stop" 3-255 ”Reserved” */ +public enum TargetMediaStateEnum implements CharacteristicEnum { + PLAY(0), + PAUSE(1), + STOP(2); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(TargetMediaStateEnum.values()) + .collect(Collectors.toMap(TargetMediaStateEnum::getCode, t -> t)); + } + + public static TargetMediaStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + TargetMediaStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java new file mode 100644 index 000000000..c6d18a858 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates volume control type. See {@link VolumeControlTypeEnum} for possible + * values. + */ +public class VolumeControlTypeCharacteristic extends EnumCharacteristic { + public VolumeControlTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E9-0000-1000-8000-0026BB765291", + "volume control type", + VolumeControlTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java new file mode 100644 index 000000000..dfeb24636 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum VolumeControlTypeEnum implements CharacteristicEnum { + NONE(0), + RELATIVE(1), + RELATIVE_WITH_CURRENT(2), + ABSOLUTE(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(VolumeControlTypeEnum.values()) + .collect(Collectors.toMap(VolumeControlTypeEnum::getCode, t -> t)); + } + + public static VolumeControlTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + VolumeControlTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java new file mode 100644 index 000000000..8f2f84e7b --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** + * This characteristic sends information about volume changes. See {@link VolumeSelectorEnum} for + * possible values. + */ +public class VolumeSelectorCharacteristic extends EnumCharacteristic { + public VolumeSelectorCharacteristic(ExceptionalConsumer setter) { + super( + "000000EA-0000-1000-8000-0026BB765291", + "volume selector", + VolumeSelectorEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java new file mode 100644 index 000000000..63bc60e50 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java @@ -0,0 +1,34 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum VolumeSelectorEnum implements CharacteristicEnum { + INCREMENT(0), + DECREMENT(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(VolumeSelectorEnum.values()) + .collect(Collectors.toMap(VolumeSelectorEnum::getCode, t -> t)); + } + + public static VolumeSelectorEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + VolumeSelectorEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java index 25a44099d..2848d367e 100644 --- a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java +++ b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java @@ -5,14 +5,18 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; abstract class AbstractServiceImpl implements Service { private final String type; private final List characteristics = new LinkedList<>(); private final List linkedServices = new LinkedList<>(); + private static final Logger logger = LoggerFactory.getLogger(AbstractServiceImpl.class); /** @param type unique UUID of the service according to HAP specification. */ public AbstractServiceImpl(String type) { + logger.info("new service:" + type); this.type = type; } @@ -28,6 +32,7 @@ public String getType() { @Override public List getLinkedServices() { + logger.info(" all linked service" + this.linkedServices); return Collections.unmodifiableList(linkedServices); } @@ -37,6 +42,8 @@ public void addCharacteristic(Characteristic characteristic) { @Override public void addLinkedService(Service service) { + logger.info("add linked service" + service); this.linkedServices.add(service); + logger.info(" all linked service" + this.linkedServices); } } diff --git a/src/main/java/io/github/hapjava/services/impl/InputSourceService.java b/src/main/java/io/github/hapjava/services/impl/InputSourceService.java new file mode 100644 index 000000000..4af2736a2 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/InputSourceService.java @@ -0,0 +1,91 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.InputSourceAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithIdentifier; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithInputDeviceType; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithTargetVisibilityState; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic; +import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateCharacteristic; + +/** This service describes a input source. */ +public class InputSourceService extends AbstractServiceImpl { + + public InputSourceService( + ConfiguredNameCharacteristic configuredNameCharacteristic, + InputSourceTypeCharacteristic inputSourceTypeCharacteristic, + IsConfiguredCharacteristic isConfiguredCharacteristic, + CurrentVisibilityStateCharacteristic currentVisibilityStateCharacteristic) { + super("000000D9-0000-1000-8000-0026BB765291"); + addCharacteristic(configuredNameCharacteristic); + addCharacteristic(inputSourceTypeCharacteristic); + addCharacteristic(isConfiguredCharacteristic); + addCharacteristic(currentVisibilityStateCharacteristic); + } + + public InputSourceService(InputSourceAccessory accessory) { + this( + new ConfiguredNameCharacteristic( + accessory::getConfiguredName, + accessory::setConfiguredName, + accessory::subscribeConfiguredName, + accessory::unsubscribeConfiguredName), + new InputSourceTypeCharacteristic( + accessory::getInputSourceType, + accessory::subscribeInputSourceType, + accessory::unsubscribeInputSourceType), + new IsConfiguredCharacteristic( + accessory::isConfigured, + accessory::setIsConfigured, + accessory::subscribeIsConfigured, + accessory::unsubscribeIsConfigured), + new CurrentVisibilityStateCharacteristic( + accessory::getCurrentVisibilityState, + accessory::subscribeCurrentVisibilityState, + accessory::unsubscribeCurrentVisibilityState)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithIdentifier) { + addOptionalCharacteristic( + new IdentifierCharacteristic(((AccessoryWithIdentifier) accessory)::getIdentifier)); + } + if (accessory instanceof AccessoryWithInputDeviceType) { + addOptionalCharacteristic( + new InputDeviceTypeCharacteristic( + ((AccessoryWithInputDeviceType) accessory)::getInputDeviceType, + ((AccessoryWithInputDeviceType) accessory)::subscribeInputDeviceType, + ((AccessoryWithInputDeviceType) accessory)::unsubscribeInputDeviceType)); + } + if (accessory instanceof AccessoryWithTargetVisibilityState) { + addOptionalCharacteristic( + new TargetVisibilityStateCharacteristic( + ((AccessoryWithTargetVisibilityState) accessory)::getTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::setTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::subscribeTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::unsubscribeTargetVisibilityState)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(IdentifierCharacteristic identifier) { + addCharacteristic(identifier); + } + + public void addOptionalCharacteristic(InputDeviceTypeCharacteristic inputDeviceType) { + addCharacteristic(inputDeviceType); + } + + public void addOptionalCharacteristic(TargetVisibilityStateCharacteristic targetVisibilityState) { + addCharacteristic(targetVisibilityState); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/TelevisionService.java b/src/main/java/io/github/hapjava/services/impl/TelevisionService.java new file mode 100644 index 000000000..8846cffe1 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/TelevisionService.java @@ -0,0 +1,139 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.TelevisionAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithBrightness; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithClosedCaptions; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithCurrentMediaState; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithPictureMode; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithPowerMode; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithTargetMediaState; +import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic; +import io.github.hapjava.characteristics.impl.common.ActiveEnum; +import io.github.hapjava.characteristics.impl.common.ActiveIdentifierCharacteristic; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic; +import io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic; +import io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.PowerModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.RemoteKeyCharacteristic; +import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic; + +/** This service describes a television. */ +public class TelevisionService extends AbstractServiceImpl { + + public TelevisionService( + ActiveCharacteristic activeCharacteristic, + ActiveIdentifierCharacteristic activeIdentifierCharacteristic, + ConfiguredNameCharacteristic configuredNameCharacteristic, + RemoteKeyCharacteristic remoteKeyCharacteristic, + SleepDiscoveryModeCharacteristic sleepDiscoveryModeCharacteristic) { + super("000000D8-0000-1000-8000-0026BB765291"); + addCharacteristic(activeCharacteristic); + addCharacteristic(activeIdentifierCharacteristic); + addCharacteristic(configuredNameCharacteristic); + addCharacteristic(remoteKeyCharacteristic); + addCharacteristic(sleepDiscoveryModeCharacteristic); + } + + public TelevisionService(TelevisionAccessory accessory) { + this( + new ActiveCharacteristic( + () -> accessory.isActive().thenApply(s -> s ? ActiveEnum.ACTIVE : ActiveEnum.INACTIVE), + (v) -> accessory.setActive(v == ActiveEnum.ACTIVE), + accessory::subscribeActive, + accessory::unsubscribeActive), + new ActiveIdentifierCharacteristic( + accessory::getActiveIdentifier, + accessory::setActiveIdentifier, + accessory::subscribeActiveIdentifier, + accessory::unsubscribeActiveIdentifier), + new ConfiguredNameCharacteristic( + accessory::getConfiguredName, + accessory::setConfiguredName, + accessory::subscribeConfiguredName, + accessory::unsubscribeConfiguredName), + new RemoteKeyCharacteristic(accessory::setRemoteKey), + new SleepDiscoveryModeCharacteristic( + accessory::getSleepDiscoveryMode, + accessory::subscribeSleepDiscoveryMode, + accessory::unsubscribeSleepDiscoveryMode)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithBrightness) { + addOptionalCharacteristic( + new BrightnessCharacteristic( + ((AccessoryWithBrightness) accessory)::getBrightness, + ((AccessoryWithBrightness) accessory)::setBrightness, + ((AccessoryWithBrightness) accessory)::subscribeBrightness, + ((AccessoryWithBrightness) accessory)::unsubscribeBrightness)); + } + if (accessory instanceof AccessoryWithPowerMode) { + addOptionalCharacteristic( + new PowerModeCharacteristic(((AccessoryWithPowerMode) accessory)::setPowerMode)); + } + if (accessory instanceof AccessoryWithClosedCaptions) { + addOptionalCharacteristic( + new ClosedCaptionsCharacteristic( + ((AccessoryWithClosedCaptions) accessory)::getClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::setClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::subscribeClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::unsubscribeClosedCaptions)); + } + if (accessory instanceof AccessoryWithCurrentMediaState) { + addOptionalCharacteristic( + new CurrentMediaStateCharacteristic( + ((AccessoryWithCurrentMediaState) accessory)::getCurrentMediaState, + ((AccessoryWithCurrentMediaState) accessory)::subscribeCurrentMediaState, + ((AccessoryWithCurrentMediaState) accessory)::unsubscribeCurrentMediaState)); + } + if (accessory instanceof AccessoryWithTargetMediaState) { + addOptionalCharacteristic( + new TargetMediaStateCharacteristic( + ((AccessoryWithTargetMediaState) accessory)::getTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::setTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::subscribeTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::unsubscribeTargetMediaState)); + } + if (accessory instanceof AccessoryWithPictureMode) { + addOptionalCharacteristic( + new PictureModeCharacteristic( + ((AccessoryWithPictureMode) accessory)::getPictureMode, + ((AccessoryWithPictureMode) accessory)::setPictureMode, + ((AccessoryWithPictureMode) accessory)::subscribePictureMode, + ((AccessoryWithPictureMode) accessory)::unsubscribePictureMode)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(PowerModeCharacteristic powerMode) { + addCharacteristic(powerMode); + } + + public void addOptionalCharacteristic(ClosedCaptionsCharacteristic closedCaptions) { + addCharacteristic(closedCaptions); + } + + public void addOptionalCharacteristic(CurrentMediaStateCharacteristic currentMediaState) { + addCharacteristic(currentMediaState); + } + + public void addOptionalCharacteristic(TargetMediaStateCharacteristic targetMediaState) { + addCharacteristic(targetMediaState); + } + + public void addOptionalCharacteristic(PictureModeCharacteristic pictureMode) { + addCharacteristic(pictureMode); + } + + public void addOptionalCharacteristic(BrightnessCharacteristic brightness) { + addCharacteristic(brightness); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java b/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java new file mode 100644 index 000000000..b53fb81bd --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java @@ -0,0 +1,75 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.TelevisionSpeakerAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithActive; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolume; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolumeControlType; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolumeSelector; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; +import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic; +import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic; + +/** This service describes a television. */ +public class TelevisionSpeakerService extends AbstractServiceImpl { + + public TelevisionSpeakerService(MuteCharacteristic muteCharacteristic) { + super("00000113-0000-1000-8000-0026BB765291"); + addCharacteristic(muteCharacteristic); + } + + public TelevisionSpeakerService(TelevisionSpeakerAccessory accessory) { + this( + new MuteCharacteristic( + accessory::isMuted, + accessory::setMute, + accessory::subscribeMuteState, + accessory::unsubscribeMuteState)); + if (accessory instanceof AccessoryWithActive) { + addOptionalCharacteristic( + new ActiveCharacteristic( + ((AccessoryWithActive) accessory)::getActive, + ((AccessoryWithActive) accessory)::setActive, + ((AccessoryWithActive) accessory)::subscribeActive, + ((AccessoryWithActive) accessory)::unsubscribeActive)); + } + if (accessory instanceof AccessoryWithVolume) { + addOptionalCharacteristic( + new VolumeCharacteristic( + ((AccessoryWithVolume) accessory)::getVolume, + ((AccessoryWithVolume) accessory)::setVolume, + ((AccessoryWithVolume) accessory)::subscribeVolume, + ((AccessoryWithVolume) accessory)::unsubscribeVolume)); + } + if (accessory instanceof AccessoryWithVolumeSelector) { + addOptionalCharacteristic( + new VolumeSelectorCharacteristic( + ((AccessoryWithVolumeSelector) accessory)::setVolumeSelector)); + } + if (accessory instanceof AccessoryWithVolumeControlType) { + addOptionalCharacteristic( + new VolumeControlTypeCharacteristic( + ((AccessoryWithVolumeControlType) accessory)::getVolumeControlType, + ((AccessoryWithVolumeControlType) accessory)::subscribeVolumeControlType, + ((AccessoryWithVolumeControlType) accessory)::unsubscribeVolumeControlType)); + ; + } + } + + public void addOptionalCharacteristic(ActiveCharacteristic activeCharacteristic) { + addCharacteristic(activeCharacteristic); + } + + public void addOptionalCharacteristic(VolumeCharacteristic volume) { + addCharacteristic(volume); + } + + public void addOptionalCharacteristic(VolumeSelectorCharacteristic volumeSelector) { + addCharacteristic(volumeSelector); + } + + public void addOptionalCharacteristic(VolumeControlTypeCharacteristic volumeControlType) { + addCharacteristic(volumeControlType); + } +} From 18f6442f837881ab0bb4686f6f5533be87a95de9 Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 3 Feb 2022 22:46:26 +0100 Subject: [PATCH 02/73] update readme Signed-off-by: Eugen Freiter --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03fb0aaba..ba3cc6e7e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html) Supported HomeKit Accessories ========= -Current implementation fully supports 38 HomeKit accessory/services. +Current implementation fully supports 41 HomeKit accessory/services. | HomeKit Accessory & Service type | Supported by Java-HAP | |--------------------|--------------------| @@ -52,6 +52,7 @@ Current implementation fully supports 38 HomeKit accessory/services. | Heater Cooler | :white_check_mark: | | Humidifier Dehumidifier | :white_check_mark: | | Humidity Sensor | :white_check_mark: | +| Input Source | :white_check_mark: | | Irrigation System | :white_check_mark: | | Leak Sensor | :white_check_mark: | | Light Bulb | :white_check_mark: | @@ -72,6 +73,8 @@ Current implementation fully supports 38 HomeKit accessory/services. | Switch | :white_check_mark: | | Target Control | :x: | | Target Control Management | :x: | +| Television | :white_check_mark: | +| Television Speaker | :white_check_mark: | | Temperature Sensor | :white_check_mark: | | Thermostat | :white_check_mark: | | Valve | :white_check_mark: | From 4f1404d714fab411a4f9e9895dcb914d3a9db3bd Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Fri, 4 Feb 2022 14:02:39 +0100 Subject: [PATCH 03/73] add smart speaker Signed-off-by: Eugen Freiter --- .../accessories/SmartSpeakerAccessory.java | 62 ++++++++++++ .../accessories/TelevisionAccessory.java | 8 +- .../AccessoryWithAirPlayEnable.java | 37 ++++++++ .../AccessoryWithConfiguredName.java | 34 +++++++ .../AccessoryWithMute.java | 37 ++++++++ .../common/AirPlayEnableCharacteristic.java | 32 +++++++ .../hapjava/services/impl/DoorService.java | 14 +++ .../services/impl/SmartSpeakerService.java | 95 +++++++++++++++++++ 8 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java diff --git a/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java b/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java new file mode 100644 index 000000000..9f70af2be --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java @@ -0,0 +1,62 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.SmartSpeakerService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Smart Speaker accessory. */ +public interface SmartSpeakerAccessory extends HomekitAccessory { + + /** + * Retrieves the current media state (see {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum} for supported values). + * + * @return a future that will contain the current media state + */ + CompletableFuture getCurrentMediaState(); + + /** + * Subscribes to changes in the current media state. + * + * @param callback the function to call when the current media state changes. + */ + void subscribeCurrentMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current media state. */ + void unsubscribeCurrentMediaState(); + + /** + * Retrieves the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @return a future that will contain the target media state + */ + CompletableFuture getTargetMediaState(); + + /** + * Set the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @param targetMediaState target media state + * @return a future that completes when the change is made + */ + CompletableFuture setTargetMediaState(TargetMediaStateEnum targetMediaState); + + /** + * Subscribes to changes in the target media state. + * + * @param callback the function to call when the target media state changes. + */ + void subscribeTargetMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target media state. */ + void unsubscribeTargetMediaState(); + + @Override + default Collection getServices() { + return Collections.singleton(new SmartSpeakerService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java index 3a1a1c515..b51efe319 100644 --- a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java +++ b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java @@ -72,7 +72,7 @@ public interface TelevisionAccessory extends HomekitAccessory { CompletableFuture getConfiguredName(); /** - * Sets the mute status + * Sets the configured name * * @param name configured name * @return a future that completes when the change is made @@ -81,13 +81,13 @@ public interface TelevisionAccessory extends HomekitAccessory { CompletableFuture setConfiguredName(String name) throws Exception; /** - * Subscribes to changes in mute state. + * Subscribes to changes in configured name. * - * @param callback the function to call when the state changes. + * @param callback the function to call when the configureed name changes. */ void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); - /** Unsubscribes from changes in the mute state. */ + /** Unsubscribes from changes in the configured name state. */ void unsubscribeConfiguredName(); /** diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java new file mode 100644 index 000000000..76b90b43f --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with AirPlay enable characteristic {@link + * io.github.hapjava.characteristics.impl.common.AirPlayEnableCharacteristic}. + */ +public interface AccessoryWithAirPlayEnable { + + /** + * Retrieves the AirPlay enable state + * + * @return a future that will contain the AirPlay enable state. + */ + CompletableFuture getAirPlayEnable(); + + /** + * Sets the AirPlay enable state + * + * @param state AirPlay enable state + * @return a future that completes when the AirPlay enable is changed + * @throws Exception when the AirPlay enable cannot be set + */ + CompletableFuture setAirPlayEnable(Integer state) throws Exception; + + /** + * Subscribes to changes in the AirPlay enable state. + * + * @param callback the function to call when the AirPlay enable state changes. + */ + void subscribeAirPlayEnable(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the AirPlay enable state. */ + void unsubscribeAirPlayEnable(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java new file mode 100644 index 000000000..0c9528130 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java @@ -0,0 +1,34 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** Accessory with configured name. */ +public interface AccessoryWithConfiguredName { + + /** + * Retrieves configured name. + * + * @return configured name + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the configured name + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in configured name. + * + * @param callback the function to call when the configureed name changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the configured name state. */ + void unsubscribeConfiguredName(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java new file mode 100644 index 000000000..a0bb08579 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with mute characteristic {@link + * io.github.hapjava.characteristics.impl.audio.MuteCharacteristic}. + */ +public interface AccessoryWithMute { + + /** + * Retrieves mute status. + * + * @return true if accessory is muted + */ + CompletableFuture isMuted(); + + /** + * Sets the mute status + * + * @param mute true if accessory should be muted + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setMute(boolean mute) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeMuteState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeMuteState(); +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java new file mode 100644 index 000000000..6158f13be --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java @@ -0,0 +1,32 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.EventableCharacteristic; +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** characteristic to control air play. */ +public class AirPlayEnableCharacteristic extends IntegerCharacteristic + implements EventableCharacteristic { + + public AirPlayEnableCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "0000025B-0000-1000-8000-0026BB765291", + "AirPlay enable", + 0, + 1, + "", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/DoorService.java b/src/main/java/io/github/hapjava/services/impl/DoorService.java index c4c3b40b3..51f956f5f 100644 --- a/src/main/java/io/github/hapjava/services/impl/DoorService.java +++ b/src/main/java/io/github/hapjava/services/impl/DoorService.java @@ -2,8 +2,10 @@ import io.github.hapjava.accessories.DoorAccessory; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithHoldPosition; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithMute; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithObstructionDetection; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; import io.github.hapjava.characteristics.impl.common.NameCharacteristic; import io.github.hapjava.characteristics.impl.common.ObstructionDetectedCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.CurrentPositionCharacteristic; @@ -53,6 +55,14 @@ public DoorService(DoorAccessory accessory) { ((AccessoryWithObstructionDetection) accessory)::subscribeObstructionDetected, ((AccessoryWithObstructionDetection) accessory)::unsubscribeObstructionDetected)); } + if (accessory instanceof AccessoryWithMute) { + addOptionalCharacteristic( + new MuteCharacteristic( + ((AccessoryWithMute) accessory)::isMuted, + ((AccessoryWithMute) accessory)::setMute, + ((AccessoryWithMute) accessory)::subscribeMuteState, + ((AccessoryWithMute) accessory)::unsubscribeMuteState)); + } } public void addOptionalCharacteristic(NameCharacteristic name) { @@ -67,4 +77,8 @@ public void addOptionalCharacteristic( ObstructionDetectedCharacteristic obstructionDetectedCharacteristic) { addCharacteristic(obstructionDetectedCharacteristic); } + + public void addOptionalCharacteristic(MuteCharacteristic muteCharacteristic) { + addCharacteristic(muteCharacteristic); + } } diff --git a/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java b/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java new file mode 100644 index 000000000..e5f57eff4 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java @@ -0,0 +1,95 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.SmartSpeakerAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithAirPlayEnable; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithConfiguredName; +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.AirPlayEnableCharacteristic; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic; + +/** A smart speaker service can be used to control the audio output settings on a speaker device. */ +public class SmartSpeakerService extends AbstractServiceImpl { + + public SmartSpeakerService( + CurrentMediaStateCharacteristic currentMediaStateCharacteristic, + TargetMediaStateCharacteristic targetMediaStateCharacteristic) { + super("00000228-0000-1000-8000-0026BB765291"); + addCharacteristic(currentMediaStateCharacteristic); + addCharacteristic(targetMediaStateCharacteristic); + } + + public SmartSpeakerService(SmartSpeakerAccessory accessory) { + this( + new CurrentMediaStateCharacteristic( + accessory::getCurrentMediaState, + accessory::subscribeCurrentMediaState, + accessory::unsubscribeCurrentMediaState), + new TargetMediaStateCharacteristic( + accessory::getTargetMediaState, + accessory::setTargetMediaState, + accessory::subscribeTargetMediaState, + accessory::unsubscribeTargetMediaState)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithVolume) { + addOptionalCharacteristic( + new VolumeCharacteristic( + ((AccessoryWithVolume) accessory)::getVolume, + ((AccessoryWithVolume) accessory)::setVolume, + ((AccessoryWithVolume) accessory)::subscribeVolume, + ((AccessoryWithVolume) accessory)::unsubscribeVolume)); + } + if (accessory instanceof AccessoryWithConfiguredName) { + addOptionalCharacteristic( + new ConfiguredNameCharacteristic( + ((AccessoryWithConfiguredName) accessory)::getConfiguredName, + ((AccessoryWithConfiguredName) accessory)::setConfiguredName, + ((AccessoryWithConfiguredName) accessory)::subscribeConfiguredName, + ((AccessoryWithConfiguredName) accessory)::unsubscribeConfiguredName)); + } + if (accessory instanceof AccessoryWithMute) { + addOptionalCharacteristic( + new MuteCharacteristic( + ((AccessoryWithMute) accessory)::isMuted, + ((AccessoryWithMute) accessory)::setMute, + ((AccessoryWithMute) accessory)::subscribeMuteState, + ((AccessoryWithMute) accessory)::unsubscribeMuteState)); + } + if (accessory instanceof AccessoryWithAirPlayEnable) { + addOptionalCharacteristic( + new AirPlayEnableCharacteristic( + ((AccessoryWithAirPlayEnable) accessory)::getAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::setAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::subscribeAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::unsubscribeAirPlayEnable)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(VolumeCharacteristic volume) { + addCharacteristic(volume); + } + + public void addOptionalCharacteristic(ConfiguredNameCharacteristic configuredNameCharacteristic) { + addCharacteristic(configuredNameCharacteristic); + } + + public void addOptionalCharacteristic(MuteCharacteristic muteCharacteristic) { + addCharacteristic(muteCharacteristic); + } + + public void addOptionalCharacteristic(AirPlayEnableCharacteristic airPlayEnableCharacteristic) { + addCharacteristic(airPlayEnableCharacteristic); + } +} From 99e3d0f341ee616fdce977cd5deeb94c9f24884b Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Sat, 5 Feb 2022 10:45:33 +0100 Subject: [PATCH 04/73] upate README Signed-off-by: Eugen Freiter --- CHANGES.md | 3 +++ README.md | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb5668235..9c56f2dc2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ * Log accessory names instead of futures. [#150](https://github.com/hap-java/HAP-Java/issues/150) * Fix rotation speed data type (BREAKING API CHANGE). According to HAP specification it must be float * Fix UUID of HAP Version characteristic +* Add Mute characteristic to Doorbell service ## New * New characteristics: @@ -22,11 +23,13 @@ * Remote Key * Volume Control Type * Volume Selector + * AirPlay Enable * New services * Input Source * Television * Television Speaker + * Smart Speaker # HAP-Java 2.0.0 * major refactoring to support optional characteristics diff --git a/README.md b/README.md index ba3cc6e7e..6d1f88458 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Include HAP-Java in your project using maven: io.github.hap-java hap - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ``` @@ -28,7 +28,7 @@ read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html) Supported HomeKit Accessories ========= -Current implementation fully supports 41 HomeKit accessory/services. +Current implementation fully supports 42 HomeKit accessory/services. | HomeKit Accessory & Service type | Supported by Java-HAP | |--------------------|--------------------| @@ -67,6 +67,7 @@ Current implementation fully supports 41 HomeKit accessory/services. | Service Label | :white_check_mark: | | Siri | :x: | | Slat | :white_check_mark: | +| Smart Speaker | :white_check_mark: | | Smoke Sensor | :white_check_mark: | | Speaker | :white_check_mark: | | Stateless Programmable Switch | :white_check_mark: | From efa62a16ae8f158861f1d6150d4aecb9af73b5ae Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Sat, 5 Feb 2022 11:03:38 +0100 Subject: [PATCH 05/73] close JsonWriters Signed-off-by: Eugen Freiter --- CHANGES.md | 1 + .../server/impl/json/AccessoryController.java | 7 ++++--- .../server/impl/json/CharacteristicsController.java | 8 ++++---- .../hapjava/server/impl/json/EventController.java | 12 +++++++----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a3a55a290..42876c4c9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Fixes * Log accessory names instead of futures. [#150](https://github.com/hap-java/HAP-Java/issues/150) * Fix rotation speed data type (BREAKING API CHANGE). According to HAP specification it must be float +* Close JsonWriters [#149](https://github.com/hap-java/HAP-Java/issues/149) # HAP-Java 2.0.0 * major refactoring to support optional characteristics diff --git a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java index 98e6cd266..ef21422af 100644 --- a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java +++ b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java @@ -18,6 +18,7 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; +import javax.json.JsonWriter; public class AccessoryController { @@ -64,9 +65,9 @@ public HttpResponse listing() throws Exception { .add("services", serviceArrayBuilders.get(accessory.getId()))); } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Json.createWriter(baos) - .write(Json.createObjectBuilder().add("accessories", accessories).build()); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonWriter jsonWriter = Json.createWriter(baos)) { + jsonWriter.write(Json.createObjectBuilder().add("accessories", accessories).build()); return new HapJsonResponse(baos.toByteArray()); } } 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 df5509dda..4d20a0c6b 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 @@ -58,10 +58,10 @@ public HttpResponse get(HttpRequest request) throws Exception { "Accessory " + aid + " has no characteristics or does not exist. Request: " + uri); } } - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Json.createWriter(baos) - .write( - Json.createObjectBuilder().add("characteristics", characteristics.build()).build()); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonWriter jsonWriter = Json.createWriter(baos)) { + jsonWriter.write( + Json.createObjectBuilder().add("characteristics", characteristics.build()).build()); return new HapJsonResponse(baos.toByteArray()); } } 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 80be265ae..41473f716 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 @@ -9,6 +9,7 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObject; import javax.json.JsonObjectBuilder; +import javax.json.JsonWriter; public class EventController { @@ -24,8 +25,9 @@ public HttpResponse getMessage(int accessoryId, int iid, EventableCharacteristic JsonObject data = Json.createObjectBuilder().add("characteristics", characteristics).build(); - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Json.createWriter(baos).write(data); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonWriter jsonWriter = Json.createWriter(baos)) { + jsonWriter.write(data); byte[] dataBytes = baos.toByteArray(); return new EventResponse(dataBytes); @@ -44,9 +46,9 @@ public HttpResponse getMessage(ArrayList notifications) thr } JsonObject data = Json.createObjectBuilder().add("characteristics", characteristics).build(); - - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - Json.createWriter(baos).write(data); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonWriter jsonWriter = Json.createWriter(baos)) { + jsonWriter.write(data); byte[] dataBytes = baos.toByteArray(); return new EventResponse(dataBytes); From f241c8902117a6174b4e3ef3fe30b917b69fc80d Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 3 Feb 2022 22:42:25 +0100 Subject: [PATCH 06/73] add support for television service Signed-off-by: Eugen Freiter --- CHANGES.md | 25 ++++ .../accessories/InputSourceAccessory.java | 104 +++++++++++++ .../accessories/TelevisionAccessory.java | 123 ++++++++++++++++ .../TelevisionSpeakerAccessory.java | 43 ++++++ .../AccessoryWithActive.java | 38 +++++ .../AccessoryWithClosedCaptions.java | 37 +++++ .../AccessoryWithCurrentMediaState.java | 30 ++++ .../AccessoryWithIdentifier.java | 17 +++ .../AccessoryWithInputDeviceType.java | 29 ++++ .../AccessoryWithPictureMode.java | 39 +++++ .../AccessoryWithPowerMode.java | 16 ++ .../AccessoryWithTargetMediaState.java | 37 +++++ .../AccessoryWithTargetVisibilityState.java | 36 +++++ .../AccessoryWithVolumeControlType.java | 30 ++++ .../AccessoryWithVolumeSelector.java | 20 +++ .../impl/base/StringCharacteristic.java | 80 ++++++++++ .../ActiveIdentifierCharacteristic.java | 29 ++++ .../common/ConfiguredNameCharacteristic.java | 27 ++++ .../impl/common/IdentifierCharacteristic.java | 22 +++ .../impl/common/VersionCharacteristic.java | 2 +- .../CurrentVisibilityStateCharacteristic.java | 27 ++++ .../CurrentVisibilityStateEnum.java | 36 +++++ .../InputDeviceTypeCharacteristic.java | 26 ++++ .../impl/inputsource/InputDeviceTypeEnum.java | 38 +++++ .../InputSourceTypeCharacteristic.java | 26 ++++ .../impl/inputsource/InputSourceTypeEnum.java | 43 ++++++ .../TargetVisibilityStateCharacteristic.java | 29 ++++ .../TargetVisibilityStateEnum.java | 34 +++++ .../ClosedCaptionsCharacteristic.java | 31 ++++ .../impl/television/ClosedCaptionsEnum.java | 35 +++++ .../CurrentMediaStateCharacteristic.java | 28 ++++ .../television/CurrentMediaStateEnum.java | 37 +++++ .../television/PictureModeCharacteristic.java | 30 ++++ .../impl/television/PictureModeEnum.java | 44 ++++++ .../television/PowerModeCharacteristic.java | 20 +++ .../impl/television/PowerModeEnum.java | 35 +++++ .../television/RemoteKeyCharacteristic.java | 22 +++ .../impl/television/RemoteKeyEnum.java | 45 ++++++ .../SleepDiscoveryModeCharacteristic.java | 28 ++++ .../television/SleepDiscoveryModeEnum.java | 35 +++++ .../TargetMediaStateCharacteristic.java | 27 ++++ .../impl/television/TargetMediaStateEnum.java | 36 +++++ .../VolumeControlTypeCharacteristic.java | 28 ++++ .../VolumeControlTypeEnum.java | 36 +++++ .../VolumeSelectorCharacteristic.java | 22 +++ .../televisionspeaker/VolumeSelectorEnum.java | 34 +++++ .../services/impl/AbstractServiceImpl.java | 7 + .../services/impl/InputSourceService.java | 91 ++++++++++++ .../services/impl/TelevisionService.java | 139 ++++++++++++++++++ .../impl/TelevisionSpeakerService.java | 75 ++++++++++ 50 files changed, 1927 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java create mode 100644 src/main/java/io/github/hapjava/services/impl/InputSourceService.java create mode 100644 src/main/java/io/github/hapjava/services/impl/TelevisionService.java create mode 100644 src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java diff --git a/CHANGES.md b/CHANGES.md index 42876c4c9..0f293c195 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,31 @@ * Log accessory names instead of futures. [#150](https://github.com/hap-java/HAP-Java/issues/150) * Fix rotation speed data type (BREAKING API CHANGE). According to HAP specification it must be float * Close JsonWriters [#149](https://github.com/hap-java/HAP-Java/issues/149) +* Fix UUID of HAP Version characteristic + +## New +* New characteristics: + * Identifier + * Input Device Type + * Input Source Type + * Configured Name + * Current Visibility State + * Target Visibility State + * Sleep Discovery Mode + * Active Identifier + * Closed Captions + * Current Media State + * Target Media State + * Picture Mode + * Power Mode + * Remote Key + * Volume Control Type + * Volume Selector + +* New services + * Input Source + * Television + * Television Speaker # HAP-Java 2.0.0 * major refactoring to support optional characteristics diff --git a/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java b/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java new file mode 100644 index 000000000..cfa5ae3d6 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/InputSourceAccessory.java @@ -0,0 +1,104 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.common.IsConfiguredEnum; +import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateEnum; +import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.InputSourceService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Input Source accessory. */ +public interface InputSourceAccessory extends HomekitAccessory { + + /** + * Retrieves configured name of input source. + * + * @return configured name of input source + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the configured name. + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in configured name. + * + * @param callback the function to call when the configured name changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the configured name. */ + void unsubscribeConfiguredName(); + + /** + * Retrieves the flag whether input source is configured. + * + * @return a future that will contain the flag . + */ + CompletableFuture isConfigured(); + /** + * set the flag whether input source is configured. + * + * @param state is configured state + * @return a future that completes when the change is made + */ + CompletableFuture setIsConfigured(IsConfiguredEnum state); + + /** + * Subscribes to changes in isConfigured. + * + * @param callback the function to call when the state changes. + */ + void subscribeIsConfigured(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in isConfigured. */ + void unsubscribeIsConfigured(); + + /** + * Retrieves the input source type. + * + * @return a future that will contain the input source type. + */ + CompletableFuture getInputSourceType(); + + /** + * Subscribes to changes in input source type. + * + * @param callback the function to call when the type changes. + */ + void subscribeInputSourceType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the input source type. */ + void unsubscribeInputSourceType(); + + /** + * Retrieves the current visibility state. + * + * @return a future that will contain the current visibility state. + */ + CompletableFuture getCurrentVisibilityState(); + + /** + * Subscribes to changes in current visibility state. + * + * @param callback the function to call when the state changes. + */ + void subscribeCurrentVisibilityState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current visibility state. */ + void unsubscribeCurrentVisibilityState(); + + @Override + default Collection getServices() { + return Collections.singleton(new InputSourceService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java new file mode 100644 index 000000000..3a1a1c515 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java @@ -0,0 +1,123 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.RemoteKeyEnum; +import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.TelevisionService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Television accessory. */ +public interface TelevisionAccessory extends HomekitAccessory { + + /** + * Retrieves the current active state of the TV. + * + * @return a future that will contain the state + */ + CompletableFuture isActive(); + + /** + * Sets the active state of the TV + * + * @param state the state to set + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setActive(boolean state) throws Exception; + + /** + * Subscribes to changes in the active state of the TV . + * + * @param callback the function to call when the active state changes. + */ + void subscribeActive(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active state of the TV. */ + void unsubscribeActive(); + + /** + * Retrieves the active identifier + * + * @return a future that will contain the active identifier. + */ + CompletableFuture getActiveIdentifier(); + + /** + * Sets the active identifier + * + * @param identifier the active identifier + * @return a future that completes when the active identifier is changed + * @throws Exception when the active identifier cannot be set + */ + CompletableFuture setActiveIdentifier(Integer identifier) throws Exception; + + /** + * Subscribes to changes in the active identifier. + * + * @param callback the function to call when the active identifier changes. + */ + void subscribeActiveIdentifier(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active identifier. */ + void unsubscribeActiveIdentifier(); + + /** + * Retrieves configured name. + * + * @return configured name + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the mute status + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeConfiguredName(); + + /** + * Sends the remote key. + * + * @param key remote key + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setRemoteKey(RemoteKeyEnum key) throws Exception; + + /** + * Retrieves the sleep discovery mode. + * + * @return a future that will contain the sleep discovery mode . + */ + CompletableFuture getSleepDiscoveryMode(); + + /** + * Subscribes to changes in sleep discovery mode. + * + * @param callback the function to call when the sleep discovery mode changes. + */ + void subscribeSleepDiscoveryMode(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the sleep discovery mode. */ + void unsubscribeSleepDiscoveryMode(); + + @Override + default Collection getServices() { + return Collections.singleton(new TelevisionService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java new file mode 100644 index 000000000..28df1437d --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/TelevisionSpeakerAccessory.java @@ -0,0 +1,43 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.TelevisionSpeakerService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Television speaker accessory. */ +public interface TelevisionSpeakerAccessory extends HomekitAccessory { + + /** + * Retrieves mute status. + * + * @return true if accessory is muted + */ + CompletableFuture isMuted(); + + /** + * Sets the mute status + * + * @param mute true if accessory should be muted + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setMute(boolean mute) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeMuteState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeMuteState(); + + @Override + default Collection getServices() { + return Collections.singleton(new TelevisionSpeakerService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java new file mode 100644 index 000000000..4ad11be63 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithActive.java @@ -0,0 +1,38 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.common.ActiveEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with active characteristic {@link + * io.github.hapjava.characteristics.impl.common.ActiveCharacteristic}. + */ +public interface AccessoryWithActive { + + /** + * Retrieves the active state (see {@link + * io.github.hapjava.characteristics.impl.common.ActiveEnum} for supported values). + * + * @return a future that will contain the active state + */ + CompletableFuture getActive(); + + /** + * Set the active state (see {@link ActiveEnum} for supported values). + * + * @param active active state + * @return a future that completes when the change is made + */ + CompletableFuture setActive(ActiveEnum active); + + /** + * Subscribes to changes in the active state. + * + * @param callback the function to call when the active state changes. + */ + void subscribeActive(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the active state. */ + void unsubscribeActive(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java new file mode 100644 index 000000000..c13da3289 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithClosedCaptions.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.ClosedCaptionsEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with closed captions characteristic {@link + * io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic}. + */ +public interface AccessoryWithClosedCaptions { + + /** + * Retrieves the closed captions state (see {@link ClosedCaptionsEnum} for supported values). + * + * @return a future that will contain the closed captions + */ + CompletableFuture getClosedCaptions(); + + /** + * Set the closed captions state (see {@link ClosedCaptionsEnum} for supported values). + * + * @param closedCaptions closed captions + * @return a future that completes when the change is made + */ + CompletableFuture setClosedCaptions(ClosedCaptionsEnum closedCaptions); + + /** + * Subscribes to changes in the closed captions. + * + * @param callback the function to call when the closed captions changes. + */ + void subscribeClosedCaptions(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the closed captions. */ + void unsubscribeClosedCaptions(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java new file mode 100644 index 000000000..22350d9b4 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithCurrentMediaState.java @@ -0,0 +1,30 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with current media state characteristic {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic}. + */ +public interface AccessoryWithCurrentMediaState { + + /** + * Retrieves the current media state (see {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum} for supported values). + * + * @return a future that will contain the current media state + */ + CompletableFuture getCurrentMediaState(); + + /** + * Subscribes to changes in the current media state. + * + * @param callback the function to call when the current media state changes. + */ + void subscribeCurrentMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current media state. */ + void unsubscribeCurrentMediaState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java new file mode 100644 index 000000000..b181d4d99 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithIdentifier.java @@ -0,0 +1,17 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with identifier characteristic {@link + * io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic}. + */ +public interface AccessoryWithIdentifier { + + /** + * Retrieves the identifier of service. + * + * @return a future with the identifier + */ + CompletableFuture getIdentifier(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java new file mode 100644 index 000000000..fca5f5df1 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithInputDeviceType.java @@ -0,0 +1,29 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with input devices, e.g. television. {@link + * io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic} + */ +public interface AccessoryWithInputDeviceType { + + /** + * Retrieves the input device type. See {@link InputDeviceTypeEnum} for supported values. + * + * @return a future with the value + */ + CompletableFuture getInputDeviceType(); + + /** + * Subscribes to changes in input device type. + * + * @param callback the function when the input device type changes + */ + void subscribeInputDeviceType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes */ + void unsubscribeInputDeviceType(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java new file mode 100644 index 000000000..b369b98bf --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPictureMode.java @@ -0,0 +1,39 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.PictureModeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with picture mode characteristic {@link + * io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic}. + */ +public interface AccessoryWithPictureMode { + + /** + * Retrieves the picture mode (see {@link + * io.github.hapjava.characteristics.impl.television.PictureModeEnum} for supported values). + * + * @return a future that will contain the picture mode + */ + CompletableFuture getPictureMode(); + + /** + * Set the picture mode (see {@link + * io.github.hapjava.characteristics.impl.television.PictureModeEnum} for supported values). + * + * @param pictureMode picture mode + * @return a future that completes when the change is made + */ + CompletableFuture setPictureMode(PictureModeEnum pictureMode); + + /** + * Subscribes to changes in the picture mode. + * + * @param callback the function to call when the picture mode changes. + */ + void subscribePictureMode(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the picture mode. */ + void unsubscribePictureMode(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java new file mode 100644 index 000000000..5abb7e6f1 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithPowerMode.java @@ -0,0 +1,16 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.impl.television.PowerModeEnum; +import java.util.concurrent.CompletableFuture; + +/** An accessory with power mode. */ +public interface AccessoryWithPowerMode { + + /** + * Set the power mode. See {@link PowerModeEnum} for supported values + * + * @param mode target power mode + * @return a future that completes when the change is made + */ + CompletableFuture setPowerMode(PowerModeEnum mode); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java new file mode 100644 index 000000000..e1f1a34bd --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetMediaState.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with target media state characteristic {@link + * io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic}. + */ +public interface AccessoryWithTargetMediaState { + + /** + * Retrieves the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @return a future that will contain the target media state + */ + CompletableFuture getTargetMediaState(); + + /** + * Set the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @param targetMediaState target media state + * @return a future that completes when the change is made + */ + CompletableFuture setTargetMediaState(TargetMediaStateEnum targetMediaState); + + /** + * Subscribes to changes in the target media state. + * + * @param callback the function to call when the target media state changes. + */ + void subscribeTargetMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target media state. */ + void unsubscribeTargetMediaState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java new file mode 100644 index 000000000..dcadd1cc5 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithTargetVisibilityState.java @@ -0,0 +1,36 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateEnum; +import java.util.concurrent.CompletableFuture; + +/** accessory with target visibility state characteristics. */ +public interface AccessoryWithTargetVisibilityState { + + /** + * Retrieves the target visibility state. + * + * @return a future that will contain the target visibility state + */ + CompletableFuture getTargetVisibilityState(); + + /** + * Sets the target visibility state + * + * @param state the target visibility state to set + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setTargetVisibilityState(TargetVisibilityStateEnum state) + throws Exception; + + /** + * Subscribes to changes in t target visibility state. + * + * @param callback the function to call when the target visibility state changes. + */ + void subscribeTargetVisibilityState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target visibility state. */ + void unsubscribeTargetVisibilityState(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java new file mode 100644 index 000000000..21a6f6c66 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeControlType.java @@ -0,0 +1,30 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with volume control type {@link + * io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic} + */ +public interface AccessoryWithVolumeControlType { + + /** + * Retrieves the current volume control type. see {@link VolumeControlTypeEnum} for possible + * values + * + * @return a future that will contain the type. + */ + CompletableFuture getVolumeControlType(); + + /** + * Subscribes to changes in the volume. + * + * @param callback the function to call when the state changes. + */ + void subscribeVolumeControlType(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the volume. */ + void unsubscribeVolumeControlType(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java new file mode 100644 index 000000000..0c2a2890f --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithVolumeSelector.java @@ -0,0 +1,20 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorEnum; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with volume selector {@link + * io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic} + */ +public interface AccessoryWithVolumeSelector { + + /** + * Sets the volume selector + * + * @param value the volume selector + * @return a future that completes when the volume selector is changed + * @throws Exception when the volume selector cannot be set + */ + CompletableFuture setVolumeSelector(VolumeSelectorEnum value) throws Exception; +} 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 new file mode 100644 index 000000000..4373b06c9 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/base/StringCharacteristic.java @@ -0,0 +1,80 @@ +package io.github.hapjava.characteristics.impl.base; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.json.JsonObjectBuilder; +import javax.json.JsonString; +import javax.json.JsonValue; + +/** + * A characteristic that provides an string value. + * + * @author Eugen Freiter + */ +public class StringCharacteristic extends BaseCharacteristic { + private final Optional>> getter; + private final Optional> setter; + /** + * Default constructor + * + * @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 description a description of the characteristic to be passed to the consuming device. + * @param getter getter to retrieve the value + * @param setter setter for value + * @param subscriber subscriber to subscribe to changes + * @param unsubscriber unsubscriber to unsubscribe from chnages + */ + public StringCharacteristic( + String type, + String description, + Optional>> getter, + Optional> setter, + Optional> subscriber, + Optional unsubscriber) { + super( + type, + "string", + description, + getter.isPresent(), + setter.isPresent(), + subscriber, + unsubscriber); + this.getter = getter; + this.setter = setter; + } + + /** {@inheritDoc} */ + @Override + protected CompletableFuture makeBuilder(int iid) { + return super.makeBuilder(iid); + } + + /** {@inheritDoc} */ + @Override + public String convert(JsonValue jsonValue) { + return ((JsonString) jsonValue).getString(); + } + + /** {@inheritDoc} */ + @Override + public void setValue(String value) throws Exception { + setter.get().accept(value); + } + + /** {@inheritDoc} */ + @Override + protected CompletableFuture getValue() { + return getter.map(stringGetter -> stringGetter.get()).get(); + } + + /** {@inheritDoc} */ + @Override + protected String getDefault() { + return "Unknown"; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java new file mode 100644 index 000000000..587c1b666 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ActiveIdentifierCharacteristic.java @@ -0,0 +1,29 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class ActiveIdentifierCharacteristic extends IntegerCharacteristic { + + public ActiveIdentifierCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E7-0000-1000-8000-0026BB765291", + "active identifier", + 0, + 1000, + "", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java new file mode 100644 index 000000000..471d5a368 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/ConfiguredNameCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.StringCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes a configured name. */ +public class ConfiguredNameCharacteristic extends StringCharacteristic { + + public ConfiguredNameCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E3-0000-1000-8000-0026BB765291", + "configured name", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java new file mode 100644 index 000000000..4b8f7613c --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/IdentifierCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class IdentifierCharacteristic extends IntegerCharacteristic { + + public IdentifierCharacteristic(Supplier> getter) { + super( + "000000E6-0000-1000-8000-0026BB765291", + "identifier", + 0, + 1000, + "", + Optional.of(getter), + Optional.empty(), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java index 038c46c5c..75c8aaf9e 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/VersionCharacteristic.java @@ -10,7 +10,7 @@ public class VersionCharacteristic extends StaticStringCharacteristic { public VersionCharacteristic(Supplier> getter) { super( - "00000023-0000-1000-8000-0026BB765291", + "00000037-0000-1000-8000-0026BB765291", "HAP version", Optional.of(getter), Optional.empty(), diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java new file mode 100644 index 000000000..65c358a3f --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes current visibility state. */ +public class CurrentVisibilityStateCharacteristic + extends EnumCharacteristic { + + public CurrentVisibilityStateCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000135-0000-1000-8000-0026BB765291", + "current visibility state", + CurrentVisibilityStateEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java new file mode 100644 index 000000000..2159393d8 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/CurrentVisibilityStateEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum CurrentVisibilityStateEnum implements CharacteristicEnum { + SHOWN(0), + HIDDEN(1), + UNKNOWN_1(2), + UNKNOWN_2(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(CurrentVisibilityStateEnum.values()) + .collect(Collectors.toMap(CurrentVisibilityStateEnum::getCode, t -> t)); + } + + public static CurrentVisibilityStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + CurrentVisibilityStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java new file mode 100644 index 000000000..51a6fbde5 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeCharacteristic.java @@ -0,0 +1,26 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes input device type. */ +public class InputDeviceTypeCharacteristic extends EnumCharacteristic { + + public InputDeviceTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DC-0000-1000-8000-0026BB765291", + "input device type", + InputDeviceTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java new file mode 100644 index 000000000..5c7337174 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputDeviceTypeEnum.java @@ -0,0 +1,38 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum InputDeviceTypeEnum implements CharacteristicEnum { + OTHER(0), + TV(1), + RECORDING(2), + TUNER(3), + PLAYBACK(4), + AUDIO_SYSTEM(5); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(InputDeviceTypeEnum.values()) + .collect(Collectors.toMap(InputDeviceTypeEnum::getCode, t -> t)); + } + + public static InputDeviceTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + InputDeviceTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java new file mode 100644 index 000000000..91134891b --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeCharacteristic.java @@ -0,0 +1,26 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes input source type. */ +public class InputSourceTypeCharacteristic extends EnumCharacteristic { + + public InputSourceTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DB-0000-1000-8000-0026BB765291", + "input source type", + InputSourceTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java new file mode 100644 index 000000000..94545ec18 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/InputSourceTypeEnum.java @@ -0,0 +1,43 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum InputSourceTypeEnum implements CharacteristicEnum { + OTHER(0), + HOME_SCREEN(1), + TUNER(2), + HDMI(3), + COMPOSITE_VIDEO(4), + S_VIDEO(5), + COMPONENT_VIDEO(6), + DVI(7), + AIRPLAY(8), + USB(9), + APPLICATION(10); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(InputSourceTypeEnum.values()) + .collect(Collectors.toMap(InputSourceTypeEnum::getCode, t -> t)); + } + + public static InputSourceTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + InputSourceTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java new file mode 100644 index 000000000..ea466d182 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateCharacteristic.java @@ -0,0 +1,29 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic describes target visibility state. */ +public class TargetVisibilityStateCharacteristic + extends EnumCharacteristic { + + public TargetVisibilityStateCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000134-0000-1000-8000-0026BB765291", + "target visibility state", + TargetVisibilityStateEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java new file mode 100644 index 000000000..6a75b0c30 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/inputsource/TargetVisibilityStateEnum.java @@ -0,0 +1,34 @@ +package io.github.hapjava.characteristics.impl.inputsource; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum TargetVisibilityStateEnum implements CharacteristicEnum { + SHOWN(0), + HIDDEN(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(TargetVisibilityStateEnum.values()) + .collect(Collectors.toMap(TargetVisibilityStateEnum::getCode, t -> t)); + } + + public static TargetVisibilityStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + TargetVisibilityStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java new file mode 100644 index 000000000..a963ca222 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsCharacteristic.java @@ -0,0 +1,31 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Characteristic indicates whether the service provides closed captions. See {@link + * ClosedCaptionsEnum} for possible values. + */ +public class ClosedCaptionsCharacteristic extends EnumCharacteristic { + + public ClosedCaptionsCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000DD-0000-1000-8000-0026BB765291", + "closed captions", + ClosedCaptionsEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java new file mode 100644 index 000000000..2eb994149 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/ClosedCaptionsEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Disabled” 1 ”Enabled” 2-255 ”Reserved” */ +public enum ClosedCaptionsEnum implements CharacteristicEnum { + DISABLED(0), + ENABLED(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(ClosedCaptionsEnum.values()) + .collect(Collectors.toMap(ClosedCaptionsEnum::getCode, t -> t)); + } + + public static ClosedCaptionsEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + ClosedCaptionsEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java new file mode 100644 index 000000000..ca72c4ee9 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates current media state. See {@link CurrentMediaStateEnum} for possible + * values. + */ +public class CurrentMediaStateCharacteristic extends EnumCharacteristic { + public CurrentMediaStateCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E0-0000-1000-8000-0026BB765291", + "current media state", + CurrentMediaStateEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java new file mode 100644 index 000000000..83836c4c6 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/CurrentMediaStateEnum.java @@ -0,0 +1,37 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Play” 1 ”Pause” 2 "Stop" 3 "Unknown" 4-255 ”Reserved” */ +public enum CurrentMediaStateEnum implements CharacteristicEnum { + PLAY(0), + PAUSE(1), + STOP(2), + UNKNOWN(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(CurrentMediaStateEnum.values()) + .collect(Collectors.toMap(CurrentMediaStateEnum::getCode, t -> t)); + } + + public static CurrentMediaStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + CurrentMediaStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java new file mode 100644 index 000000000..7efefa23a --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeCharacteristic.java @@ -0,0 +1,30 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic retrieves / sets picture mode. See {@link PictureModeEnum} for possible + * values. + */ +public class PictureModeCharacteristic extends EnumCharacteristic { + public PictureModeCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E2-0000-1000-8000-0026BB765291", + "picture mode", + PictureModeEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java new file mode 100644 index 000000000..ac387cf65 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PictureModeEnum.java @@ -0,0 +1,44 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 0 ”Other” 1 ”Standard” 2 "Calibrated" 3 "CalibratedDark" 4 "Vivid" 5 "Game" 6 "Computer" 7 + * "Custom" 8-255 ”Reserved” + */ +public enum PictureModeEnum implements CharacteristicEnum { + OTHER(0), + STANDARD(1), + CALIBRATED(2), + CALIBRATED_DARK(3), + VIVID(4), + GAME(5), + COMPUTER(6), + CUSTOM(7); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(PictureModeEnum.values()) + .collect(Collectors.toMap(PictureModeEnum::getCode, t -> t)); + } + + public static PictureModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + PictureModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java new file mode 100644 index 000000000..cebbe4124 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeCharacteristic.java @@ -0,0 +1,20 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** characteristic sets power mode. See {@link PowerModeEnum} for possible values. */ +public class PowerModeCharacteristic extends EnumCharacteristic { + + public PowerModeCharacteristic(ExceptionalConsumer setter) { + super( + "000000DF-0000-1000-8000-0026BB765291", + "power mode selection", + PowerModeEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java new file mode 100644 index 000000000..a171ba135 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/PowerModeEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Show” 1 ”Hide” 2-255 ”Reserved” */ +public enum PowerModeEnum implements CharacteristicEnum { + SHOW(0), + HIDE(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(PowerModeEnum.values()) + .collect(Collectors.toMap(PowerModeEnum::getCode, t -> t)); + } + + public static PowerModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + PowerModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java new file mode 100644 index 000000000..fe028d10d --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** + * This characteristic sends information about pressed key on tv remote. See {@link RemoteKeyEnum} + * for possible values. + */ +public class RemoteKeyCharacteristic extends EnumCharacteristic { + public RemoteKeyCharacteristic(ExceptionalConsumer setter) { + super( + "000000E1-0000-1000-8000-0026BB765291", + "remote key", + RemoteKeyEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} 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 new file mode 100644 index 000000000..443c23644 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/RemoteKeyEnum.java @@ -0,0 +1,45 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum RemoteKeyEnum implements CharacteristicEnum { + REWIND(0), + FAST_FORWARD(1), + NEXT_TRACK(2), + PREV_TRACK(3), + ARROW_UP(4), + ARROW_DOWN(5), + ARROW_LEFT(6), + ARROW_RIGHT(7), + SELECT(8), + BACK(9), + EXIT(10), + PLAY_PAUSE(11), + INFO(115); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(RemoteKeyEnum.values()) + .collect(Collectors.toMap(RemoteKeyEnum::getCode, t -> t)); + } + + public static RemoteKeyEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + RemoteKeyEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java new file mode 100644 index 000000000..2a7f41829 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates whether the service can be discovered. See {@link + * SleepDiscoveryModeEnum} for possible values. + */ +public class SleepDiscoveryModeCharacteristic extends EnumCharacteristic { + public SleepDiscoveryModeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E8-0000-1000-8000-0026BB765291", + "sleep discovery mode", + SleepDiscoveryModeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java new file mode 100644 index 000000000..da1377b9a --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/SleepDiscoveryModeEnum.java @@ -0,0 +1,35 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Not discoverable” 1 ”Always discoverable” 2-255 ”Reserved” */ +public enum SleepDiscoveryModeEnum implements CharacteristicEnum { + NOT_DISCOVERABLE(0), + ALWAYS_DISCOVERABLE(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(SleepDiscoveryModeEnum.values()) + .collect(Collectors.toMap(SleepDiscoveryModeEnum::getCode, t -> t)); + } + + public static SleepDiscoveryModeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + SleepDiscoveryModeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java new file mode 100644 index 000000000..8909bb1ea --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateCharacteristic.java @@ -0,0 +1,27 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** This characteristic sets media state. See {@link TargetMediaStateEnum} for possible values. */ +public class TargetMediaStateCharacteristic extends EnumCharacteristic { + public TargetMediaStateCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "00000137-0000-1000-8000-0026BB765291", + "current media state", + TargetMediaStateEnum.values(), + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java new file mode 100644 index 000000000..b6f6633ff --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/television/TargetMediaStateEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.television; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** 0 ”Play” 1 ”Pause” 2 "Stop" 3-255 ”Reserved” */ +public enum TargetMediaStateEnum implements CharacteristicEnum { + PLAY(0), + PAUSE(1), + STOP(2); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(TargetMediaStateEnum.values()) + .collect(Collectors.toMap(TargetMediaStateEnum::getCode, t -> t)); + } + + public static TargetMediaStateEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + TargetMediaStateEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java new file mode 100644 index 000000000..c6d18a858 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeCharacteristic.java @@ -0,0 +1,28 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This characteristic indicates volume control type. See {@link VolumeControlTypeEnum} for possible + * values. + */ +public class VolumeControlTypeCharacteristic extends EnumCharacteristic { + public VolumeControlTypeCharacteristic( + Supplier> getter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "000000E9-0000-1000-8000-0026BB765291", + "volume control type", + VolumeControlTypeEnum.values(), + Optional.of(getter), + Optional.empty(), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java new file mode 100644 index 000000000..dfeb24636 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeControlTypeEnum.java @@ -0,0 +1,36 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum VolumeControlTypeEnum implements CharacteristicEnum { + NONE(0), + RELATIVE(1), + RELATIVE_WITH_CURRENT(2), + ABSOLUTE(3); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(VolumeControlTypeEnum.values()) + .collect(Collectors.toMap(VolumeControlTypeEnum::getCode, t -> t)); + } + + public static VolumeControlTypeEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + VolumeControlTypeEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java new file mode 100644 index 000000000..8f2f84e7b --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorCharacteristic.java @@ -0,0 +1,22 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.impl.base.EnumCharacteristic; +import java.util.Optional; + +/** + * This characteristic sends information about volume changes. See {@link VolumeSelectorEnum} for + * possible values. + */ +public class VolumeSelectorCharacteristic extends EnumCharacteristic { + public VolumeSelectorCharacteristic(ExceptionalConsumer setter) { + super( + "000000EA-0000-1000-8000-0026BB765291", + "volume selector", + VolumeSelectorEnum.values(), + Optional.empty(), + Optional.of(setter), + Optional.empty(), + Optional.empty()); + } +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java new file mode 100644 index 000000000..63bc60e50 --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/televisionspeaker/VolumeSelectorEnum.java @@ -0,0 +1,34 @@ +package io.github.hapjava.characteristics.impl.televisionspeaker; + +import io.github.hapjava.characteristics.CharacteristicEnum; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public enum VolumeSelectorEnum implements CharacteristicEnum { + INCREMENT(0), + DECREMENT(1); + + private static final Map reverse; + + static { + reverse = + Arrays.stream(VolumeSelectorEnum.values()) + .collect(Collectors.toMap(VolumeSelectorEnum::getCode, t -> t)); + } + + public static VolumeSelectorEnum fromCode(Integer code) { + return reverse.get(code); + } + + private final int code; + + VolumeSelectorEnum(int code) { + this.code = code; + } + + @Override + public int getCode() { + return code; + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java index 25a44099d..2848d367e 100644 --- a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java +++ b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java @@ -5,14 +5,18 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; abstract class AbstractServiceImpl implements Service { private final String type; private final List characteristics = new LinkedList<>(); private final List linkedServices = new LinkedList<>(); + private static final Logger logger = LoggerFactory.getLogger(AbstractServiceImpl.class); /** @param type unique UUID of the service according to HAP specification. */ public AbstractServiceImpl(String type) { + logger.info("new service:" + type); this.type = type; } @@ -28,6 +32,7 @@ public String getType() { @Override public List getLinkedServices() { + logger.info(" all linked service" + this.linkedServices); return Collections.unmodifiableList(linkedServices); } @@ -37,6 +42,8 @@ public void addCharacteristic(Characteristic characteristic) { @Override public void addLinkedService(Service service) { + logger.info("add linked service" + service); this.linkedServices.add(service); + logger.info(" all linked service" + this.linkedServices); } } diff --git a/src/main/java/io/github/hapjava/services/impl/InputSourceService.java b/src/main/java/io/github/hapjava/services/impl/InputSourceService.java new file mode 100644 index 000000000..4af2736a2 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/InputSourceService.java @@ -0,0 +1,91 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.InputSourceAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithIdentifier; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithInputDeviceType; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithTargetVisibilityState; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.IdentifierCharacteristic; +import io.github.hapjava.characteristics.impl.common.IsConfiguredCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.CurrentVisibilityStateCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.InputDeviceTypeCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.InputSourceTypeCharacteristic; +import io.github.hapjava.characteristics.impl.inputsource.TargetVisibilityStateCharacteristic; + +/** This service describes a input source. */ +public class InputSourceService extends AbstractServiceImpl { + + public InputSourceService( + ConfiguredNameCharacteristic configuredNameCharacteristic, + InputSourceTypeCharacteristic inputSourceTypeCharacteristic, + IsConfiguredCharacteristic isConfiguredCharacteristic, + CurrentVisibilityStateCharacteristic currentVisibilityStateCharacteristic) { + super("000000D9-0000-1000-8000-0026BB765291"); + addCharacteristic(configuredNameCharacteristic); + addCharacteristic(inputSourceTypeCharacteristic); + addCharacteristic(isConfiguredCharacteristic); + addCharacteristic(currentVisibilityStateCharacteristic); + } + + public InputSourceService(InputSourceAccessory accessory) { + this( + new ConfiguredNameCharacteristic( + accessory::getConfiguredName, + accessory::setConfiguredName, + accessory::subscribeConfiguredName, + accessory::unsubscribeConfiguredName), + new InputSourceTypeCharacteristic( + accessory::getInputSourceType, + accessory::subscribeInputSourceType, + accessory::unsubscribeInputSourceType), + new IsConfiguredCharacteristic( + accessory::isConfigured, + accessory::setIsConfigured, + accessory::subscribeIsConfigured, + accessory::unsubscribeIsConfigured), + new CurrentVisibilityStateCharacteristic( + accessory::getCurrentVisibilityState, + accessory::subscribeCurrentVisibilityState, + accessory::unsubscribeCurrentVisibilityState)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithIdentifier) { + addOptionalCharacteristic( + new IdentifierCharacteristic(((AccessoryWithIdentifier) accessory)::getIdentifier)); + } + if (accessory instanceof AccessoryWithInputDeviceType) { + addOptionalCharacteristic( + new InputDeviceTypeCharacteristic( + ((AccessoryWithInputDeviceType) accessory)::getInputDeviceType, + ((AccessoryWithInputDeviceType) accessory)::subscribeInputDeviceType, + ((AccessoryWithInputDeviceType) accessory)::unsubscribeInputDeviceType)); + } + if (accessory instanceof AccessoryWithTargetVisibilityState) { + addOptionalCharacteristic( + new TargetVisibilityStateCharacteristic( + ((AccessoryWithTargetVisibilityState) accessory)::getTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::setTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::subscribeTargetVisibilityState, + ((AccessoryWithTargetVisibilityState) accessory)::unsubscribeTargetVisibilityState)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(IdentifierCharacteristic identifier) { + addCharacteristic(identifier); + } + + public void addOptionalCharacteristic(InputDeviceTypeCharacteristic inputDeviceType) { + addCharacteristic(inputDeviceType); + } + + public void addOptionalCharacteristic(TargetVisibilityStateCharacteristic targetVisibilityState) { + addCharacteristic(targetVisibilityState); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/TelevisionService.java b/src/main/java/io/github/hapjava/services/impl/TelevisionService.java new file mode 100644 index 000000000..8846cffe1 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/TelevisionService.java @@ -0,0 +1,139 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.TelevisionAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithBrightness; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithClosedCaptions; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithCurrentMediaState; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithPictureMode; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithPowerMode; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithTargetMediaState; +import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic; +import io.github.hapjava.characteristics.impl.common.ActiveEnum; +import io.github.hapjava.characteristics.impl.common.ActiveIdentifierCharacteristic; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.lightbulb.BrightnessCharacteristic; +import io.github.hapjava.characteristics.impl.television.ClosedCaptionsCharacteristic; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic; +import io.github.hapjava.characteristics.impl.television.PictureModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.PowerModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.RemoteKeyCharacteristic; +import io.github.hapjava.characteristics.impl.television.SleepDiscoveryModeCharacteristic; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic; + +/** This service describes a television. */ +public class TelevisionService extends AbstractServiceImpl { + + public TelevisionService( + ActiveCharacteristic activeCharacteristic, + ActiveIdentifierCharacteristic activeIdentifierCharacteristic, + ConfiguredNameCharacteristic configuredNameCharacteristic, + RemoteKeyCharacteristic remoteKeyCharacteristic, + SleepDiscoveryModeCharacteristic sleepDiscoveryModeCharacteristic) { + super("000000D8-0000-1000-8000-0026BB765291"); + addCharacteristic(activeCharacteristic); + addCharacteristic(activeIdentifierCharacteristic); + addCharacteristic(configuredNameCharacteristic); + addCharacteristic(remoteKeyCharacteristic); + addCharacteristic(sleepDiscoveryModeCharacteristic); + } + + public TelevisionService(TelevisionAccessory accessory) { + this( + new ActiveCharacteristic( + () -> accessory.isActive().thenApply(s -> s ? ActiveEnum.ACTIVE : ActiveEnum.INACTIVE), + (v) -> accessory.setActive(v == ActiveEnum.ACTIVE), + accessory::subscribeActive, + accessory::unsubscribeActive), + new ActiveIdentifierCharacteristic( + accessory::getActiveIdentifier, + accessory::setActiveIdentifier, + accessory::subscribeActiveIdentifier, + accessory::unsubscribeActiveIdentifier), + new ConfiguredNameCharacteristic( + accessory::getConfiguredName, + accessory::setConfiguredName, + accessory::subscribeConfiguredName, + accessory::unsubscribeConfiguredName), + new RemoteKeyCharacteristic(accessory::setRemoteKey), + new SleepDiscoveryModeCharacteristic( + accessory::getSleepDiscoveryMode, + accessory::subscribeSleepDiscoveryMode, + accessory::unsubscribeSleepDiscoveryMode)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithBrightness) { + addOptionalCharacteristic( + new BrightnessCharacteristic( + ((AccessoryWithBrightness) accessory)::getBrightness, + ((AccessoryWithBrightness) accessory)::setBrightness, + ((AccessoryWithBrightness) accessory)::subscribeBrightness, + ((AccessoryWithBrightness) accessory)::unsubscribeBrightness)); + } + if (accessory instanceof AccessoryWithPowerMode) { + addOptionalCharacteristic( + new PowerModeCharacteristic(((AccessoryWithPowerMode) accessory)::setPowerMode)); + } + if (accessory instanceof AccessoryWithClosedCaptions) { + addOptionalCharacteristic( + new ClosedCaptionsCharacteristic( + ((AccessoryWithClosedCaptions) accessory)::getClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::setClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::subscribeClosedCaptions, + ((AccessoryWithClosedCaptions) accessory)::unsubscribeClosedCaptions)); + } + if (accessory instanceof AccessoryWithCurrentMediaState) { + addOptionalCharacteristic( + new CurrentMediaStateCharacteristic( + ((AccessoryWithCurrentMediaState) accessory)::getCurrentMediaState, + ((AccessoryWithCurrentMediaState) accessory)::subscribeCurrentMediaState, + ((AccessoryWithCurrentMediaState) accessory)::unsubscribeCurrentMediaState)); + } + if (accessory instanceof AccessoryWithTargetMediaState) { + addOptionalCharacteristic( + new TargetMediaStateCharacteristic( + ((AccessoryWithTargetMediaState) accessory)::getTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::setTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::subscribeTargetMediaState, + ((AccessoryWithTargetMediaState) accessory)::unsubscribeTargetMediaState)); + } + if (accessory instanceof AccessoryWithPictureMode) { + addOptionalCharacteristic( + new PictureModeCharacteristic( + ((AccessoryWithPictureMode) accessory)::getPictureMode, + ((AccessoryWithPictureMode) accessory)::setPictureMode, + ((AccessoryWithPictureMode) accessory)::subscribePictureMode, + ((AccessoryWithPictureMode) accessory)::unsubscribePictureMode)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(PowerModeCharacteristic powerMode) { + addCharacteristic(powerMode); + } + + public void addOptionalCharacteristic(ClosedCaptionsCharacteristic closedCaptions) { + addCharacteristic(closedCaptions); + } + + public void addOptionalCharacteristic(CurrentMediaStateCharacteristic currentMediaState) { + addCharacteristic(currentMediaState); + } + + public void addOptionalCharacteristic(TargetMediaStateCharacteristic targetMediaState) { + addCharacteristic(targetMediaState); + } + + public void addOptionalCharacteristic(PictureModeCharacteristic pictureMode) { + addCharacteristic(pictureMode); + } + + public void addOptionalCharacteristic(BrightnessCharacteristic brightness) { + addCharacteristic(brightness); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java b/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java new file mode 100644 index 000000000..b53fb81bd --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/TelevisionSpeakerService.java @@ -0,0 +1,75 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.TelevisionSpeakerAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithActive; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolume; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolumeControlType; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithVolumeSelector; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; +import io.github.hapjava.characteristics.impl.audio.VolumeCharacteristic; +import io.github.hapjava.characteristics.impl.common.ActiveCharacteristic; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeControlTypeCharacteristic; +import io.github.hapjava.characteristics.impl.televisionspeaker.VolumeSelectorCharacteristic; + +/** This service describes a television. */ +public class TelevisionSpeakerService extends AbstractServiceImpl { + + public TelevisionSpeakerService(MuteCharacteristic muteCharacteristic) { + super("00000113-0000-1000-8000-0026BB765291"); + addCharacteristic(muteCharacteristic); + } + + public TelevisionSpeakerService(TelevisionSpeakerAccessory accessory) { + this( + new MuteCharacteristic( + accessory::isMuted, + accessory::setMute, + accessory::subscribeMuteState, + accessory::unsubscribeMuteState)); + if (accessory instanceof AccessoryWithActive) { + addOptionalCharacteristic( + new ActiveCharacteristic( + ((AccessoryWithActive) accessory)::getActive, + ((AccessoryWithActive) accessory)::setActive, + ((AccessoryWithActive) accessory)::subscribeActive, + ((AccessoryWithActive) accessory)::unsubscribeActive)); + } + if (accessory instanceof AccessoryWithVolume) { + addOptionalCharacteristic( + new VolumeCharacteristic( + ((AccessoryWithVolume) accessory)::getVolume, + ((AccessoryWithVolume) accessory)::setVolume, + ((AccessoryWithVolume) accessory)::subscribeVolume, + ((AccessoryWithVolume) accessory)::unsubscribeVolume)); + } + if (accessory instanceof AccessoryWithVolumeSelector) { + addOptionalCharacteristic( + new VolumeSelectorCharacteristic( + ((AccessoryWithVolumeSelector) accessory)::setVolumeSelector)); + } + if (accessory instanceof AccessoryWithVolumeControlType) { + addOptionalCharacteristic( + new VolumeControlTypeCharacteristic( + ((AccessoryWithVolumeControlType) accessory)::getVolumeControlType, + ((AccessoryWithVolumeControlType) accessory)::subscribeVolumeControlType, + ((AccessoryWithVolumeControlType) accessory)::unsubscribeVolumeControlType)); + ; + } + } + + public void addOptionalCharacteristic(ActiveCharacteristic activeCharacteristic) { + addCharacteristic(activeCharacteristic); + } + + public void addOptionalCharacteristic(VolumeCharacteristic volume) { + addCharacteristic(volume); + } + + public void addOptionalCharacteristic(VolumeSelectorCharacteristic volumeSelector) { + addCharacteristic(volumeSelector); + } + + public void addOptionalCharacteristic(VolumeControlTypeCharacteristic volumeControlType) { + addCharacteristic(volumeControlType); + } +} From 6f4166339808a88b9a6a9a08058b8b164d39e626 Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 3 Feb 2022 22:46:26 +0100 Subject: [PATCH 07/73] update readme Signed-off-by: Eugen Freiter --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 03fb0aaba..ba3cc6e7e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html) Supported HomeKit Accessories ========= -Current implementation fully supports 38 HomeKit accessory/services. +Current implementation fully supports 41 HomeKit accessory/services. | HomeKit Accessory & Service type | Supported by Java-HAP | |--------------------|--------------------| @@ -52,6 +52,7 @@ Current implementation fully supports 38 HomeKit accessory/services. | Heater Cooler | :white_check_mark: | | Humidifier Dehumidifier | :white_check_mark: | | Humidity Sensor | :white_check_mark: | +| Input Source | :white_check_mark: | | Irrigation System | :white_check_mark: | | Leak Sensor | :white_check_mark: | | Light Bulb | :white_check_mark: | @@ -72,6 +73,8 @@ Current implementation fully supports 38 HomeKit accessory/services. | Switch | :white_check_mark: | | Target Control | :x: | | Target Control Management | :x: | +| Television | :white_check_mark: | +| Television Speaker | :white_check_mark: | | Temperature Sensor | :white_check_mark: | | Thermostat | :white_check_mark: | | Valve | :white_check_mark: | From 40e0338174b9d76a9181cba29eeafeceba15a31c Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Fri, 4 Feb 2022 14:02:39 +0100 Subject: [PATCH 08/73] add smart speaker Signed-off-by: Eugen Freiter --- .../accessories/SmartSpeakerAccessory.java | 62 ++++++++++++ .../accessories/TelevisionAccessory.java | 8 +- .../AccessoryWithAirPlayEnable.java | 37 ++++++++ .../AccessoryWithConfiguredName.java | 34 +++++++ .../AccessoryWithMute.java | 37 ++++++++ .../common/AirPlayEnableCharacteristic.java | 32 +++++++ .../hapjava/services/impl/DoorService.java | 14 +++ .../services/impl/SmartSpeakerService.java | 95 +++++++++++++++++++ 8 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java create mode 100644 src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java create mode 100644 src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java create mode 100644 src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java diff --git a/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java b/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java new file mode 100644 index 000000000..9f70af2be --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/SmartSpeakerAccessory.java @@ -0,0 +1,62 @@ +package io.github.hapjava.accessories; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateEnum; +import io.github.hapjava.services.Service; +import io.github.hapjava.services.impl.SmartSpeakerService; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.CompletableFuture; + +/** Smart Speaker accessory. */ +public interface SmartSpeakerAccessory extends HomekitAccessory { + + /** + * Retrieves the current media state (see {@link + * io.github.hapjava.characteristics.impl.television.CurrentMediaStateEnum} for supported values). + * + * @return a future that will contain the current media state + */ + CompletableFuture getCurrentMediaState(); + + /** + * Subscribes to changes in the current media state. + * + * @param callback the function to call when the current media state changes. + */ + void subscribeCurrentMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the current media state. */ + void unsubscribeCurrentMediaState(); + + /** + * Retrieves the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @return a future that will contain the target media state + */ + CompletableFuture getTargetMediaState(); + + /** + * Set the target media state (see {@link TargetMediaStateEnum} for supported values). + * + * @param targetMediaState target media state + * @return a future that completes when the change is made + */ + CompletableFuture setTargetMediaState(TargetMediaStateEnum targetMediaState); + + /** + * Subscribes to changes in the target media state. + * + * @param callback the function to call when the target media state changes. + */ + void subscribeTargetMediaState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the target media state. */ + void unsubscribeTargetMediaState(); + + @Override + default Collection getServices() { + return Collections.singleton(new SmartSpeakerService(this)); + } +} diff --git a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java index 3a1a1c515..b51efe319 100644 --- a/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java +++ b/src/main/java/io/github/hapjava/accessories/TelevisionAccessory.java @@ -72,7 +72,7 @@ public interface TelevisionAccessory extends HomekitAccessory { CompletableFuture getConfiguredName(); /** - * Sets the mute status + * Sets the configured name * * @param name configured name * @return a future that completes when the change is made @@ -81,13 +81,13 @@ public interface TelevisionAccessory extends HomekitAccessory { CompletableFuture setConfiguredName(String name) throws Exception; /** - * Subscribes to changes in mute state. + * Subscribes to changes in configured name. * - * @param callback the function to call when the state changes. + * @param callback the function to call when the configureed name changes. */ void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); - /** Unsubscribes from changes in the mute state. */ + /** Unsubscribes from changes in the configured name state. */ void unsubscribeConfiguredName(); /** diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java new file mode 100644 index 000000000..76b90b43f --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithAirPlayEnable.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with AirPlay enable characteristic {@link + * io.github.hapjava.characteristics.impl.common.AirPlayEnableCharacteristic}. + */ +public interface AccessoryWithAirPlayEnable { + + /** + * Retrieves the AirPlay enable state + * + * @return a future that will contain the AirPlay enable state. + */ + CompletableFuture getAirPlayEnable(); + + /** + * Sets the AirPlay enable state + * + * @param state AirPlay enable state + * @return a future that completes when the AirPlay enable is changed + * @throws Exception when the AirPlay enable cannot be set + */ + CompletableFuture setAirPlayEnable(Integer state) throws Exception; + + /** + * Subscribes to changes in the AirPlay enable state. + * + * @param callback the function to call when the AirPlay enable state changes. + */ + void subscribeAirPlayEnable(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the AirPlay enable state. */ + void unsubscribeAirPlayEnable(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java new file mode 100644 index 000000000..0c9528130 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithConfiguredName.java @@ -0,0 +1,34 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** Accessory with configured name. */ +public interface AccessoryWithConfiguredName { + + /** + * Retrieves configured name. + * + * @return configured name + */ + CompletableFuture getConfiguredName(); + + /** + * Sets the configured name + * + * @param name configured name + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setConfiguredName(String name) throws Exception; + + /** + * Subscribes to changes in configured name. + * + * @param callback the function to call when the configureed name changes. + */ + void subscribeConfiguredName(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the configured name state. */ + void unsubscribeConfiguredName(); +} diff --git a/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java new file mode 100644 index 000000000..a0bb08579 --- /dev/null +++ b/src/main/java/io/github/hapjava/accessories/optionalcharacteristic/AccessoryWithMute.java @@ -0,0 +1,37 @@ +package io.github.hapjava.accessories.optionalcharacteristic; + +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import java.util.concurrent.CompletableFuture; + +/** + * Accessory with mute characteristic {@link + * io.github.hapjava.characteristics.impl.audio.MuteCharacteristic}. + */ +public interface AccessoryWithMute { + + /** + * Retrieves mute status. + * + * @return true if accessory is muted + */ + CompletableFuture isMuted(); + + /** + * Sets the mute status + * + * @param mute true if accessory should be muted + * @return a future that completes when the change is made + * @throws Exception when the change cannot be made + */ + CompletableFuture setMute(boolean mute) throws Exception; + + /** + * Subscribes to changes in mute state. + * + * @param callback the function to call when the state changes. + */ + void subscribeMuteState(HomekitCharacteristicChangeCallback callback); + + /** Unsubscribes from changes in the mute state. */ + void unsubscribeMuteState(); +} diff --git a/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java new file mode 100644 index 000000000..6158f13be --- /dev/null +++ b/src/main/java/io/github/hapjava/characteristics/impl/common/AirPlayEnableCharacteristic.java @@ -0,0 +1,32 @@ +package io.github.hapjava.characteristics.impl.common; + +import io.github.hapjava.characteristics.EventableCharacteristic; +import io.github.hapjava.characteristics.ExceptionalConsumer; +import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback; +import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** characteristic to control air play. */ +public class AirPlayEnableCharacteristic extends IntegerCharacteristic + implements EventableCharacteristic { + + public AirPlayEnableCharacteristic( + Supplier> getter, + ExceptionalConsumer setter, + Consumer subscriber, + Runnable unsubscriber) { + super( + "0000025B-0000-1000-8000-0026BB765291", + "AirPlay enable", + 0, + 1, + "", + Optional.of(getter), + Optional.of(setter), + Optional.of(subscriber), + Optional.of(unsubscriber)); + } +} diff --git a/src/main/java/io/github/hapjava/services/impl/DoorService.java b/src/main/java/io/github/hapjava/services/impl/DoorService.java index c4c3b40b3..51f956f5f 100644 --- a/src/main/java/io/github/hapjava/services/impl/DoorService.java +++ b/src/main/java/io/github/hapjava/services/impl/DoorService.java @@ -2,8 +2,10 @@ import io.github.hapjava.accessories.DoorAccessory; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithHoldPosition; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithMute; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName; import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithObstructionDetection; +import io.github.hapjava.characteristics.impl.audio.MuteCharacteristic; import io.github.hapjava.characteristics.impl.common.NameCharacteristic; import io.github.hapjava.characteristics.impl.common.ObstructionDetectedCharacteristic; import io.github.hapjava.characteristics.impl.windowcovering.CurrentPositionCharacteristic; @@ -53,6 +55,14 @@ public DoorService(DoorAccessory accessory) { ((AccessoryWithObstructionDetection) accessory)::subscribeObstructionDetected, ((AccessoryWithObstructionDetection) accessory)::unsubscribeObstructionDetected)); } + if (accessory instanceof AccessoryWithMute) { + addOptionalCharacteristic( + new MuteCharacteristic( + ((AccessoryWithMute) accessory)::isMuted, + ((AccessoryWithMute) accessory)::setMute, + ((AccessoryWithMute) accessory)::subscribeMuteState, + ((AccessoryWithMute) accessory)::unsubscribeMuteState)); + } } public void addOptionalCharacteristic(NameCharacteristic name) { @@ -67,4 +77,8 @@ public void addOptionalCharacteristic( ObstructionDetectedCharacteristic obstructionDetectedCharacteristic) { addCharacteristic(obstructionDetectedCharacteristic); } + + public void addOptionalCharacteristic(MuteCharacteristic muteCharacteristic) { + addCharacteristic(muteCharacteristic); + } } diff --git a/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java b/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java new file mode 100644 index 000000000..e5f57eff4 --- /dev/null +++ b/src/main/java/io/github/hapjava/services/impl/SmartSpeakerService.java @@ -0,0 +1,95 @@ +package io.github.hapjava.services.impl; + +import io.github.hapjava.accessories.SmartSpeakerAccessory; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithAirPlayEnable; +import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithConfiguredName; +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.AirPlayEnableCharacteristic; +import io.github.hapjava.characteristics.impl.common.ConfiguredNameCharacteristic; +import io.github.hapjava.characteristics.impl.common.NameCharacteristic; +import io.github.hapjava.characteristics.impl.television.CurrentMediaStateCharacteristic; +import io.github.hapjava.characteristics.impl.television.TargetMediaStateCharacteristic; + +/** A smart speaker service can be used to control the audio output settings on a speaker device. */ +public class SmartSpeakerService extends AbstractServiceImpl { + + public SmartSpeakerService( + CurrentMediaStateCharacteristic currentMediaStateCharacteristic, + TargetMediaStateCharacteristic targetMediaStateCharacteristic) { + super("00000228-0000-1000-8000-0026BB765291"); + addCharacteristic(currentMediaStateCharacteristic); + addCharacteristic(targetMediaStateCharacteristic); + } + + public SmartSpeakerService(SmartSpeakerAccessory accessory) { + this( + new CurrentMediaStateCharacteristic( + accessory::getCurrentMediaState, + accessory::subscribeCurrentMediaState, + accessory::unsubscribeCurrentMediaState), + new TargetMediaStateCharacteristic( + accessory::getTargetMediaState, + accessory::setTargetMediaState, + accessory::subscribeTargetMediaState, + accessory::unsubscribeTargetMediaState)); + if (accessory instanceof AccessoryWithName) { + addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName)); + } + if (accessory instanceof AccessoryWithVolume) { + addOptionalCharacteristic( + new VolumeCharacteristic( + ((AccessoryWithVolume) accessory)::getVolume, + ((AccessoryWithVolume) accessory)::setVolume, + ((AccessoryWithVolume) accessory)::subscribeVolume, + ((AccessoryWithVolume) accessory)::unsubscribeVolume)); + } + if (accessory instanceof AccessoryWithConfiguredName) { + addOptionalCharacteristic( + new ConfiguredNameCharacteristic( + ((AccessoryWithConfiguredName) accessory)::getConfiguredName, + ((AccessoryWithConfiguredName) accessory)::setConfiguredName, + ((AccessoryWithConfiguredName) accessory)::subscribeConfiguredName, + ((AccessoryWithConfiguredName) accessory)::unsubscribeConfiguredName)); + } + if (accessory instanceof AccessoryWithMute) { + addOptionalCharacteristic( + new MuteCharacteristic( + ((AccessoryWithMute) accessory)::isMuted, + ((AccessoryWithMute) accessory)::setMute, + ((AccessoryWithMute) accessory)::subscribeMuteState, + ((AccessoryWithMute) accessory)::unsubscribeMuteState)); + } + if (accessory instanceof AccessoryWithAirPlayEnable) { + addOptionalCharacteristic( + new AirPlayEnableCharacteristic( + ((AccessoryWithAirPlayEnable) accessory)::getAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::setAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::subscribeAirPlayEnable, + ((AccessoryWithAirPlayEnable) accessory)::unsubscribeAirPlayEnable)); + } + } + + public void addOptionalCharacteristic(NameCharacteristic name) { + addCharacteristic(name); + } + + public void addOptionalCharacteristic(VolumeCharacteristic volume) { + addCharacteristic(volume); + } + + public void addOptionalCharacteristic(ConfiguredNameCharacteristic configuredNameCharacteristic) { + addCharacteristic(configuredNameCharacteristic); + } + + public void addOptionalCharacteristic(MuteCharacteristic muteCharacteristic) { + addCharacteristic(muteCharacteristic); + } + + public void addOptionalCharacteristic(AirPlayEnableCharacteristic airPlayEnableCharacteristic) { + addCharacteristic(airPlayEnableCharacteristic); + } +} From c17a9d914e7446c6ec14e5c8d894df4691c5c2ee Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Sat, 5 Feb 2022 10:45:33 +0100 Subject: [PATCH 09/73] upate README Signed-off-by: Eugen Freiter --- CHANGES.md | 3 +++ README.md | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0f293c195..400e99ae2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Fix rotation speed data type (BREAKING API CHANGE). According to HAP specification it must be float * Close JsonWriters [#149](https://github.com/hap-java/HAP-Java/issues/149) * Fix UUID of HAP Version characteristic +* Add Mute characteristic to Doorbell service ## New * New characteristics: @@ -23,11 +24,13 @@ * Remote Key * Volume Control Type * Volume Selector + * AirPlay Enable * New services * Input Source * Television * Television Speaker + * Smart Speaker # HAP-Java 2.0.0 * major refactoring to support optional characteristics diff --git a/README.md b/README.md index ba3cc6e7e..6d1f88458 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Include HAP-Java in your project using maven: io.github.hap-java hap - 2.0.0-SNAPSHOT + 2.0.1-SNAPSHOT ``` @@ -28,7 +28,7 @@ read the [Javadoc](https://hap-java.github.io/HAP-Java/apidocs/index.html) Supported HomeKit Accessories ========= -Current implementation fully supports 41 HomeKit accessory/services. +Current implementation fully supports 42 HomeKit accessory/services. | HomeKit Accessory & Service type | Supported by Java-HAP | |--------------------|--------------------| @@ -67,6 +67,7 @@ Current implementation fully supports 41 HomeKit accessory/services. | Service Label | :white_check_mark: | | Siri | :x: | | Slat | :white_check_mark: | +| Smart Speaker | :white_check_mark: | | Smoke Sensor | :white_check_mark: | | Speaker | :white_check_mark: | | Stateless Programmable Switch | :white_check_mark: | From 4d9da49cb5e5443bf6faf4711f77a91a21e51bdf Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Sun, 6 Feb 2022 14:05:04 +0100 Subject: [PATCH 10/73] remove unnecessary logging Signed-off-by: Eugen Freiter --- .../io/github/hapjava/services/impl/AbstractServiceImpl.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java index 2848d367e..4dfd9f5f6 100644 --- a/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java +++ b/src/main/java/io/github/hapjava/services/impl/AbstractServiceImpl.java @@ -16,7 +16,6 @@ abstract class AbstractServiceImpl implements Service { /** @param type unique UUID of the service according to HAP specification. */ public AbstractServiceImpl(String type) { - logger.info("new service:" + type); this.type = type; } @@ -32,7 +31,6 @@ public String getType() { @Override public List getLinkedServices() { - logger.info(" all linked service" + this.linkedServices); return Collections.unmodifiableList(linkedServices); } @@ -42,8 +40,6 @@ public void addCharacteristic(Characteristic characteristic) { @Override public void addLinkedService(Service service) { - logger.info("add linked service" + service); this.linkedServices.add(service); - logger.info(" all linked service" + this.linkedServices); } } From 1f72143a2c88fb0a14d9b21ad3106d7feb288e7c Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 10 Feb 2022 11:46:55 +0100 Subject: [PATCH 11/73] add support for HomeKit categories Signed-off-by: Eugen Freiter --- .../server/HomekitAccessoryCategories.java | 40 ++++++++++++++++++ .../hapjava/server/impl/HomekitRoot.java | 41 ++++++++++++++++--- .../hapjava/server/impl/HomekitServer.java | 5 ++- .../impl/http/impl/AccessoryHandler.java | 6 +-- .../http/impl/DefaultHttpRequestImpl.java | 4 +- .../impl/http/impl/NettyResponseUtil.java | 7 ++-- .../impl/jmdns/JmdnsHomekitAdvertiser.java | 9 +++- .../hapjava/server/impl/HomekitRootTest.java | 6 ++- .../jmdns/JmdnsHomekitAdvertiserTest.java | 2 +- 9 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java diff --git a/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java b/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java new file mode 100644 index 000000000..05768a947 --- /dev/null +++ b/src/main/java/io/github/hapjava/server/HomekitAccessoryCategories.java @@ -0,0 +1,40 @@ +package io.github.hapjava.server; + +public final class HomekitAccessoryCategories { + public static final int OTHER = 1; + public static final int BRIDGES = 2; + public static final int FANS = 3; + public static final int GARAGE_DOOR_OPENERS = 4; + public static final int LIGHTING = 5; + public static final int LOCKS = 6; + public static final int OUTLETS = 7; + public static final int SWITCHES = 8; + public static final int THERMOSTATS = 9; + public static final int SENSORS = 10; + public static final int SECURITY_SYSTEMS = 11; + public static final int DOORS = 12; + public static final int WINDOWS = 13; + public static final int WINDOW_COVERINGS = 14; + public static final int PROGRAMMABLE_SWITCHES = 15; + public static final int RANGE_EXTENDER = 16; + public static final int IP_CAMERAS = 17; + public static final int VIDEO_DOORBELLS = 18; + public static final int AIR_PURIFIERS = 19; + public static final int HEATERS = 20; + public static final int AIR_CONDITIONERS = 21; + public static final int HUMIDIFIERS = 22; + public static final int DEHUMIDIFIERS = 23; + public static final int APPLE_TV = 24; + public static final int HOMEPOD = 25; + public static final int SPEAKER = 26; + public static final int AIRPORT = 27; + public static final int SPRINKLERS = 28; + public static final int FAUCETS = 29; + public static final int SHOWER_SYSTEMS = 30; + public static final int TELEVISION = 31; + public static final int REMOTES = 32; + public static final int ROUTER = 33; + public static final int AUDIO_RECEIVER = 34; + public static final int TV_SET_TOP_BOX = 35; + public static final int TV_STREAMING_STICK = 36; +} 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 e619fa1e7..ca182f812 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java @@ -2,6 +2,7 @@ import io.github.hapjava.accessories.Bridge; import io.github.hapjava.accessories.HomekitAccessory; +import io.github.hapjava.server.HomekitAccessoryCategories; import io.github.hapjava.server.HomekitAuthInfo; import io.github.hapjava.server.HomekitWebHandler; import io.github.hapjava.server.impl.connections.HomekitClientConnectionFactoryImpl; @@ -25,24 +26,36 @@ public class HomekitRoot { private static final Logger logger = LoggerFactory.getLogger(HomekitRoot.class); - + private static final int DEFAULT_ACCESSORY_CATEGORY = HomekitAccessoryCategories.OTHER; private final JmdnsHomekitAdvertiser advertiser; private final HomekitWebHandler webHandler; private final HomekitAuthInfo authInfo; private final String label; + private final int category; private final HomekitRegistry registry; private final SubscriptionManager subscriptions = new SubscriptionManager(); private boolean started = false; private int configurationIndex = 1; HomekitRoot( - String label, HomekitWebHandler webHandler, InetAddress localhost, HomekitAuthInfo authInfo) + String label, HomekitWebHandler webHandler, InetAddress host, HomekitAuthInfo authInfo) + throws IOException { + this(label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(host)); + } + + HomekitRoot( + String label, + int category, + HomekitWebHandler webHandler, + InetAddress host, + HomekitAuthInfo authInfo) throws IOException { - this(label, webHandler, authInfo, new JmdnsHomekitAdvertiser(localhost)); + this(label, category, webHandler, authInfo, new JmdnsHomekitAdvertiser(host)); } HomekitRoot( String label, + int category, HomekitWebHandler webHandler, HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) @@ -51,14 +64,25 @@ public class HomekitRoot { this.webHandler = webHandler; this.authInfo = authInfo; this.label = label; + this.category = category; this.registry = new HomekitRegistry(label); } - HomekitRoot(String label, HomekitWebHandler webHandler, JmDNS jmdns, HomekitAuthInfo authInfo) + HomekitRoot( + String label, + int category, + HomekitWebHandler webHandler, + JmDNS jmdns, + HomekitAuthInfo authInfo) throws IOException { - this(label, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns)); + this(label, category, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns)); } + HomekitRoot(String label, HomekitWebHandler webHandler, JmDNS jmdns, HomekitAuthInfo authInfo) + throws IOException { + this( + label, DEFAULT_ACCESSORY_CATEGORY, webHandler, authInfo, new JmdnsHomekitAdvertiser(jmdns)); + } /** * 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 @@ -127,7 +151,12 @@ public void start() { try { refreshAuthInfo(); advertiser.advertise( - label, authInfo.getMac(), port, configurationIndex, authInfo.getSetupId()); + label, + category, + authInfo.getMac(), + port, + configurationIndex, + authInfo.getSetupId()); } catch (Exception e) { throw new RuntimeException(e); } 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 d348ca682..af129a7a8 100644 --- a/src/main/java/io/github/hapjava/server/impl/HomekitServer.java +++ b/src/main/java/io/github/hapjava/server/impl/HomekitServer.java @@ -139,6 +139,7 @@ public HomekitStandaloneAccessoryServer createStandaloneAccessory( public HomekitRoot createBridge( HomekitAuthInfo authInfo, String label, + int category, String manufacturer, String model, String serialNumber, @@ -147,9 +148,9 @@ public HomekitRoot createBridge( throws IOException { HomekitRoot root; if (jmdns != null) { - root = new HomekitRoot(label, http, jmdns, authInfo); + root = new HomekitRoot(label, category, http, jmdns, authInfo); } else { - root = new HomekitRoot(label, http, localAddress, authInfo); + root = new HomekitRoot(label, category, http, localAddress, authInfo); } root.addAccessory( new HomekitBridge( diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java b/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java index 0e1bf1146..6b840c713 100644 --- a/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/http/impl/AccessoryHandler.java @@ -72,9 +72,9 @@ private void sendResponse( HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(responseBody.getBytes(StandardCharsets.UTF_8))); - response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain"); - response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes()); - response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); ctx.write(response); ctx.flush(); } diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java b/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java index 48a3ceb35..a124705f1 100644 --- a/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java +++ b/src/main/java/io/github/hapjava/server/impl/http/impl/DefaultHttpRequestImpl.java @@ -13,7 +13,7 @@ public DefaultHttpRequestImpl(io.netty.handler.codec.http.HttpRequest request) { @Override public String getUri() { - return request.getUri(); + return request.uri(); } @Override @@ -23,7 +23,7 @@ public byte[] getBody() { @Override public HttpMethod getMethod() { - io.netty.handler.codec.http.HttpMethod method = request.getMethod(); + io.netty.handler.codec.http.HttpMethod method = request.method(); if (method.equals(io.netty.handler.codec.http.HttpMethod.GET)) { return HttpMethod.GET; } else if (method.equals(io.netty.handler.codec.http.HttpMethod.POST)) { diff --git a/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java b/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java index fba52eef5..9e7963fb3 100644 --- a/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java +++ b/src/main/java/io/github/hapjava/server/impl/http/impl/NettyResponseUtil.java @@ -4,7 +4,8 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import java.util.Map.Entry; @@ -25,8 +26,8 @@ public static FullHttpResponse createResponse(HttpResponse homekitResponse) { for (Entry header : homekitResponse.getHeaders().entrySet()) { response.headers().add(header.getKey(), header.getValue()); } - response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes()); - response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); return response; } } 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 04016561e..1535a490a 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 @@ -27,6 +27,7 @@ public class JmdnsHomekitAdvertiser { private int port; private int configurationIndex; private ServiceInfo serviceInfo; + private int category; public JmdnsHomekitAdvertiser(JmDNS jmdns) { this.jmdns = jmdns; @@ -37,7 +38,8 @@ public JmdnsHomekitAdvertiser(InetAddress localAddress) throws UnknownHostExcept } public synchronized void advertise( - String label, String mac, int port, int configurationIndex, String setupId) throws Exception { + String label, int category, String mac, int port, int configurationIndex, String setupId) + throws Exception { if (isAdvertising) { throw new IllegalStateException("HomeKit advertiser is already running"); } @@ -45,6 +47,7 @@ public synchronized void advertise( this.mac = mac; this.port = port; this.setupId = setupId; + this.category = category; this.configurationIndex = configurationIndex; logger.trace("Advertising accessory " + label); @@ -114,7 +117,9 @@ private ServiceInfo buildServiceInfo() { props.put("c#", Integer.toString(configurationIndex)); props.put("s#", "1"); props.put("ff", "0"); - props.put("ci", "1"); + props.put("ci", Integer.toString(category)); + props.put("pv", "1.1"); + return ServiceInfo.create(SERVICE_TYPE, label, port, 1, 1, props); } } 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 719fea9d1..3ac40288a 100644 --- a/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java +++ b/src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.when; import io.github.hapjava.accessories.HomekitAccessory; +import io.github.hapjava.server.HomekitAccessoryCategories; import io.github.hapjava.server.HomekitAuthInfo; import io.github.hapjava.server.HomekitWebHandler; import io.github.hapjava.server.impl.http.HomekitClientConnectionFactory; @@ -38,7 +39,8 @@ public void setup() throws Exception { when(webHandler.start(any())).thenReturn(CompletableFuture.completedFuture(PORT)); advertiser = mock(JmdnsHomekitAdvertiser.class); authInfo = mock(HomekitAuthInfo.class); - root = new HomekitRoot(LABEL, webHandler, authInfo, advertiser); + root = + new HomekitRoot(LABEL, HomekitAccessoryCategories.OTHER, webHandler, authInfo, advertiser); } @Test @@ -78,7 +80,7 @@ public void testAdvertiserStarts() throws Exception { when(authInfo.getSetupId()).thenReturn(SETUPID); root.start(); - verify(advertiser).advertise(eq(LABEL), eq(mac), eq(PORT), eq(1), eq(SETUPID)); + verify(advertiser).advertise(eq(LABEL), eq(1), eq(mac), eq(PORT), eq(1), eq(SETUPID)); } @Test 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 1c04b2868..870bd739b 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 @@ -61,6 +61,6 @@ private ServiceInfo getArgumentFromUnregister() { } private void advertise() throws Exception { - subject.advertise("test", "00:00:00:00:00:00", 1234, 1, "1"); + subject.advertise("test", 1, "00:00:00:00:00:00", 1234, 1, "1"); } } From 234b785caed8dfc57e10c1c9083ae5680823a469 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 11 Feb 2022 13:54:56 +0000 Subject: [PATCH 12/73] [maven-release-plugin] prepare release hap-2.0.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6adc9e33f..73297653a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.1-SNAPSHOT + 2.0.1 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.1 From aca73d5ce78990f6f54f37b8fbe87308ec471052 Mon Sep 17 00:00:00 2001 From: eugen Date: Fri, 11 Feb 2022 16:06:12 +0100 Subject: [PATCH 13/73] revert changes done by release --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 73297653a..6adc9e33f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.1 + 2.0.1-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.1 + HEAD From 1a664e45e4577433cc4e657cfd9c4110212bbac5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 11 Feb 2022 15:14:32 +0000 Subject: [PATCH 14/73] [maven-release-plugin] prepare release hap-2.0.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6adc9e33f..73297653a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.1-SNAPSHOT + 2.0.1 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.1 From 7dc243be6ae2c9963ee2c7ca50c925e099a1ed95 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 11 Feb 2022 15:14:33 +0000 Subject: [PATCH 15/73] [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 73297653a..ba85a561c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.1 + 2.0.2-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.1 + HEAD From b8fb0f9478b6ff9f0a38fa8a2cb263d1bb98b178 Mon Sep 17 00:00:00 2001 From: Eugen Freiter Date: Thu, 17 Feb 2022 14:12:03 +0100 Subject: [PATCH 16/73] mark primary service Signed-off-by: Eugen Freiter --- .../io/github/hapjava/accessories/HomekitAccessory.java | 2 +- .../hapjava/server/impl/json/AccessoryController.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java b/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java index f61fd223d..5e2bbe3b6 100644 --- a/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java +++ b/src/main/java/io/github/hapjava/accessories/HomekitAccessory.java @@ -86,6 +86,6 @@ default Collection getServices() { * @return primary service */ default Service getPrimaryService() { - return getServices().iterator().next(); + return getServices().isEmpty() ? null : getServices().iterator().next(); }; } diff --git a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java index ef21422af..fdd223ec3 100644 --- a/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java +++ b/src/main/java/io/github/hapjava/server/impl/json/AccessoryController.java @@ -42,7 +42,7 @@ public HttpResponse listing() throws Exception { iidLookup.putAll(swapKeyAndValue(registry.getCharacteristics(accessory.getId()))); for (Service service : servicesByInterfaceId.values()) { - serviceFutures.add(toJson(service, iidLookup)); + serviceFutures.add(toJson(service, iidLookup, accessory.getPrimaryService() == service)); } accessoryServiceFutures.put(accessory.getId(), serviceFutures); @@ -72,8 +72,8 @@ public HttpResponse listing() throws Exception { } } - private CompletableFuture toJson(Service service, Map iidLookup) - throws Exception { + private CompletableFuture toJson( + Service service, Map iidLookup, boolean isPrimary) throws Exception { String shortType = service.getType().replaceAll("^0*([0-9a-fA-F]+)-0000-1000-8000-0026BB765291$", "$1"); JsonObjectBuilder builder = @@ -103,7 +103,7 @@ private CompletableFuture toJson(Service service, Map Date: Tue, 11 Oct 2022 15:19:58 -0600 Subject: [PATCH 17/73] avoid null pointer error if a characteristic goes missing like if a characteristic has been removed, but the iOS device hasn't updated its db yet, and tries to set it --- .../server/impl/json/CharacteristicsController.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 4d20a0c6b..d061a11fc 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 @@ -51,7 +51,7 @@ public HttpResponse get(HttpRequest request) throws Exception { characteristics.add(characteristic.add("aid", aid).add("iid", iid).build()); } else { logger.warn( - "Accessory " + aid + " does not have characteristic " + iid + "Request: " + uri); + "Accessory " + aid + " does not have characteristic " + iid + ". Request: " + uri); } } else { logger.warn( @@ -77,7 +77,16 @@ public HttpResponse put(HttpRequest request, HomekitClientConnection connection) JsonObject jsonCharacteristic = (JsonObject) value; int aid = jsonCharacteristic.getInt("aid"); int iid = jsonCharacteristic.getInt("iid"); - Characteristic characteristic = registry.getCharacteristics(aid).get(iid); + Map accessory = registry.getCharacteristics(aid); + if (accessory.isEmpty()) { + logger.warn("Accessory {} has no characteristics or does not exist.", aid); + return new HapJsonNoContentResponse(); + } + Characteristic characteristic = accessory.get(iid); + if (characteristic == null) { + logger.warn("Accessory {} does not have characteristic {}.", aid, iid); + return new HapJsonNoContentResponse(); + } if (jsonCharacteristic.containsKey("value")) { characteristic.setValue(jsonCharacteristic.get("value")); From 7d5f01eb99d3c2fb72a4db622bad40647e3f4c53 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Tue, 11 Oct 2022 15:21:56 -0600 Subject: [PATCH 18/73] color temperature is in mireds, not Kelvin --- .../impl/lightbulb/ColorTemperatureCharacteristic.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/ColorTemperatureCharacteristic.java b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/ColorTemperatureCharacteristic.java index 12886c7a1..987ab5eee 100644 --- a/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/ColorTemperatureCharacteristic.java +++ b/src/main/java/io/github/hapjava/characteristics/impl/lightbulb/ColorTemperatureCharacteristic.java @@ -9,7 +9,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; -/** This characteristic describes color temperature in Kelvin */ +/** This characteristic describes color temperature in mireds */ public class ColorTemperatureCharacteristic extends IntegerCharacteristic implements EventableCharacteristic { public static final int DEFAULT_MIN_VALUE = 50; @@ -27,7 +27,7 @@ public ColorTemperatureCharacteristic( "color temperature", minValue, maxValue, - "K", + "mired", Optional.of(getter), Optional.of(setter), Optional.of(subscriber), From 64972c8bf860a9d200c3b26a4f530cb10c160ced Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Tue, 11 Oct 2022 15:22:09 -0600 Subject: [PATCH 19/73] make BaseCharacteristic.getDefault public --- .../hapjava/characteristics/impl/base/BaseCharacteristic.java | 2 +- .../characteristics/impl/base/BooleanCharacteristic.java | 2 +- .../hapjava/characteristics/impl/base/EnumCharacteristic.java | 2 +- .../hapjava/characteristics/impl/base/FloatCharacteristic.java | 2 +- .../characteristics/impl/base/IntegerCharacteristic.java | 2 +- .../characteristics/impl/base/StaticStringCharacteristic.java | 2 +- .../hapjava/characteristics/impl/base/StringCharacteristic.java | 2 +- 7 files changed, 7 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 a90f1fadc..a0a302c29 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 @@ -181,7 +181,7 @@ public void unsubscribe() { * * @return a sensible default value. */ - protected abstract T getDefault(); + public abstract T getDefault(); /** * Writes the value key to the serialized 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 83d7f765a..3e07936a8 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 @@ -71,7 +71,7 @@ protected void setValue(Boolean value) throws Exception { /** {@inheritDoc} */ @Override - protected Boolean getDefault() { + public Boolean getDefault() { return false; } } 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 f983efe16..df3287de5 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 @@ -111,7 +111,7 @@ protected void setValue(Integer value) throws Exception { /** {@inheritDoc} */ @Override - protected Integer getDefault() { + public Integer getDefault() { // as default return first item from valid values if (validValues != null && validValues.length > 0) { return validValues[0].getCode(); 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 e15da4fde..3781cc14d 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 @@ -135,7 +135,7 @@ protected void setValue(Double value) throws Exception { /** {@inheritDoc} */ @Override - protected Double getDefault() { + public Double getDefault() { return minValue; } } 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 fcd804ccd..3ea8d87bc 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 @@ -82,7 +82,7 @@ protected void setValue(Integer value) throws Exception { /** {@inheritDoc} */ @Override - protected Integer getDefault() { + public Integer getDefault() { return minValue; } 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 d5de2f9ce..ec851bbf7 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 @@ -65,7 +65,7 @@ protected CompletableFuture getValue() { /** {@inheritDoc} */ @Override - protected String getDefault() { + public String getDefault() { return "Unknown"; } } 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 4373b06c9..5282042d4 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 @@ -74,7 +74,7 @@ protected CompletableFuture getValue() { /** {@inheritDoc} */ @Override - protected String getDefault() { + public String getDefault() { return "Unknown"; } } From daa07eecee6a04a530ac4d33623822be28c760e1 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Tue, 11 Oct 2022 15:56:18 -0600 Subject: [PATCH 20/73] fix various small bugs where a BaseCharacteristic was given null for a callback --- CHANGES.md | 3 +++ .../impl/base/BaseCharacteristic.java | 19 +++++++++++++++---- .../impl/base/FloatCharacteristic.java | 2 +- .../impl/base/IntegerCharacteristic.java | 2 +- .../impl/base/StaticStringCharacteristic.java | 2 +- .../impl/base/StringCharacteristic.java | 4 ++-- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 400e99ae2..debc2dfbf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# HAP-Java 2.0.2 + * Various minor stability issues for after an accessory has been removed. + # HAP-Java 2.0.1 ## Fixes * Log accessory names instead of futures. [#150](https://github.com/hap-java/HAP-Java/issues/150) 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 a0a302c29..77b0bc452 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 @@ -125,15 +125,26 @@ public final void setValue(JsonValue jsonValue) { try { setValue(convert(jsonValue)); } catch (Exception e) { - logger.warn("Error while setting JSON value", e); + logger.warn( + "Error while setting JSON value {} for characteristic {}", + jsonValue, + getClass().getName(), + e); } } /** {@inheritDoc} */ @Override public void supplyValue(JsonObjectBuilder builder) { + CompletableFuture futureValue = getValue(); + + if (futureValue == null) { + setJsonValue(builder, getDefault()); + return; + } + try { - setJsonValue(builder, getValue().get()); + setJsonValue(builder, futureValue.get()); } catch (InterruptedException | ExecutionException e) { logger.warn("Error retrieving value", e); setJsonValue(builder, getDefault()); @@ -143,13 +154,13 @@ public void supplyValue(JsonObjectBuilder builder) { /** {@inheritDoc} */ @Override public void subscribe(HomekitCharacteristicChangeCallback callback) { - subscriber.get().accept(callback); + subscriber.ifPresent(s -> s.accept(callback)); } /** {@inheritDoc} */ @Override public void unsubscribe() { - unsubscriber.get().run(); + unsubscriber.ifPresent(u -> u.run()); } /** 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 3781cc14d..a8a98dc27 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,7 @@ protected final CompletableFuture getValue() { @Override protected void setValue(Double value) throws Exception { - setter.get().accept(value); + if (setter.isPresent()) setter.get().accept(value); } /** {@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 3ea8d87bc..22a6dce83 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,7 @@ protected CompletableFuture getValue() { @Override protected void setValue(Integer value) throws Exception { - setter.get().accept(value); + if (setter.isPresent()) setter.get().accept(value); } /** {@inheritDoc} */ 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 ec851bbf7..576df7e0e 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 @@ -60,7 +60,7 @@ public void setValue(String value) throws Exception { /** {@inheritDoc} */ @Override protected CompletableFuture getValue() { - return getter.map(stringGetter -> stringGetter.get()).get(); + return getter.map(stringGetter -> stringGetter.get()).orElse(null); } /** {@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 5282042d4..65cf45a71 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,13 +63,13 @@ public String convert(JsonValue jsonValue) { /** {@inheritDoc} */ @Override public void setValue(String value) throws Exception { - setter.get().accept(value); + if (setter.isPresent()) setter.get().accept(value); } /** {@inheritDoc} */ @Override protected CompletableFuture getValue() { - return getter.map(stringGetter -> stringGetter.get()).get(); + return getter.map(stringGetter -> stringGetter.get()).orElse(null); } /** {@inheritDoc} */ From f28f6082b5148fa9c9ea3fec0886f10968756c7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 12 Oct 2022 14:35:31 +0000 Subject: [PATCH 21/73] [maven-release-plugin] prepare release hap-2.0.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ba85a561c..79bdd1c69 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.2-SNAPSHOT + 2.0.2 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.2 From 0ae78f684bef1c7051bebeb998c4326898bf92a9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 12 Oct 2022 14:35:33 +0000 Subject: [PATCH 22/73] [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 79bdd1c69..b682cf656 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ hap hap-java Homekit Accessory Protocol for Java - 2.0.2 + 2.0.3-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.2 + HEAD From 781311e94c7967080119daec41d288a3a90b7691 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Wed, 12 Oct 2022 08:51:02 -0600 Subject: [PATCH 23/73] update git user for release action see https://github.com/actions/checkout/issues/13#issuecomment-724415212 also update the actions to non-deprecated versions --- .github/workflows/deploy.yml | 9 +++++---- .github/workflows/maven.yml | 5 +++-- .github/workflows/release.yml | 9 +++++---- RELEASING.md | 2 +- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d439198c7..ba402983e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,10 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 for Maven Central - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: '11' architecture: x64 server-id: ossrh @@ -26,8 +27,8 @@ jobs: server-password: MAVEN_CENTRAL_TOKEN - name: Setup git profile run: | - git config --global user.name github-actions - git config --global user.email github-actions@github.com + 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 env: diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a07bff253..c7f0c8de8 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -13,10 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: '11' architecture: x64 - name: Build with Maven diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0aa72794f..3e1ae1875 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,10 +16,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: + distribution: 'zulu' java-version: '11' architecture: x64 server-id: ossrh @@ -29,8 +30,8 @@ jobs: server-password: MAVEN_CENTRAL_TOKEN - name: Setup git profile run: | - git config user.name github-actions - git config user.email github-actions@github.com + 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: diff --git a/RELEASING.md b/RELEASING.md index a3001c9f3..6dd790693 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,6 +2,6 @@ These actions can only be performed by someone with maintainer level access to the repository. -1. Run the [Stage Release on Master Branch](https://github.com/hap-java/HAP-Java/actions/workflows/release.yml) Action +1. Run the [Stage Release on Master Branch](https://github.com/hap-java/HAP-Java/actions/workflows/release.yml) Action with a version such as `2.0.2` 2. After this completes, find the release on the [Releases page](https://github.com/hap-java/HAP-Java/releases) 3. Edit the release to include the changelog details From 17f87f1ff5e1d5a43672950ac4f6aa3a2f935d15 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 7 Oct 2022 12:47:34 -0600 Subject: [PATCH 24/73] 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 25/73] 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 26/73] 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 27/73] [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 28/73] [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 29/73] 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 30/73] 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 31/73] [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 32/73] [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 33/73] 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 34/73] 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 35/73] 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 36/73] 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 37/73] 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 38/73] [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 39/73] [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 40/73] 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 41/73] 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 42/73] 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 43/73] 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 44/73] 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 45/73] 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 46/73] [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 47/73] [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 48/73] 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 49/73] 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 50/73] [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 51/73] [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 52/73] 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 53/73] 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 54/73] 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 55/73] 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 56/73] 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 57/73] [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 58/73] [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 59/73] 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 60/73] 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 61/73] 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 62/73] 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 63/73] 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 64/73] [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 65/73] [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 66/73] 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 67/73] 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 68/73] 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 69/73] 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 70/73] 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 71/73] 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 72/73] 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 73/73] 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