Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# HAP-Java 2.0.10
* Add Stateful Programmable Switch accessory, service and Programmable Switch Output State characteristic [#161](https://github.com/hap-java/HAP-Java/issues/161)

# 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.github.hapjava.accessories;

import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEnum;
import io.github.hapjava.services.Service;
import io.github.hapjava.services.impl.StatefulProgrammableSwitchService;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;

/**
* The Stateful Programmable Switch accessory: a programmable switch that also keeps an output state
* (0 or 1).
*
* <p>This service is not part of the public HAP specification and the Apple Home app may not
* display it; third-party HomeKit apps can use it.
*/
public interface StatefulProgrammableSwitchAccessory extends HomekitAccessory {

/**
* Retrieves the last states of the switch. Bluetooth device should return the last event, the IP
* device should always return null
*
* @return state of the switch event
*/
CompletableFuture<ProgrammableSwitchEnum> getSwitchEvent();

/**
* Subscribes to changes in switch event, i.e. pressing on the button.
*
* @param callback the function to call when the state changes.
*/
void subscribeSwitchEvent(HomekitCharacteristicChangeCallback callback);

/** Unsubscribes from changes in switch event. */
void unsubscribeSwitchEvent();

/**
* Retrieves the output state of the switch.
*
* @return a future with the output state, 0 or 1
*/
CompletableFuture<Integer> getSwitchOutputState();

/**
* Sets the output state of the switch.
*
* @param state the output state to set, 0 or 1
* @return a future that completes when the change is made
* @throws Exception when the output state cannot be set
*/
CompletableFuture<Void> setSwitchOutputState(int state) throws Exception;

/**
* Subscribes to changes in the output state of the switch.
*
* @param callback the function to call when the state changes.
*/
void subscribeSwitchOutputState(HomekitCharacteristicChangeCallback callback);

/** Unsubscribes from changes in the output state of the switch. */
void unsubscribeSwitchOutputState();

@Override
default Collection<Service> getServices() {
return Collections.singleton(new StatefulProgrammableSwitchService(this));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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;

/**
* This characteristic describes the output state of a stateful programmable switch, e.g. {@link
* io.github.hapjava.accessories.StatefulProgrammableSwitchAccessory}. Valid values are 0 and 1.
*/
public class ProgrammableSwitchOutputStateCharacteristic extends IntegerCharacteristic
implements EventableCharacteristic {
public static final int MIN_VALUE = 0;
public static final int MAX_VALUE = 1;

public ProgrammableSwitchOutputStateCharacteristic(
Supplier<CompletableFuture<Integer>> getter,
ExceptionalConsumer<Integer> setter,
Consumer<HomekitCharacteristicChangeCallback> subscriber,
Runnable unsubscriber) {
super(
"00000074-0000-1000-8000-0026BB765291",
"uint8",
"programmable switch output state",
MIN_VALUE,
MAX_VALUE,
null,
Optional.of(getter),
Optional.of(setter),
Optional.of(subscriber),
Optional.of(unsubscriber));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.hapjava.services.impl;

import io.github.hapjava.accessories.StatefulProgrammableSwitchAccessory;
import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchOutputStateCharacteristic;

/**
* This service describes a stateful programmable switch. It is not part of the public HAP
* specification; the definition follows HAP-NodeJS.
*/
public class StatefulProgrammableSwitchService extends AbstractServiceImpl {

public StatefulProgrammableSwitchService(
ProgrammableSwitchEventCharacteristic switchEvent,
ProgrammableSwitchOutputStateCharacteristic outputState) {
super("00000088-0000-1000-8000-0026BB765291");
addCharacteristic(switchEvent);
addCharacteristic(outputState);
}

public StatefulProgrammableSwitchService(StatefulProgrammableSwitchAccessory accessory) {
this(
new ProgrammableSwitchEventCharacteristic(
accessory::getSwitchEvent,
accessory::subscribeSwitchEvent,
accessory::unsubscribeSwitchEvent),
new ProgrammableSwitchOutputStateCharacteristic(
accessory::getSwitchOutputState,
accessory::setSwitchOutputState,
accessory::subscribeSwitchOutputState,
accessory::unsubscribeSwitchOutputState));
if (accessory instanceof AccessoryWithName) {
addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName));
}
}

public void addOptionalCharacteristic(NameCharacteristic name) {
addCharacteristic(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package io.github.hapjava.services.impl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
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.StatefulProgrammableSwitchAccessory;
import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.characteristics.EventableCharacteristic;
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEnum;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchOutputStateCharacteristic;
import io.github.hapjava.services.Service;
import java.util.Collection;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonString;
import org.junit.Before;
import org.junit.Test;

public class StatefulProgrammableSwitchServiceTest {

private StatefulProgrammableSwitchAccessory accessory;

@Before
public void setup() throws Exception {
accessory = mock(StatefulProgrammableSwitchAccessory.class);
when(accessory.getSwitchEvent()).thenReturn(CompletableFuture.completedFuture(null));
when(accessory.getSwitchOutputState()).thenReturn(CompletableFuture.completedFuture(1));
}

@Test
public void serviceHasStatefulSwitchTypeAndRequiredCharacteristics() {
StatefulProgrammableSwitchService service = new StatefulProgrammableSwitchService(accessory);

assertThat(service.getType()).isEqualTo("00000088-0000-1000-8000-0026BB765291");
assertThat(service.getCharacteristics())
.hasSize(2)
.hasExactlyElementsOfTypes(
ProgrammableSwitchEventCharacteristic.class,
ProgrammableSwitchOutputStateCharacteristic.class);
}

@Test
public void nameIsAddedWhenAccessoryHasName() {
StatefulProgrammableSwitchAccessory namedAccessory =
mock(
StatefulProgrammableSwitchAccessory.class,
withSettings().extraInterfaces(AccessoryWithName.class));

StatefulProgrammableSwitchService service =
new StatefulProgrammableSwitchService(namedAccessory);

assertThat(service.getCharacteristics())
.hasSize(3)
.last()
.isInstanceOf(NameCharacteristic.class);
}

@Test
public void defaultGetServicesReturnsStatefulSwitchService() {
Collection<Service> services = new TestSwitch().getServices();

assertThat(services).hasSize(1);
assertThat(services.iterator().next()).isInstanceOf(StatefulProgrammableSwitchService.class);
}

@Test
public void outputStateAdvertisesUint8WithRangeZeroToOne() {
JsonObject json =
outputState(new StatefulProgrammableSwitchService(accessory)).toJson(9).join();

assertThat(json.getString("type")).isEqualTo("74");
assertThat(json.getString("format")).isEqualTo("uint8");
assertThat(
json.getJsonArray("perms").getValuesAs(JsonString.class).stream()
.map(JsonString::getString)
.collect(Collectors.toList()))
.containsExactly("pr", "pw", "ev");
assertThat(json.getInt("value")).isEqualTo(1);
assertThat(json.getInt("minValue")).isEqualTo(0);
assertThat(json.getInt("maxValue")).isEqualTo(1);
assertThat(json.getInt("minStep")).isEqualTo(1);
assertThat(json.containsKey("unit")).isFalse();
}

@Test
public void writingOutputStateCallsAccessorySetter() throws Exception {
outputState(new StatefulProgrammableSwitchService(accessory))
.setValue(Json.createArrayBuilder().add(1).build().get(0));

verify(accessory).setSwitchOutputState(1);
}

@Test
public void outputStateSubscriptionsAreDelegatedToAccessory() {
EventableCharacteristic outputState =
(EventableCharacteristic) outputState(new StatefulProgrammableSwitchService(accessory));
HomekitCharacteristicChangeCallback callback = () -> {};

outputState.subscribe(callback);
outputState.unsubscribe();

verify(accessory).subscribeSwitchOutputState(callback);
verify(accessory).unsubscribeSwitchOutputState();
}

@Test
public void switchEventStaysReadOnlyAndEventable() {
JsonObject json =
new StatefulProgrammableSwitchService(accessory)
.getCharacteristics()
.get(0)
.toJson(8)
.join();

assertThat(json.getString("type")).isEqualTo("73");
assertThat(json.getJsonArray("perms").toString()).isEqualTo("[\"pr\",\"ev\"]");

((EventableCharacteristic)
new StatefulProgrammableSwitchService(accessory).getCharacteristics().get(0))
.subscribe(() -> {});
verify(accessory).subscribeSwitchEvent(any());
}

private static Characteristic outputState(StatefulProgrammableSwitchService service) {
return service.getCharacteristics().get(1);
}

/** Minimal accessory, so the interface's default getServices() runs for real. */
private static class TestSwitch implements StatefulProgrammableSwitchAccessory {
@Override
public int getId() {
return 2;
}

@Override
public CompletableFuture<String> getName() {
return CompletableFuture.completedFuture("switch");
}

@Override
public void identify() {}

@Override
public CompletableFuture<String> getSerialNumber() {
return CompletableFuture.completedFuture("1");
}

@Override
public CompletableFuture<String> getModel() {
return CompletableFuture.completedFuture("model");
}

@Override
public CompletableFuture<String> getManufacturer() {
return CompletableFuture.completedFuture("maker");
}

@Override
public CompletableFuture<String> getFirmwareRevision() {
return CompletableFuture.completedFuture("1.0");
}

@Override
public CompletableFuture<ProgrammableSwitchEnum> getSwitchEvent() {
return CompletableFuture.completedFuture(null);
}

@Override
public void subscribeSwitchEvent(HomekitCharacteristicChangeCallback callback) {}

@Override
public void unsubscribeSwitchEvent() {}

@Override
public CompletableFuture<Integer> getSwitchOutputState() {
return CompletableFuture.completedFuture(0);
}

@Override
public CompletableFuture<Void> setSwitchOutputState(int state) {
return CompletableFuture.completedFuture(null);
}

@Override
public void subscribeSwitchOutputState(HomekitCharacteristicChangeCallback callback) {}

@Override
public void unsubscribeSwitchOutputState() {}
}
}
Loading