Skip to content

Commit

Permalink
Use JAX-RS for hue emulation
Browse files Browse the repository at this point in the history
* Split RESTapi class into logical units (ConfigurationAccess, UserManagement, LightsAndGroups)
* rules support
* scenes support
* schedule support
* Refactor tests: Use jersey JAX-RS server to test without requiring the framework (non OSGi tests)

Signed-off-by: David Graeff <[email protected]>

schedules

Signed-off-by: David Gräff <[email protected]>
  • Loading branch information
David Graeff committed Apr 6, 2019
1 parent 6d5239c commit 11ec316
Show file tree
Hide file tree
Showing 86 changed files with 8,089 additions and 2,364 deletions.
66 changes: 47 additions & 19 deletions bundles/org.openhab.io.hueemulation/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
# openHAB Hue Emulation Service

Hue Emulation exposes openHAB items as Hue devices to other Hue HTTP API compatible applications like an Amazon Echo, Google Home or
Hue Emulation exposes openHAB items as Hue lights to other Hue API compatible applications like Amazon Echos, Google Homes or
any Hue compatible application.

Because Amazon Echo and Google Home control openHAB locally this way, it is a fast and reliable way
to voice control your installation. See the Troubleshoot section down below though.

This service is independent of the also available Hue binding!

Currently the following Hue functionality is supported:

* Lights: Maps to items
* Groups: Maps to group items
* Rooms: Maps to group items with a specific tag
* Scenes: Maps to rules (new rule engine) that are tagged with "scene"
* Rules: Maps to rules (new rule engine) that are tagged with "huerule"
* Schedule: Maps to rules (new rule engine) that are tagged with "hueschedule"

You can create / modify and remove groups, rooms, scenes, rules and schedules from within Hue compatible devices and apps.

## Discovery:

As soon as the binding is enabled, it will announce the presence of an (emulated) HUE bridge of the second generation (square bridge).
As soon as the service is enabled, it will announce the presence of an (emulated) HUE bridge of the second generation (square bridge).
Hue bridges are using the Universal Plug and Play (UPnP) protocol for discovery.

Like the real HUE bridge the service must be put into pairing mode before other applications can access it.
By default the pairing mode disables itself after 1 minute (can be configured).

## Exposed devices
## Exposed lights

It is important to note that you are exposing *Items* not *Things* or *Channels*.
Only Color, Dimmer and Switch type *Items* are supported.
Only Color, Dimmer, Rollershutter and Switch type *Items* are supported.

This service can emulate 3 different devices:

Expand All @@ -31,9 +44,9 @@ The exposed Hue-type depends on some criteria:
* If the item has the category "Light": It will be exposed as a switch.

This initial type determination is overridden if the item is tagged.
Tags can be configured in Paper UI, please refer to the next section.

The following default tags are setup:

* "Switchable": Item will be exposed as an OSRAM SMART+ Plug
* "Lighting": Item will be exposed as a dimmable white bulb
* "ColorLighting": Item will be exposed as a color bulb
Expand All @@ -45,13 +58,14 @@ You can tag items manually though as well.

## Exposed names

Your items labels are used for exposing! The default naming schema in Paper UI
for automatically linked items unfortunately names *Items* like their Channel names,
Your items labels are used for exposing!
The default naming schema for automatically linked items unfortunately names *Items* like their Channel names,
so usually "Brightness" or "Color". You want to rename those.

## Configuration:

All options are available in Paper UI.
All options are available in the graphical interface.
For textual configuration find the following keys to set.

Pairing can be turned on and off:

Expand Down Expand Up @@ -84,15 +98,28 @@ Usually you do not want to set this option, but change the primary address confi
org.openhab.hueemulation:discoveryIp=192.168.1.100
```

The hue emulation service supports three types of emulated bulbs. You need to tell the service
which item tag corresponds to which emulated bulb type.
One of the comma separated tags must match for the item to be exposed.
Can be empty to match an item based on the other criteria.
Can be empty to match an item based on other criterias.

```
org.openhab.hueemulation:restrictToTagsSwitches=Switchable
org.openhab.hueemulation:restrictToTagsWhiteLights=Lighting
org.openhab.hueemulation:restrictToTagsColorLights=ColorLighting
```

The above default assignment means that every item that has the tag "Switchable"
will be emulated as a Zigbee Switch. If You want your Zigbee Switches to be exposed
as lights instead (because your Amazon Echo does not support switches for example),
you want to have (this can be set in the graphical interface as well):

```
org.openhab.hueemulation:restrictToTagsSwitches=NONE
org.openhab.hueemulation:restrictToTagsWhiteLights=Lighting,Switchable
org.openhab.hueemulation:restrictToTagsColorLights=ColorLighting
```

## Troubleshooting

Some devices like the Amazon Echo, Google Home and all Philips devices expect a Hue bridge to
Expand All @@ -104,21 +131,22 @@ You can test if the hue emulation does its job by enabling pairing mode includin

1. Navigate with your browser to "http://your-openhab-ip/description.xml" to check the discovery
response. Check the IP address in there.
2. Navigate with your browser to "http://your-openhab-ip/api/testuser/lights?debug=true"
2. Navigate with your browser to "http://your-openhab-ip/api/testuser/lights"
to check all exposed lights and switches.
You can use the openHAB "REST Doc" extension for better formatting of the result.

## Text configuration example
Depending on the firmware version of your Amazon Echo, it may not support coloured bulbs or switches.
Please assign "ColorLighting" and "Switchable" to the `WhiteLights` type as explained above.

The item label will be used as the Hue device name.
Also note that Amazon Echos are stubborn as. You might need to remove all former recognised devices multiple
times and perform the search via different Echos and also the web or mobile application.

```
Switch TestSwitch "Kitchen Switch" [ "Switchable" ]
Color TestColorBulb "Bathroom" [ "ColorLighting" ]
Dimmer TestDimmer "Hallway" [ "Lighting" ]
```
## Text configuration example

If you have an item with a channel or binding attached the tag needs to be applied before the channel.
The item label will be used as the Hue device name.

```
Switch BLamp1 "Bedroom Lamp 1" <switch> (FirstFloor) [ "Lighting" ] {mqtt=">[mosquitto:cmnd/sonoff-BLamp1/POWER:command:*:default],<[mosquitto:stat/sonoff-BLamp1/POWER:state:default]"}
Switch TestSwitch "Kitchen Switch" [ "Switchable" ] {channel="..."}
Color TestColorBulb "Bathroom" [ "ColorLighting" ] {channel="..."}
Dimmer TestDimmer "Hallway" [ "Lighting" ] {channel="..."}
```
21 changes: 21 additions & 0 deletions bundles/org.openhab.io.hueemulation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,25 @@

<name>openHAB Add-ons :: Bundles :: Hue Emulation Service</name>

<dependencies>
<!-- https://mvnrepository.com/artifact/org.glassfish.grizzly/grizzly-http-server -->
<dependency>
<groupId>org.glassfish.grizzly</groupId>
<artifactId>grizzly-http-server</artifactId>
<version>2.4.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-http</artifactId>
<version>2.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>2.28</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/**
* Copyright (c) 2010-2019 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.io.hueemulation.internal;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.Metadata;
import org.eclipse.smarthome.core.items.MetadataKey;
import org.eclipse.smarthome.core.items.MetadataRegistry;
import org.eclipse.smarthome.core.net.NetworkAddressService;
import org.openhab.io.hueemulation.internal.dto.HueAuthorizedConfig;
import org.openhab.io.hueemulation.internal.dto.HueDataStore;
import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
import org.openhab.io.hueemulation.internal.dto.HueSensorEntry;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

/**
* This component sets up the hue data store and gets the service configuration.
* It also determines the address for the upnp service.
* <p>
* This is a central component and required by all other components and may not
* depend on anything in this bundle.
*
* @author David Graeff - Initial contribution
*/
@Component
@NonNullByDefault
public class ConfigStore {
public static final String METAKEY = "HUEEMU";

private final Logger logger = LoggerFactory.getLogger(ConfigStore.class);

public HueDataStore ds = new HueDataStore();

/**
* This is the main gson instance, to be obtained by all components that operate on the dto data fields
*/
public final Gson gson = new GsonBuilder().registerTypeAdapter(HueLightEntry.class, new HueLightEntry.Serializer())
.registerTypeAdapter(HueSensorEntry.class, new HueSensorEntry.Serializer())
.registerTypeAdapter(HueRuleEntry.Condition.class, new HueRuleEntry.SerializerCondition())
.registerTypeAdapter(HueAuthorizedConfig.class, new HueAuthorizedConfig.Serializer())
.registerTypeAdapter(HueSuccessGeneric.class, new HueSuccessGeneric.Serializer())
.registerTypeAdapter(HueSuccessResponseStateChanged.class, new HueSuccessResponseStateChanged.Serializer())
.registerTypeAdapter(HueGroupEntry.class, new HueGroupEntry.Serializer(this)).create();

@Reference
protected @NonNullByDefault({}) ConfigurationAdmin configAdmin;

@Reference
protected @NonNullByDefault({}) NetworkAddressService networkAddressService;

@Reference
protected @NonNullByDefault({}) MetadataRegistry metadataRegistry;

//// objects, set within activate()
protected @NonNullByDefault({}) InetAddress address;
protected @NonNullByDefault({}) HueEmulationConfig config;

public Set<String> switchFilter = Collections.emptySet();
public Set<String> colorFilter = Collections.emptySet();
public Set<String> whiteFilter = Collections.emptySet();

private int highestAssignedHueID = 1;

ConfigStore() {
}

/**
* For test dependency injection
*
* @param networkAddressService The network address service
* @param configAdmin The configuration admin service
* @param metadataRegistry The metadataRegistry service
*/
public ConfigStore(NetworkAddressService networkAddressService, ConfigurationAdmin configAdmin,
@Nullable MetadataRegistry metadataRegistry) {
this.networkAddressService = networkAddressService;
this.configAdmin = configAdmin;
this.metadataRegistry = metadataRegistry;

}

@Activate
public void activate(Map<String, Object> properties) {
this.config = new Configuration(properties).as(HueEmulationConfig.class);

if (config.discoveryHttpPort == 0) {
config.discoveryHttpPort = Integer.getInteger("org.osgi.service.http.port", 8080);
}

String discoveryIp = config.discoveryIp;
if (discoveryIp == null) {
discoveryIp = networkAddressService.getPrimaryIpv4HostAddress();
}

if (discoveryIp == null) {
logger.warn("No primary IP address configured. Discovery disabled!");
return;
}

try {
address = InetAddress.getByName(discoveryIp);
} catch (UnknownHostException e) {
logger.warn("No primary IP address configured. Discovery disabled!", e);
address = InetAddress.getLoopbackAddress();
return;
}

// Get and apply configurations
ds.config.linkbutton = config.pairingEnabled;
ds.config.createNewUserOnEveryEndpoint = config.createNewUserOnEveryEndpoint;
ds.config.networkopenduration = config.pairingTimeout;
ds.config.mac = NetworkUtils.getMAC(address);
ds.config.ipaddress = address.getHostAddress();
ds.config.devicename = config.devicename;
if (config.uuid.isEmpty()) {
config.uuid = UUID.randomUUID().toString();
org.osgi.service.cm.Configuration configuration;
try {
configuration = configAdmin.getConfiguration(HueEmulationService.CONFIG_PID);
Dictionary<String, Object> dictionary = configuration.getProperties();
dictionary.put(HueEmulationConfig.CONFIG_UUID, config.uuid);
configuration.update(dictionary); // This will restart the service (and call activate again)
return;
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}

ds.config.uuid = config.uuid;
ds.config.bridgeid = config.uuid.replace("-", "").toUpperCase();
if (ds.config.bridgeid.length() > 12) {
ds.config.bridgeid = ds.config.bridgeid.substring(0, 12);
}

determineHighestAssignedHueID();
}

protected void determineHighestAssignedHueID() {
for (Metadata metadata : metadataRegistry.getAll()) {
if (!metadata.getUID().getNamespace().equals(METAKEY)) {
continue;
}
try {
int hueId = Integer.parseInt(metadata.getValue());
if (hueId > highestAssignedHueID) {
highestAssignedHueID = hueId;
}
} catch (NumberFormatException e) {
logger.warn("A non numeric hue ID '{}' was assigned. Ignoring!", metadata.getValue());
}
}
}

/**
* Although hue IDs are strings, a lot of implementations out there assume them to be numbers. Therefore
* we map each item to a number and store that in the meta data provider.
*
* @param item The item to map
* @return A stringified integer number
*/
public String mapItemUIDtoHueID(@Nullable Item item) {
if (item == null) {
throw new IllegalArgumentException();
}

MetadataKey key = new MetadataKey(METAKEY, item.getUID());
Metadata metadata = metadataRegistry.get(key);
int hueId = 0;
if (metadata != null) {
try {
hueId = Integer.parseInt(metadata.getValue());
} catch (NumberFormatException e) {
logger.warn("A non numeric hue ID '{}' was assigned. Ignore and reassign a different id now!",
metadata.getValue());
}
}
if (hueId == 0) {
++highestAssignedHueID;
hueId = highestAssignedHueID;
metadataRegistry.update(new Metadata(key, String.valueOf(hueId), null));
}

return String.valueOf(hueId);
}

public InetAddress getAddress() {
return address;
}

public HueEmulationConfig getConfig() {
return config;
}

public int getHighestAssignedHueID() {
return highestAssignedHueID;
}
}
Loading

0 comments on commit 11ec316

Please sign in to comment.