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
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
* Occupancy sensor support [#59](https://github.com/hap-java/HAP-Java/pull/59)
* Leak sensors and valve support added [#52](https://github.com/hap-java/HAP-Java/pull/52)
* Notifications are batched now, when possible [#66](https://github.com/hap-java/HAP-Java/pull/66)
* Linked services support
19 changes: 19 additions & 0 deletions src/main/java/io/github/hapjava/HomekitRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ public void addAccessory(HomekitAccessory accessory) {
addAccessorySkipRangeCheck(accessory);
}

public void addLinkedAccessory(
HomekitAccessory primaryAccessory, HomekitAccessory linkedAccessory) {
if (linkedAccessory.getId() <= 1 && !(linkedAccessory instanceof Bridge)) {
throw new IndexOutOfBoundsException(
"The ID of an linkedAccessory used in a bridge must be greater than 1");
}
addLinkedAccessorySkipRangeCheck(primaryAccessory, linkedAccessory);
}

/**
* Skips the range check. Used by {@link HomekitStandaloneAccessoryServer} as well as {@link
* #addAccessory(HomekitAccessory)};
Expand All @@ -83,6 +92,16 @@ void addAccessorySkipRangeCheck(HomekitAccessory accessory) {
}
}

void addLinkedAccessorySkipRangeCheck(
HomekitAccessory primaryAccessory, HomekitAccessory linkedAccessory) {
this.registry.addLinked(primaryAccessory, linkedAccessory);
logger.info("Added linked accessory " + linkedAccessory.getLabel());
if (started) {
registry.reset();
webHandler.resetConnections();
}
}

/**
* 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
Expand Down
35 changes: 33 additions & 2 deletions src/main/java/io/github/hapjava/impl/HomekitRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import io.github.hapjava.Service;
import io.github.hapjava.characteristics.Characteristic;
import io.github.hapjava.impl.services.AccessoryInformationService;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -15,14 +20,17 @@ public class HomekitRegistry {

private final String label;
private final Map<Integer, HomekitAccessory> accessories;
private final Map<Integer, List<HomekitAccessory>> linkedAccessories;
private final Map<HomekitAccessory, List<Service>> services = new HashMap<>();
private final Map<Service, List<Service>> linkedServices = new HashMap<>();
private final Map<HomekitAccessory, Map<Integer, Characteristic>> characteristics =
new HashMap<>();
private boolean isAllowUnauthenticatedRequests = false;

public HomekitRegistry(String label) {
this.label = label;
this.accessories = new ConcurrentHashMap<>();
this.linkedAccessories = new ConcurrentHashMap<>();
reset();
}

Expand All @@ -35,7 +43,20 @@ public synchronized void reset() {
try {
newServices = new ArrayList<>(2);
newServices.add(new AccessoryInformationService(accessory));
newServices.addAll(accessory.getServices());
Collection<Service> services = accessory.getServices();
newServices.addAll(services);
List<HomekitAccessory> linkedAccessories = this.linkedAccessories.get(accessory.getId());
if (linkedAccessories != null) {
for (HomekitAccessory linkedAccessory : linkedAccessories) {
Collection<Service> linkedServices = linkedAccessory.getServices();
for (Service service : services) {
this.linkedServices
.computeIfAbsent(service, k -> new ArrayList<>())
.addAll(linkedServices);
}
newServices.addAll(linkedServices);
}
}
} catch (Exception e) {
logger.error("Could not instantiate services for accessory " + accessory.getLabel(), e);
services.put(accessory, Collections.emptyList());
Expand Down Expand Up @@ -77,6 +98,12 @@ public void add(HomekitAccessory accessory) {
accessories.put(accessory.getId(), accessory);
}

public void addLinked(HomekitAccessory primaryAccessory, HomekitAccessory linkedAccessory) {
linkedAccessories
.computeIfAbsent(primaryAccessory.getId(), k -> new ArrayList<>())
.add(linkedAccessory);
}

public void remove(HomekitAccessory accessory) {
accessories.remove(accessory.getId());
}
Expand All @@ -88,4 +115,8 @@ public boolean isAllowUnauthenticatedRequests() {
public void setAllowUnauthenticatedRequests(boolean allow) {
this.isAllowUnauthenticatedRequests = allow;
}

public List<Service> getLinkedServices(Service primaryService) {
return linkedServices.getOrDefault(primaryService, Collections.emptyList());
}
}
33 changes: 27 additions & 6 deletions src/main/java/io/github/hapjava/impl/json/AccessoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
Expand All @@ -29,13 +31,23 @@ public AccessoryController(HomekitRegistry registry) {
public HttpResponse listing() throws Exception {
JsonArrayBuilder accessories = Json.createArrayBuilder();

Map<Integer, List<CompletableFuture<JsonObject>>> accessoryServiceFutures = new HashMap<>();
Map<HomekitAccessory, Map<Object, Integer>> idsByAccessory = new HashMap<>();
for (HomekitAccessory accessory : registry.getAccessories()) {
Map<Object, Integer> ids = idsByAccessory.computeIfAbsent(accessory, k -> new HashMap<>());
int iid = 0;
for (Service service : registry.getServices(accessory.getId())) {
ids.put(service, ++iid);
for (Characteristic characteristic : service.getCharacteristics()) {
ids.put(characteristic, ++iid);
}
}
}
Map<Integer, List<CompletableFuture<JsonObject>>> accessoryServiceFutures = new HashMap<>();
for (HomekitAccessory accessory : registry.getAccessories()) {
List<CompletableFuture<JsonObject>> serviceFutures = new ArrayList<>();
Map<Object, Integer> ids = idsByAccessory.get(accessory);
for (Service service : registry.getServices(accessory.getId())) {
serviceFutures.add(toJson(service, iid));
iid += service.getCharacteristics().size() + 1;
serviceFutures.add(toJson(ids, service));
}
accessoryServiceFutures.put(accessory.getId(), serviceFutures);
}
Expand Down Expand Up @@ -64,17 +76,20 @@ public HttpResponse listing() throws Exception {
}
}

private CompletableFuture<JsonObject> toJson(Service service, int interfaceId) throws Exception {
private CompletableFuture<JsonObject> toJson(Map<Object, Integer> ids, Service service)
throws Exception {
String shortType =
service.getType().replaceAll("^0*([0-9a-fA-F]+)-0000-1000-8000-0026BB765291$", "$1");
JsonObjectBuilder builder =
Json.createObjectBuilder().add("iid", ++interfaceId).add("type", shortType);
Json.createObjectBuilder().add("iid", ids.get(service)).add("type", shortType);
List<Characteristic> characteristics = service.getCharacteristics();
Collection<CompletableFuture<JsonObject>> characteristicFutures =
new ArrayList<>(characteristics.size());
for (Characteristic characteristic : characteristics) {
characteristicFutures.add(characteristic.toJson(++interfaceId));
characteristicFutures.add(characteristic.toJson(ids.get(characteristic)));
}
Set<Integer> linkedServiceIds =
registry.getLinkedServices(service).stream().map(ids::get).collect(Collectors.toSet());

return CompletableFuture.allOf(
characteristicFutures.toArray(new CompletableFuture<?>[characteristicFutures.size()]))
Expand All @@ -85,6 +100,12 @@ private CompletableFuture<JsonObject> toJson(Service service, int interfaceId) t
.map(future -> future.join())
.forEach(c -> jsonCharacteristics.add(c));
builder.add("characteristics", jsonCharacteristics);
if (!linkedServiceIds.isEmpty()) {
builder.add("primary", true);
JsonArrayBuilder jsonLinkedServices = Json.createArrayBuilder();
linkedServiceIds.forEach(jsonLinkedServices::add);
builder.add("linked", jsonLinkedServices);
}
return builder.build();
});
}
Expand Down