io.github.zeroone3010.yahueapi.v2.Hue Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of yetanotherhueapi Show documentation
Show all versions of yetanotherhueapi Show documentation
A library for controlling Philips Hue lights.
The newest version!
package io.github.zeroone3010.yahueapi.v2;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.launchdarkly.eventsource.ConnectStrategy;
import com.launchdarkly.eventsource.EventSource;
import com.launchdarkly.eventsource.background.BackgroundEventSource;
import io.github.zeroone3010.yahueapi.HueApiException;
import io.github.zeroone3010.yahueapi.HueBridgeConnectionBuilder;
import io.github.zeroone3010.yahueapi.SecureJsonFactory;
import io.github.zeroone3010.yahueapi.v2.domain.BridgeResource;
import io.github.zeroone3010.yahueapi.v2.domain.ButtonResource;
import io.github.zeroone3010.yahueapi.v2.domain.DeviceResource;
import io.github.zeroone3010.yahueapi.v2.domain.GroupResource;
import io.github.zeroone3010.yahueapi.v2.domain.LightResource;
import io.github.zeroone3010.yahueapi.v2.domain.Resource;
import io.github.zeroone3010.yahueapi.v2.domain.ResourceRoot;
import io.github.zeroone3010.yahueapi.v2.domain.ResourceType;
import io.github.zeroone3010.yahueapi.v2.domain.RoomResource;
import io.github.zeroone3010.yahueapi.v2.domain.ZoneResource;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static io.github.zeroone3010.yahueapi.v2.domain.ResourceType.MOTION;
import static io.github.zeroone3010.yahueapi.v2.domain.ResourceType.TEMPERATURE;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
public class Hue {
private static final Logger logger = LoggerFactory.getLogger("io.github.zeroone3010.yahueapi");
private static final int EXPECTED_NEW_LIGHTS_SEARCH_TIME_IN_SECONDS = 50;
public static final String HUE_APPLICATION_KEY_HEADER = "hue-application-key";
public static final long EVENTS_CONNECTION_TIMEOUT_MINUTES = 1L;
public static final Duration EVENTS_READ_TIMEOUT = Duration.ofMillis(Integer.MAX_VALUE);
final ObjectMapper objectMapper;
private final LightFactory lightFactory;
private final SwitchFactory switchFactory;
private final GroupFactory groupFactory;
private final MotionSensorFactory motionSensorFactory;
private final TemperatureSensorFactory temperatureSensorFactory;
private final URL resourceUrl;
private final URL eventUrl;
private final String apiKey;
private Map allResources;
private Map lights;
private Map switches;
private Map groups;
private Map motionSensors;
private Map temperatureSensors;
private final String bridgeIp;
private String bridgeId;
/**
* The basic constructor for initializing the Hue Bridge APIv2 connection for this library.
*
* @param bridgeIp The IP address of the Hue Bridge.
* @param apiKey The API key of your application.
* @since 3.0.0
*/
public Hue(final String bridgeIp, final String apiKey) {
this.bridgeIp = bridgeIp;
try {
this.resourceUrl = new URL("https://" + this.bridgeIp + "/clip/v2/resource");
} catch (MalformedURLException e) {
throw new HueApiException(e);
}
try {
this.eventUrl = new URL("https://" + this.bridgeIp + "/eventstream/clip/v2");
} catch (MalformedURLException e) {
throw new HueApiException(e);
}
this.apiKey = apiKey;
this.objectMapper = HttpUtil.buildObjectMapper(this.bridgeIp);
lightFactory = new LightFactory(this, objectMapper);
switchFactory = new SwitchFactory(this, objectMapper);
groupFactory = new GroupFactory(this, objectMapper);
motionSensorFactory = new MotionSensorFactory(this, objectMapper);
temperatureSensorFactory = new TemperatureSensorFactory(this, objectMapper);
refresh();
}
URL getResourceUrl() {
return resourceUrl;
}
/**
* Refreshes the room, lamp, etc. data from the Hue Bridge, in case
* it has been updated since the application was started.
*
* @since 3.0.0
*/
public void refresh() {
try (final InputStream inputStream = getUrlConnection("").getInputStream()) {
final ResourceRoot resourceRoot = objectMapper.readValue(inputStream, ResourceRoot.class);
allResources = resourceRoot.getData().stream().collect(Collectors.toMap(r -> r.getId(), r -> r));
logger.trace("Resource root: " + resourceRoot);
} catch (final IOException e) {
throw new HueApiException(e);
}
lights = allResources.values().stream()
.filter(r -> r instanceof LightResource)
.map(r -> (LightResource) r)
.map(this::buildLight)
.collect(toMap(LightImpl::getId, light -> light));
final List devices = allResources.values().stream()
.filter(r -> r instanceof DeviceResource)
.map(r -> (DeviceResource) r)
.collect(toList());
final List allButtons = allResources.values().stream()
.filter(r -> r instanceof ButtonResource)
.map(r -> (ButtonResource) r)
.collect(toList());
switches = devices.stream()
.map(device -> buildSwitch(device, allButtons))
.filter(Objects::nonNull)
.collect(toMap(Switch::getId, s -> s));
motionSensors = devices.stream()
.filter(device -> device.getServices().stream().anyMatch(service -> MOTION == service.getResourceType()))
.map(device -> buildMotionSensor(device))
.collect(Collectors.toMap(MotionSensor::getId, d -> d));
temperatureSensors = devices.stream()
.filter(device -> device.getServices().stream().anyMatch(service -> TEMPERATURE == service.getResourceType()))
.map(device -> buildTemperatureSensor(device))
.collect(Collectors.toMap(TemperatureSensor::getId, d -> d));
groups = allResources.values().stream()
.filter(r -> r instanceof RoomResource || r instanceof ZoneResource)
.map(r -> (GroupResource) r)
.map(this::buildGroup)
.collect(toMap(GroupImpl::getId, group -> group));
bridgeId = allResources.values().stream()
.filter(r -> r instanceof BridgeResource)
.map(r -> (BridgeResource) r)
.map(bridge -> bridge.getBridgeId())
.findFirst()
.orElse(null);
}
URLConnection getUrlConnection(final String path) {
try {
final URL url = new URL(this.resourceUrl.toString() + path);
return getUrlConnection(url);
} catch (final MalformedURLException e) {
throw new HueApiException(e);
}
}
HttpsURLConnection getUrlConnection(final URL url) {
try {
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
final SecureJsonFactory factory = (SecureJsonFactory) objectMapper.getFactory();
urlConnection.setSSLSocketFactory(factory.getSocketFactory());
urlConnection.setHostnameVerifier(factory.getHostnameVerifier());
urlConnection.setRequestProperty(HUE_APPLICATION_KEY_HEADER, apiKey);
return urlConnection;
} catch (IOException e) {
throw new HueApiException(e);
}
}
private LightImpl buildLight(final LightResource lightResource) {
return lightFactory.buildLight(lightResource, resourceUrl);
}
private GroupImpl buildGroup(final GroupResource groupResource) {
return groupFactory.buildGroup(groupResource, allResources);
}
private Switch buildSwitch(final DeviceResource deviceResource, final List allButtons) {
final Map buttons = Optional.ofNullable(allButtons).orElse(emptyList()).stream()
.collect(Collectors.toMap(ButtonResource::getId, button -> button));
return switchFactory.buildSwitch(deviceResource, buttons);
}
private MotionSensorImpl buildMotionSensor(final DeviceResource device) {
return motionSensorFactory.buildMotionSensor(device, resourceUrl);
}
private TemperatureSensorImpl buildTemperatureSensor(final DeviceResource device) {
return temperatureSensorFactory.buildTemperatureSensor(device, resourceUrl);
}
/**
* Returns all the lights configured into the Bridge.
*
* @return A Map of lights, the keys being their ids.
*/
public Map getLights() {
return lights;
}
/**
* Returns all the switches configured into the Bridge.
* Different kinds of switches include, for example, the Philips Hue dimmer switch and the Philips Hue Tap switch.
*
* @return A Map of switches, the keys being the IDs of the switches.
* @since 3.0.0
*/
public Map getSwitches() {
return switches;
}
/**
* Returns all the motion sensors configured into the Bridge.
*
* @return A Map of motion sensors, the keys being the IDs of the sensors.
* @since 3.0.0
*/
public Map getMotionSensors() {
return motionSensors;
}
/**
* Returns all the temperature sensors configured into the Bridge.
*
* @return A Map of motion sensors, the keys being the IDs of the sensors.
* @since 3.0.0
*/
public Map getTemperatureSensors() {
return temperatureSensors;
}
Resource getResource(final UUID uuid) {
return allResources.get(uuid);
}
List getDevices() {
return allResources.values().stream()
.filter(resource -> resource.getType() == ResourceType.DEVICE)
.map(resource -> (DeviceResource) resource)
.collect(toList());
}
/**
* Returns all the rooms configured into the Bridge.
*
* @return A Map of rooms, the keys being the IDs of the rooms.
* @since 3.0.0
*/
public Map getRooms() {
return groups.values().stream()
.filter(r -> r.getType() == ResourceType.ROOM)
.collect(toMap(Group::getId, r -> r));
}
/**
* Returns all the zones configured into the Bridge.
*
* @return A Map of zones, the keys being the IDs of the zones.
* @since 3.0.0
*/
public Map getZones() {
return groups.values().stream()
.filter(r -> r.getType() == ResourceType.ZONE)
.collect(toMap(Group::getId, r -> r));
}
/**
* Returns a specific room by its name.
*
* @param roomName The name of a room
* @return A room or {@code Optional.empty()} if a room with the given name does not exist.
* @since 3.0.0
*/
public Optional getRoomByName(final String roomName) {
return getRooms().values().stream().filter(group -> Objects.equals(group.getName(), roomName)).findFirst();
}
/**
* Returns a specific zone by its name.
*
* @param zoneName The name of a zone
* @return A zone or {@code Optional.empty()} if a zone with the given name does not exist.
* @since 3.0.0
*/
public Optional getZoneByName(final String zoneName) {
return getZones().values().stream().filter(group -> Objects.equals(group.getName(), zoneName)).findFirst();
}
/**
* Returns the technical ID of the Bridge.
*
* @return A String such as "00173321ae25bae8"
* @since 3.0.0
*/
public String getBridgeId() {
return bridgeId;
}
public HueEventSource subscribeToEvents(final HueEventListener eventListener) {
try {
SSLSocketFactory factory;
X509TrustManager trustManager;
JsonFactory jsonFactory = objectMapper.getFactory();
SecureJsonFactory secureJsonFactory = (SecureJsonFactory) jsonFactory;
factory = secureJsonFactory.getSocketFactory();
trustManager = secureJsonFactory.getTrustManager();
final OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(factory, trustManager)
.connectTimeout(Duration.ofMinutes(EVENTS_CONNECTION_TIMEOUT_MINUTES))
.readTimeout(EVENTS_READ_TIMEOUT)
.hostnameVerifier(secureJsonFactory.getHostnameVerifier())
.build();
final BasicHueEventHandler eventHandler = new BasicHueEventHandler(this, eventListener);
final BackgroundEventSource.Builder builder = new BackgroundEventSource.Builder(eventHandler,
new EventSource.Builder(ConnectStrategy.http(eventUrl.toURI())
.httpClient(client)
.headers(Headers.of(HUE_APPLICATION_KEY_HEADER, apiKey))
.connectTimeout(5000, TimeUnit.MILLISECONDS)
).retryDelay(3000, TimeUnit.MILLISECONDS)
);
final BackgroundEventSource eventSource = builder.build();
eventSource.start();
return new LaunchDarklyEventSource(eventSource);
} catch (final Exception e) {
throw new HueApiException(e);
}
}
/**
* Orders the Bridge to search for new lights. The operation takes some 40-60 seconds -- longer, if there are many
* new lights found. Returns a {@code Future} that is resolved with a collection of new lights found, if any.
*
* @return A {@code Future} that is resolved in some 40-60 seconds with the new lights that have been found, if any.
*/
public Future> searchForNewLights() {
try {
final String searchStartResult = HttpUtil.post(new URL("https://" + bridgeIp + "/api/" + apiKey + "/lights"), "", null);
logger.info("Starting to search for new lights: " + searchStartResult);
final Supplier> newLightsSupplier = () -> {
NewLightsResult newLightsResult = getNewLightsSearchStatus();
int seconds = EXPECTED_NEW_LIGHTS_SEARCH_TIME_IN_SECONDS;
while (newLightsResult.getStatus() != NewLightsSearchStatus.COMPLETED) {
try {
TimeUnit.SECONDS.sleep(1L);
logger.info("Searching for new lights. Approximately " + (seconds--) + " seconds left.");
newLightsResult = getNewLightsSearchStatus();
} catch (final InterruptedException e) {
throw new HueApiException("Search for new lights was interrupted unexpectedly");
}
}
return newLightsResult.getNewLights();
};
return CompletableFuture.supplyAsync(newLightsSupplier);
} catch (final Exception e) {
throw new HueApiException("Failed to search for new lights", e);
}
}
/**
* Returns the status of new lights search -- see {@link #searchForNewLights()}. Note that
* you do not need to call this method manually when you are using the {@link #searchForNewLights()}
* method: internally it checks whether the search is finished by calling this exact method.
* This method is provided as public for convenience in case you need to return to the results
* later or need to find out about searches performed by some other means than this library.
*
* @return Status of the last search for new lights, and the new lights, if any.
* @see #searchForNewLights()
*/
public NewLightsResult getNewLightsSearchStatus() {
final JsonNode result;
try {
result = objectMapper.readTree(new URL("https://" + bridgeIp + "/api/" + apiKey + "/lights/new"));
} catch (final IOException e) {
throw new HueApiException(e);
}
final String rawLastScan = result.get("lastscan").textValue();
final NewLightsSearchStatus status;
final ZonedDateTime lastScanTime;
final Collection newLights = new ArrayList<>();
switch (rawLastScan) {
case "active":
status = NewLightsSearchStatus.ACTIVE;
lastScanTime = null;
break;
case "none":
status = NewLightsSearchStatus.NONE;
lastScanTime = null;
break;
default:
status = NewLightsSearchStatus.COMPLETED;
lastScanTime = TimeUtil.stringTimestampToZonedDateTime(rawLastScan);
refresh();
final Iterator fieldNameIterator = result.fieldNames();
while (fieldNameIterator.hasNext()) {
final String lightIdField = fieldNameIterator.next();
if ("lastscan".equals(lightIdField)) {
continue;
}
final Optional light = getLights().values().stream()
.filter(aLight -> Objects.equals(String.format("/lights/%s", lightIdField), ((LightImpl) aLight).getIdV1()))
.findFirst();
if (light.isPresent()) {
newLights.add(light.get());
} else {
logger.warn("New light {} not found. Weird.", lightIdField);
}
}
break;
}
return new NewLightsResult(newLights, status, lastScanTime);
}
/**
* The method to be used if you do not have an API key for your application yet.
* Returns a {@code HueBridgeConnectionBuilder} that initializes the process of
* adding a new application to the Bridge. You can test if you are connecting to
* a Hue Bridge endpoint before initializing the connection.
*
* @param bridgeIp The IP address of the Bridge.
* @return A connection builder that initializes the application for the Bridge.
* @since 3.0.0
*/
public static HueBridgeConnectionBuilder hueBridgeConnectionBuilder(final String bridgeIp) {
return new HueBridgeConnectionBuilder(bridgeIp);
}
}