
io.github.hapjava.server.impl.HomekitRoot Maven / Gradle / Ivy
Show all versions of hap Show documentation
package io.github.hapjava.server.impl;
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;
import io.github.hapjava.server.impl.connections.SubscriptionManager;
import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
import java.io.IOException;
import java.net.InetAddress;
import javax.jmdns.JmDNS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides advertising and handling for HomeKit accessories. This class handles the advertising of
* HomeKit accessories and contains one or more accessories. When implementing a bridge accessory,
* you will interact with this class directly. Instantiate it via {@link
* HomekitServer#createBridge(HomekitAuthInfo, String, String, String, String, String, String)}. For
* single accessories, this is composed by {@link HomekitStandaloneAccessoryServer}.
*
* @author Andy Lintner
*/
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;
private int nestedBatches = 0;
private boolean madeChanges = false;
HomekitRoot(
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, category, webHandler, authInfo, new JmdnsHomekitAdvertiser(host));
}
HomekitRoot(
String label,
int category,
HomekitWebHandler webHandler,
HomekitAuthInfo authInfo,
JmdnsHomekitAdvertiser advertiser)
throws IOException {
this.advertiser = advertiser;
this.webHandler = webHandler;
this.authInfo = authInfo;
this.label = label;
this.category = category;
this.registry = new HomekitRegistry(label, subscriptions);
}
HomekitRoot(
String label,
int category,
HomekitWebHandler webHandler,
JmDNS jmdns,
HomekitAuthInfo authInfo)
throws IOException {
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));
}
/**
* 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();
}
/**
* 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.
*/
public void addAccessory(HomekitAccessory accessory) {
if (accessory.getId() <= 1 && !(accessory instanceof Bridge)) {
throw new IndexOutOfBoundsException(
"The ID of an accessory used in a bridge must be greater than 1");
}
addAccessorySkipRangeCheck(accessory);
}
/**
* Skips the range check. Used by {@link HomekitStandaloneAccessoryServer} as well as {@link
* #addAccessory(HomekitAccessory)};
*
* @param accessory to advertise and handle.
*/
void addAccessorySkipRangeCheck(HomekitAccessory accessory) {
this.registry.add(accessory);
if (logger.isTraceEnabled()) {
accessory.getName().thenAccept(name -> logger.trace("Added accessory {}", name));
}
madeChanges = true;
if (started && nestedBatches == 0) {
registry.reset();
}
}
/**
* Removes an accessory from being handled or advertised by this root.
*
* @param accessory accessory to cease advertising and handling
*/
public void removeAccessory(HomekitAccessory accessory) {
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));
}
}
/**
* Starts advertising and handling the previously added HomeKit accessories. You should try to
* call this after you have used the {@link #addAccessory(HomekitAccessory)} method to add all the
* initial accessories you plan on advertising, as any later additions will cause the HomeKit
* clients to reconnect.
*/
public void start() {
started = true;
madeChanges = false;
registry.reset();
webHandler
.start(
new HomekitClientConnectionFactoryImpl(authInfo, registry, subscriptions, advertiser))
.thenAccept(
port -> {
try {
refreshAuthInfo();
advertiser.advertise(
label,
category,
authInfo.getMac(),
port,
configurationIndex,
authInfo.getSetupId());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
/** Stops advertising and handling the HomeKit accessories. */
public void stop() {
advertiser.stop();
webHandler.stop();
subscriptions.removeAll();
started = false;
}
/**
* Refreshes auth info after it has been changed outside this library
*
* @throws IOException if there is an error in the underlying protocol, such as a TCP error
*/
public void refreshAuthInfo() throws IOException {
advertiser.setDiscoverable(!authInfo.hasUser());
}
/**
* By default, most homekit requests require that the client be paired. Allowing unauthenticated
* requests can be useful for debugging, but should not be used in production.
*
* @param allow whether to allow unauthenticated requests
*/
public void allowUnauthenticatedRequests(boolean allow) {
registry.setAllowUnauthenticatedRequests(allow);
}
/**
* By default, the bridge advertises itself at revision 1. If you make changes to the accessories
* you're including in the bridge after your first call to {@link start()}, you should increment
* this number. The behavior of the client if the configuration index were to decrement is
* undefined, so this implementation will not manage the configuration index by automatically
* incrementing - preserving this state across invocations should be handled externally.
*
* @param revision an integer, greater than or equal to one, indicating the revision of the
* accessory information
* @throws IOException if there is an error in the underlying protocol, such as a TCP error
*/
public void setConfigurationIndex(int revision) throws IOException {
if (revision < 1) {
throw new IllegalArgumentException("revision must be greater than or equal to 1");
}
this.configurationIndex = revision;
if (this.started) {
advertiser.setConfigurationIndex(revision);
}
}
HomekitRegistry getRegistry() {
return registry;
}
}