Skip to content

Commit 62077b7

Browse files
author
udaw
committed
Add Stateful Programmable Switch accessory (#161)
Add StatefulProgrammableSwitchAccessory, StatefulProgrammableSwitchService (0x88) and ProgrammableSwitchOutputStateCharacteristic (0x74, uint8, 0..1, pr/pw/ev). The service is not in the public HAP specification; the definitions follow HAP-NodeJS. Purely additive: no existing class changes.
1 parent 7c7678a commit 62077b7

5 files changed

Lines changed: 351 additions & 0 deletions

File tree

‎CHANGES.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# HAP-Java 2.0.10
2+
* Add Stateful Programmable Switch accessory, service and Programmable Switch Output State characteristic [#161](https://github.com/hap-java/HAP-Java/issues/161)
3+
14
# HAP-Java 2.0.9
25
* Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202)
36
* Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.github.hapjava.accessories;
2+
3+
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
4+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEnum;
5+
import io.github.hapjava.services.Service;
6+
import io.github.hapjava.services.impl.StatefulProgrammableSwitchService;
7+
import java.util.Collection;
8+
import java.util.Collections;
9+
import java.util.concurrent.CompletableFuture;
10+
11+
/**
12+
* The Stateful Programmable Switch accessory: a programmable switch that also keeps an output state
13+
* (0 or 1).
14+
*
15+
* <p>This service is not part of the public HAP specification and the Apple Home app may not
16+
* display it; third-party HomeKit apps can use it.
17+
*/
18+
public interface StatefulProgrammableSwitchAccessory extends HomekitAccessory {
19+
20+
/**
21+
* Retrieves the last states of the switch. Bluetooth device should return the last event, the IP
22+
* device should always return null
23+
*
24+
* @return state of the switch event
25+
*/
26+
CompletableFuture<ProgrammableSwitchEnum> getSwitchEvent();
27+
28+
/**
29+
* Subscribes to changes in switch event, i.e. pressing on the button.
30+
*
31+
* @param callback the function to call when the state changes.
32+
*/
33+
void subscribeSwitchEvent(HomekitCharacteristicChangeCallback callback);
34+
35+
/** Unsubscribes from changes in switch event. */
36+
void unsubscribeSwitchEvent();
37+
38+
/**
39+
* Retrieves the output state of the switch.
40+
*
41+
* @return a future with the output state, 0 or 1
42+
*/
43+
CompletableFuture<Integer> getSwitchOutputState();
44+
45+
/**
46+
* Sets the output state of the switch.
47+
*
48+
* @param state the output state to set, 0 or 1
49+
* @return a future that completes when the change is made
50+
* @throws Exception when the output state cannot be set
51+
*/
52+
CompletableFuture<Void> setSwitchOutputState(int state) throws Exception;
53+
54+
/**
55+
* Subscribes to changes in the output state of the switch.
56+
*
57+
* @param callback the function to call when the state changes.
58+
*/
59+
void subscribeSwitchOutputState(HomekitCharacteristicChangeCallback callback);
60+
61+
/** Unsubscribes from changes in the output state of the switch. */
62+
void unsubscribeSwitchOutputState();
63+
64+
@Override
65+
default Collection<Service> getServices() {
66+
return Collections.singleton(new StatefulProgrammableSwitchService(this));
67+
}
68+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.github.hapjava.characteristics.impl.common;
2+
3+
import io.github.hapjava.characteristics.EventableCharacteristic;
4+
import io.github.hapjava.characteristics.ExceptionalConsumer;
5+
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
6+
import io.github.hapjava.characteristics.impl.base.IntegerCharacteristic;
7+
import java.util.Optional;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.function.Consumer;
10+
import java.util.function.Supplier;
11+
12+
/**
13+
* This characteristic describes the output state of a stateful programmable switch, e.g. {@link
14+
* io.github.hapjava.accessories.StatefulProgrammableSwitchAccessory}. Valid values are 0 and 1.
15+
*/
16+
public class ProgrammableSwitchOutputStateCharacteristic extends IntegerCharacteristic
17+
implements EventableCharacteristic {
18+
public static final int MIN_VALUE = 0;
19+
public static final int MAX_VALUE = 1;
20+
21+
public ProgrammableSwitchOutputStateCharacteristic(
22+
Supplier<CompletableFuture<Integer>> getter,
23+
ExceptionalConsumer<Integer> setter,
24+
Consumer<HomekitCharacteristicChangeCallback> subscriber,
25+
Runnable unsubscriber) {
26+
super(
27+
"00000074-0000-1000-8000-0026BB765291",
28+
"uint8",
29+
"programmable switch output state",
30+
MIN_VALUE,
31+
MAX_VALUE,
32+
null,
33+
Optional.of(getter),
34+
Optional.of(setter),
35+
Optional.of(subscriber),
36+
Optional.of(unsubscriber));
37+
}
38+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.github.hapjava.services.impl;
2+
3+
import io.github.hapjava.accessories.StatefulProgrammableSwitchAccessory;
4+
import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName;
5+
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
6+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
7+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchOutputStateCharacteristic;
8+
9+
/**
10+
* This service describes a stateful programmable switch. It is not part of the public HAP
11+
* specification; the definition follows HAP-NodeJS.
12+
*/
13+
public class StatefulProgrammableSwitchService extends AbstractServiceImpl {
14+
15+
public StatefulProgrammableSwitchService(
16+
ProgrammableSwitchEventCharacteristic switchEvent,
17+
ProgrammableSwitchOutputStateCharacteristic outputState) {
18+
super("00000088-0000-1000-8000-0026BB765291");
19+
addCharacteristic(switchEvent);
20+
addCharacteristic(outputState);
21+
}
22+
23+
public StatefulProgrammableSwitchService(StatefulProgrammableSwitchAccessory accessory) {
24+
this(
25+
new ProgrammableSwitchEventCharacteristic(
26+
accessory::getSwitchEvent,
27+
accessory::subscribeSwitchEvent,
28+
accessory::unsubscribeSwitchEvent),
29+
new ProgrammableSwitchOutputStateCharacteristic(
30+
accessory::getSwitchOutputState,
31+
accessory::setSwitchOutputState,
32+
accessory::subscribeSwitchOutputState,
33+
accessory::unsubscribeSwitchOutputState));
34+
if (accessory instanceof AccessoryWithName) {
35+
addOptionalCharacteristic(new NameCharacteristic(((AccessoryWithName) accessory)::getName));
36+
}
37+
}
38+
39+
public void addOptionalCharacteristic(NameCharacteristic name) {
40+
addCharacteristic(name);
41+
}
42+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package io.github.hapjava.services.impl;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.verify;
7+
import static org.mockito.Mockito.when;
8+
import static org.mockito.Mockito.withSettings;
9+
10+
import io.github.hapjava.accessories.StatefulProgrammableSwitchAccessory;
11+
import io.github.hapjava.accessories.optionalcharacteristic.AccessoryWithName;
12+
import io.github.hapjava.characteristics.Characteristic;
13+
import io.github.hapjava.characteristics.EventableCharacteristic;
14+
import io.github.hapjava.characteristics.HomekitCharacteristicChangeCallback;
15+
import io.github.hapjava.characteristics.impl.common.NameCharacteristic;
16+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEnum;
17+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchEventCharacteristic;
18+
import io.github.hapjava.characteristics.impl.common.ProgrammableSwitchOutputStateCharacteristic;
19+
import io.github.hapjava.services.Service;
20+
import java.util.Collection;
21+
import java.util.concurrent.CompletableFuture;
22+
import java.util.stream.Collectors;
23+
import javax.json.Json;
24+
import javax.json.JsonObject;
25+
import javax.json.JsonString;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
public class StatefulProgrammableSwitchServiceTest {
30+
31+
private StatefulProgrammableSwitchAccessory accessory;
32+
33+
@Before
34+
public void setup() throws Exception {
35+
accessory = mock(StatefulProgrammableSwitchAccessory.class);
36+
when(accessory.getSwitchEvent()).thenReturn(CompletableFuture.completedFuture(null));
37+
when(accessory.getSwitchOutputState()).thenReturn(CompletableFuture.completedFuture(1));
38+
}
39+
40+
@Test
41+
public void serviceHasStatefulSwitchTypeAndRequiredCharacteristics() {
42+
StatefulProgrammableSwitchService service = new StatefulProgrammableSwitchService(accessory);
43+
44+
assertThat(service.getType()).isEqualTo("00000088-0000-1000-8000-0026BB765291");
45+
assertThat(service.getCharacteristics())
46+
.hasSize(2)
47+
.hasExactlyElementsOfTypes(
48+
ProgrammableSwitchEventCharacteristic.class,
49+
ProgrammableSwitchOutputStateCharacteristic.class);
50+
}
51+
52+
@Test
53+
public void nameIsAddedWhenAccessoryHasName() {
54+
StatefulProgrammableSwitchAccessory namedAccessory =
55+
mock(
56+
StatefulProgrammableSwitchAccessory.class,
57+
withSettings().extraInterfaces(AccessoryWithName.class));
58+
59+
StatefulProgrammableSwitchService service =
60+
new StatefulProgrammableSwitchService(namedAccessory);
61+
62+
assertThat(service.getCharacteristics())
63+
.hasSize(3)
64+
.last()
65+
.isInstanceOf(NameCharacteristic.class);
66+
}
67+
68+
@Test
69+
public void defaultGetServicesReturnsStatefulSwitchService() {
70+
Collection<Service> services = new TestSwitch().getServices();
71+
72+
assertThat(services).hasSize(1);
73+
assertThat(services.iterator().next()).isInstanceOf(StatefulProgrammableSwitchService.class);
74+
}
75+
76+
@Test
77+
public void outputStateAdvertisesUint8WithRangeZeroToOne() {
78+
JsonObject json =
79+
outputState(new StatefulProgrammableSwitchService(accessory)).toJson(9).join();
80+
81+
assertThat(json.getString("type")).isEqualTo("74");
82+
assertThat(json.getString("format")).isEqualTo("uint8");
83+
assertThat(
84+
json.getJsonArray("perms").getValuesAs(JsonString.class).stream()
85+
.map(JsonString::getString)
86+
.collect(Collectors.toList()))
87+
.containsExactly("pr", "pw", "ev");
88+
assertThat(json.getInt("value")).isEqualTo(1);
89+
assertThat(json.getInt("minValue")).isEqualTo(0);
90+
assertThat(json.getInt("maxValue")).isEqualTo(1);
91+
assertThat(json.getInt("minStep")).isEqualTo(1);
92+
assertThat(json.containsKey("unit")).isFalse();
93+
}
94+
95+
@Test
96+
public void writingOutputStateCallsAccessorySetter() throws Exception {
97+
outputState(new StatefulProgrammableSwitchService(accessory))
98+
.setValue(Json.createArrayBuilder().add(1).build().get(0));
99+
100+
verify(accessory).setSwitchOutputState(1);
101+
}
102+
103+
@Test
104+
public void outputStateSubscriptionsAreDelegatedToAccessory() {
105+
EventableCharacteristic outputState =
106+
(EventableCharacteristic) outputState(new StatefulProgrammableSwitchService(accessory));
107+
HomekitCharacteristicChangeCallback callback = () -> {};
108+
109+
outputState.subscribe(callback);
110+
outputState.unsubscribe();
111+
112+
verify(accessory).subscribeSwitchOutputState(callback);
113+
verify(accessory).unsubscribeSwitchOutputState();
114+
}
115+
116+
@Test
117+
public void switchEventStaysReadOnlyAndEventable() {
118+
JsonObject json =
119+
new StatefulProgrammableSwitchService(accessory)
120+
.getCharacteristics()
121+
.get(0)
122+
.toJson(8)
123+
.join();
124+
125+
assertThat(json.getString("type")).isEqualTo("73");
126+
assertThat(json.getJsonArray("perms").toString()).isEqualTo("[\"pr\",\"ev\"]");
127+
128+
((EventableCharacteristic)
129+
new StatefulProgrammableSwitchService(accessory).getCharacteristics().get(0))
130+
.subscribe(() -> {});
131+
verify(accessory).subscribeSwitchEvent(any());
132+
}
133+
134+
private static Characteristic outputState(StatefulProgrammableSwitchService service) {
135+
return service.getCharacteristics().get(1);
136+
}
137+
138+
/** Minimal accessory, so the interface's default getServices() runs for real. */
139+
private static class TestSwitch implements StatefulProgrammableSwitchAccessory {
140+
@Override
141+
public int getId() {
142+
return 2;
143+
}
144+
145+
@Override
146+
public CompletableFuture<String> getName() {
147+
return CompletableFuture.completedFuture("switch");
148+
}
149+
150+
@Override
151+
public void identify() {}
152+
153+
@Override
154+
public CompletableFuture<String> getSerialNumber() {
155+
return CompletableFuture.completedFuture("1");
156+
}
157+
158+
@Override
159+
public CompletableFuture<String> getModel() {
160+
return CompletableFuture.completedFuture("model");
161+
}
162+
163+
@Override
164+
public CompletableFuture<String> getManufacturer() {
165+
return CompletableFuture.completedFuture("maker");
166+
}
167+
168+
@Override
169+
public CompletableFuture<String> getFirmwareRevision() {
170+
return CompletableFuture.completedFuture("1.0");
171+
}
172+
173+
@Override
174+
public CompletableFuture<ProgrammableSwitchEnum> getSwitchEvent() {
175+
return CompletableFuture.completedFuture(null);
176+
}
177+
178+
@Override
179+
public void subscribeSwitchEvent(HomekitCharacteristicChangeCallback callback) {}
180+
181+
@Override
182+
public void unsubscribeSwitchEvent() {}
183+
184+
@Override
185+
public CompletableFuture<Integer> getSwitchOutputState() {
186+
return CompletableFuture.completedFuture(0);
187+
}
188+
189+
@Override
190+
public CompletableFuture<Void> setSwitchOutputState(int state) {
191+
return CompletableFuture.completedFuture(null);
192+
}
193+
194+
@Override
195+
public void subscribeSwitchOutputState(HomekitCharacteristicChangeCallback callback) {}
196+
197+
@Override
198+
public void unsubscribeSwitchOutputState() {}
199+
}
200+
}

0 commit comments

Comments
 (0)