added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.io.hueemulation/.classpath
Normal file
32
bundles/org.openhab.io.hueemulation/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.io.hueemulation/.project
Normal file
23
bundles/org.openhab.io.hueemulation/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.io.hueemulation</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.io.hueemulation/NOTICE
Normal file
13
bundles/org.openhab.io.hueemulation/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
198
bundles/org.openhab.io.hueemulation/README.md
Normal file
198
bundles/org.openhab.io.hueemulation/README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# openHAB Hue Emulation Service
|
||||
|
||||
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 "hueemulation_rule"
|
||||
* Schedule: Maps to rules (new rule engine) that are tagged with "hueemulation_schedule"
|
||||
|
||||
You can create / modify and remove groups, rooms, scenes, rules and schedules from within Hue compatible devices and apps.
|
||||
|
||||
## Discovery:
|
||||
|
||||
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 lights
|
||||
|
||||
It is important to note that you are exposing *Items* not *Things* or *Channels*.
|
||||
Only Color, Dimmer, Rollershutter, Switch and Group type *Items* are supported.
|
||||
Group type items require the "Huelight" tag to be exposed as devices instead of Groups.
|
||||
|
||||
This service can emulate 3 different devices:
|
||||
|
||||
* An OSRAM SMART+ Plug,
|
||||
* a dimmable white color Philips A19 bulb and
|
||||
* an a Philips Gen 3 LCT010 extended color bulb.
|
||||
|
||||
The exposed Hue-type depends on some criteria:
|
||||
|
||||
* If the item has the category "ColorLight": It will be exposed as a color bulb
|
||||
* 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.
|
||||
|
||||
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
|
||||
|
||||
It is the responsibility of binding developers to categories and default tag their available *Channels*, so that linked Items are automatically exposed with this service.
|
||||
|
||||
You can tag items manually though as well.
|
||||
|
||||
## Exposed 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 the graphical interface and via textual configuration.
|
||||
The following configurations can be adjusted.
|
||||
|
||||
Pairing can be turned on and off:
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:pairingEnabled=false
|
||||
```
|
||||
|
||||
You can define a pairing timeout in seconds.
|
||||
After that timeout, the `pairingEnabled` is automatically set to `false`.
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:pairingTimeout=60
|
||||
```
|
||||
|
||||
Enable the following option in combination with pairing to create a new API key on the fly on every API endpoint, not only via the new-user-create API.
|
||||
|
||||
Necessary for Amazon Echos and other devices where the API key cannot be reset.
|
||||
After a new installation of openHAB or a configuration pruning the old API keys are gone but Amazon Echos will keep trying with their old, invalid keys.
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:createNewUserOnEveryEndpoint=false
|
||||
```
|
||||
|
||||
Some Amazon Echo versions only allow V1 Hue bridges (the round ones, not the square ones) to be discovered.
|
||||
If the following option is enabled in combination with the pairing mode, the service will pretend to be an old Hue bridge.
|
||||
|
||||
This option resets automatically after pairing mode has been switched off by the timeout.
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:temporarilyEmulateV1bridge=false
|
||||
```
|
||||
|
||||
Permanent V1 bridge emulation (no obvious reason to enable that):
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:permanentV1bridge=false
|
||||
```
|
||||
|
||||
The hue emulation service will announce its existence via UPnP on every
|
||||
of the openHAB configured primary addresses (IPv4 and IPv6).
|
||||
|
||||
Usually you do not want to set this option, but change the primary address configuration of openHAB.
|
||||
|
||||
This option allows you to override what addresses are used for the announcement.
|
||||
You can have multiple comma separated entries.
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:discoveryIp=192.168.1.100,::FFFF:A9DB:0D85
|
||||
```
|
||||
|
||||
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 other criteria.
|
||||
|
||||
```
|
||||
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 switches to be exposed as lights instead (because your Amazon Echo does not support switches for example), you want to have:
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:restrictToTagsSwitches=NONE
|
||||
org.openhab.hueemulation:restrictToTagsWhiteLights=Lighting,Switchable
|
||||
org.openhab.hueemulation:restrictToTagsColorLights=ColorLighting
|
||||
```
|
||||
|
||||
The service tries to expose as much items as possible (greedy), based on some criteria as explained in the section above.
|
||||
If you want to exclude items, you need to tag them. Define the tags with the following option:
|
||||
|
||||
```
|
||||
org.openhab.hueemulation:ignoreItemsWithTags=internal
|
||||
```
|
||||
|
||||
The default is to not expose any items that have the "internal" tag assigned.
|
||||
You want this tag for all items that are purely used for rules, as proxy items etc.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Some devices like Amazon Echo, Google Home and all Philips devices (TVs, Apps) expect a Hue bridge to run on port 80.
|
||||
You must either
|
||||
|
||||
* port forward your openHAB installation to port 80,
|
||||
(`iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8080`)
|
||||
* install a reverse proxy on port 80, for example nginx with the following configuration:
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
* or let openHAB run on port 80 (the entire java process requires elevated privileges).
|
||||
|
||||
Please open port 80 tcp and port 1900 udp in your firewall installation.
|
||||
|
||||
You can test if the hue emulation does its job by enabling pairing mode including the option "Amazon Echo device discovery fix".
|
||||
|
||||
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/status" to check the self test report.
|
||||
|
||||
If you use the port forwarding way, the self-test page will not be able to correctly determine if your installation works on port 80.
|
||||
A reverse proxy is recommended.
|
||||
|
||||
Depending on the firmware version of your Amazon Echo, it may not support colored bulbs or switches.
|
||||
Please assign "ColorLighting" and "Switchable" to the `WhiteLights` type as explained above.
|
||||
|
||||
Also note that Amazon Echos are stubborn as.
|
||||
You might need to remove all former recognized devices multiple times and perform the search via different Echos and also the web or mobile application.
|
||||
|
||||
It might help to (temporarly) lower the emulated bridge version in the configuration as described above,
|
||||
|
||||
## Text configuration example
|
||||
|
||||
The item label will be used as the Hue device name. Please be aware that textual defined items are generally a bad idea.
|
||||
In this case renaming items in Hue compatible Apps will fail.
|
||||
|
||||
```
|
||||
Switch TestSwitch "Kitchen Switch" [ "Switchable" ] {channel="..."}
|
||||
Color TestColorBulb "Bathroom" [ "ColorLighting" ] {channel="..."}
|
||||
Dimmer TestDimmer "Hallway" [ "Lighting" ] {channel="..."}
|
||||
```
|
||||
BIN
bundles/org.openhab.io.hueemulation/doc/Philips_Hue_Bridge.jpg
Normal file
BIN
bundles/org.openhab.io.hueemulation/doc/Philips_Hue_Bridge.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
60
bundles/org.openhab.io.hueemulation/pom.xml
Normal file
60
bundles/org.openhab.io.hueemulation/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.io.hueemulation</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: IO :: Hue Emulation Service</name>
|
||||
|
||||
<properties>
|
||||
<bnd.importpackage>org.glassfish.jersey.*;resolution:="optional"</bnd.importpackage>
|
||||
</properties>
|
||||
|
||||
<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.grizzly.osgi</groupId>
|
||||
<artifactId>grizzly-httpservice</artifactId>
|
||||
<version>2.4.4</version>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</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>
|
||||
<dependency>
|
||||
<groupId>org.jupnp</groupId>
|
||||
<artifactId>org.jupnp</artifactId>
|
||||
<version>2.5.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.io.hueemulation-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-misc-hueemulation" description="Hue Emulation" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<feature>openhab-core-automation</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.io.hueemulation/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.glassfish.jersey.servlet.init;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletContainerInitializer;
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
/**
|
||||
* Shut up pax-web, which scans for this initializer class and loudly complains if it can't be found.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class JerseyServletContainerInitializer implements ServletContainerInitializer {
|
||||
@Override
|
||||
public void onStartup(Set<Class<?>> c, ServletContext ctx) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigurableService;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.common.ThreadPoolManager;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.Metadata;
|
||||
import org.openhab.core.items.MetadataKey;
|
||||
import org.openhab.core.items.MetadataRegistry;
|
||||
import org.openhab.core.net.CidrAddress;
|
||||
import org.openhab.core.net.NetUtil;
|
||||
import org.openhab.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.Deactivate;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.event.Event;
|
||||
import org.osgi.service.event.EventAdmin;
|
||||
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 by the given configuration.
|
||||
* <p>
|
||||
* Also manages the pairing timeout. The service is restarted after a pairing timeout, due to the ConfigAdmin
|
||||
* configuration change.
|
||||
* <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(immediate = false, service = { ConfigStore.class }, configurationPid = {
|
||||
HueEmulationService.CONFIG_PID }, property = { "com.eclipsesource.jaxrs.publish=false",
|
||||
ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=io:hueemulation",
|
||||
ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=io",
|
||||
ConfigurableService.SERVICE_PROPERTY_LABEL + "=Hue Emulation" })
|
||||
@NonNullByDefault
|
||||
public class ConfigStore {
|
||||
|
||||
public static final String METAKEY = "HUEEMU";
|
||||
public static final String EVENT_ADDRESS_CHANGED = "ESH_EMU_CONFIG_ADDR_CHANGED";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ConfigStore.class);
|
||||
|
||||
public HueDataStore ds = new HueDataStore();
|
||||
|
||||
protected @NonNullByDefault({}) ScheduledExecutorService scheduler;
|
||||
private @Nullable ScheduledFuture<?> pairingOffFuture;
|
||||
private @Nullable ScheduledFuture<?> writeUUIDFuture;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) EventAdmin eventAdmin;
|
||||
|
||||
//// objects, set within activate()
|
||||
private Set<InetAddress> discoveryIps = Collections.emptySet();
|
||||
protected volatile @NonNullByDefault({}) HueEmulationConfig config;
|
||||
|
||||
public Set<String> switchFilter = Collections.emptySet();
|
||||
public Set<String> colorFilter = Collections.emptySet();
|
||||
public Set<String> whiteFilter = Collections.emptySet();
|
||||
public Set<String> ignoreItemsFilter = Collections.emptySet();
|
||||
|
||||
private int highestAssignedHueID = 1;
|
||||
|
||||
public ConfigStore() {
|
||||
scheduler = ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, ScheduledExecutorService scheduler) {
|
||||
this.networkAddressService = networkAddressService;
|
||||
this.configAdmin = configAdmin;
|
||||
this.metadataRegistry = metadataRegistry;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
@Activate
|
||||
public void activate(Map<String, Object> properties) {
|
||||
this.config = new Configuration(properties).as(HueEmulationConfig.class);
|
||||
|
||||
determineHighestAssignedHueID();
|
||||
|
||||
if (config.uuid.isEmpty()) {
|
||||
config.uuid = UUID.randomUUID().toString();
|
||||
writeUUIDFuture = scheduler.schedule(() -> {
|
||||
logger.info("No unique ID assigned yet. Assigning {} and restarting...", config.uuid);
|
||||
WriteConfig.setUUID(configAdmin, config.uuid);
|
||||
}, 100, TimeUnit.MILLISECONDS);
|
||||
return;
|
||||
} else {
|
||||
modified(properties);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable InetAddress byName(@Nullable String address) {
|
||||
if (address == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
} catch (UnknownHostException e) {
|
||||
logger.warn("Given IP address could not be resolved: {}", address, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Modified
|
||||
public void modified(Map<String, Object> properties) {
|
||||
this.config = new Configuration(properties).as(HueEmulationConfig.class);
|
||||
|
||||
switchFilter = Collections.unmodifiableSet(
|
||||
Stream.of(config.restrictToTagsSwitches.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
|
||||
colorFilter = Collections.unmodifiableSet(
|
||||
Stream.of(config.restrictToTagsColorLights.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
|
||||
whiteFilter = Collections.unmodifiableSet(
|
||||
Stream.of(config.restrictToTagsWhiteLights.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
|
||||
ignoreItemsFilter = Collections.unmodifiableSet(
|
||||
Stream.of(config.ignoreItemsWithTags.split(",")).map(String::trim).collect(Collectors.toSet()));
|
||||
|
||||
// Use either the user configured
|
||||
InetAddress configuredAddress = null;
|
||||
int networkPrefixLength = 24; // Default for most networks: 255.255.255.0
|
||||
|
||||
if (config.discoveryIp != null) {
|
||||
discoveryIps = Collections.unmodifiableSet(Stream.of(config.discoveryIp.split(",")).map(String::trim)
|
||||
.map(this::byName).filter(e -> e != null).collect(Collectors.toSet()));
|
||||
} else {
|
||||
discoveryIps = new LinkedHashSet<>();
|
||||
configuredAddress = byName(networkAddressService.getPrimaryIpv4HostAddress());
|
||||
if (configuredAddress != null) {
|
||||
discoveryIps.add(configuredAddress);
|
||||
}
|
||||
for (CidrAddress a : NetUtil.getAllInterfaceAddresses()) {
|
||||
if (a.getAddress().equals(configuredAddress)) {
|
||||
networkPrefixLength = a.getPrefix();
|
||||
} else {
|
||||
discoveryIps.add(a.getAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discoveryIps.isEmpty()) {
|
||||
try {
|
||||
logger.info("No discovery ip specified. Trying to determine the host address");
|
||||
configuredAddress = InetAddress.getLocalHost();
|
||||
} catch (Exception e) {
|
||||
logger.info("Host address cannot be determined. Trying loopback address");
|
||||
configuredAddress = InetAddress.getLoopbackAddress();
|
||||
}
|
||||
} else {
|
||||
configuredAddress = discoveryIps.iterator().next();
|
||||
}
|
||||
|
||||
logger.info("Using discovery ip {}", configuredAddress.getHostAddress());
|
||||
|
||||
// Get and apply configurations
|
||||
ds.config.createNewUserOnEveryEndpoint = config.createNewUserOnEveryEndpoint;
|
||||
ds.config.networkopenduration = config.pairingTimeout;
|
||||
ds.config.devicename = config.devicename;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (config.permanentV1bridge) {
|
||||
ds.config.makeV1bridge();
|
||||
}
|
||||
|
||||
setLinkbutton(config.pairingEnabled, config.createNewUserOnEveryEndpoint, config.temporarilyEmulateV1bridge);
|
||||
ds.config.mac = NetworkUtils.getMAC(configuredAddress);
|
||||
ds.config.ipaddress = getConfiguredHostAddress(configuredAddress);
|
||||
ds.config.netmask = networkPrefixLength < 32 ? NetUtil.networkPrefixLengthToNetmask(networkPrefixLength)
|
||||
: "255.255.255.0";
|
||||
|
||||
if (eventAdmin != null) {
|
||||
eventAdmin.postEvent(new Event(EVENT_ADDRESS_CHANGED, Collections.emptyMap()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getConfiguredHostAddress(InetAddress configuredAddress) {
|
||||
String hostAddress = configuredAddress.getHostAddress();
|
||||
int percentIndex = hostAddress.indexOf("%");
|
||||
if (percentIndex != -1) {
|
||||
return hostAddress.substring(0, percentIndex);
|
||||
} else {
|
||||
return hostAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactive(int reason) {
|
||||
ScheduledFuture<?> future = pairingOffFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
future = writeUUIDFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
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(Item item) {
|
||||
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.add(new Metadata(key, String.valueOf(hueId), null));
|
||||
}
|
||||
|
||||
return String.valueOf(hueId);
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return !discoveryIps.isEmpty();
|
||||
}
|
||||
|
||||
public HueEmulationConfig getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public int getHighestAssignedHueID() {
|
||||
return highestAssignedHueID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the link button state.
|
||||
*
|
||||
* Starts a pairing timeout thread if set to true.
|
||||
* Stops any already running timers.
|
||||
*
|
||||
* @param linkbutton New link button state
|
||||
*/
|
||||
public void setLinkbutton(boolean linkbutton, boolean createUsersOnEveryEndpoint,
|
||||
boolean temporarilyEmulateV1bridge) {
|
||||
ds.config.linkbutton = linkbutton;
|
||||
config.createNewUserOnEveryEndpoint = createUsersOnEveryEndpoint;
|
||||
if (temporarilyEmulateV1bridge) {
|
||||
ds.config.makeV1bridge();
|
||||
} else if (!config.permanentV1bridge) {
|
||||
ds.config.makeV2bridge();
|
||||
}
|
||||
ScheduledFuture<?> future = pairingOffFuture;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
if (!linkbutton) {
|
||||
logger.info("Hue Emulation pairing disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Hue Emulation pairing enabled for {}s", ds.config.networkopenduration);
|
||||
pairingOffFuture = scheduler.schedule(() -> {
|
||||
logger.info("Hue Emulation disable pairing...");
|
||||
if (!config.permanentV1bridge) { // Restore bridge version
|
||||
ds.config.makeV2bridge();
|
||||
}
|
||||
config.createNewUserOnEveryEndpoint = false;
|
||||
config.temporarilyEmulateV1bridge = false;
|
||||
WriteConfig.unsetPairingMode(configAdmin);
|
||||
}, ds.config.networkopenduration * 1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public Set<InetAddress> getDiscoveryIps() {
|
||||
return discoveryIps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
/**
|
||||
* The pure item type is not enough to decide how we expose an item.
|
||||
* We need to consider the assigned tags and category as well. This
|
||||
* computed device type is stored next to the item itself.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public enum DeviceType {
|
||||
SwitchType,
|
||||
WhiteType,
|
||||
WhiteTemperatureType,
|
||||
ColorType
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The configuration for {@link HueEmulationService}.
|
||||
*
|
||||
* @author David Graeff - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueEmulationConfig {
|
||||
public boolean pairingEnabled = false;
|
||||
public static final String CONFIG_PAIRING_ENABLED = "pairingEnabled";
|
||||
|
||||
/**
|
||||
* The Amazon echos have no means to recreate a new api key and they don't care about the 403-forbidden http status
|
||||
* code. If the addon has pruned its api-key list, echos will not be able to discover new devices. Set this option
|
||||
* to just create a new user on the fly.
|
||||
*/
|
||||
public boolean createNewUserOnEveryEndpoint = false;
|
||||
public static final String CONFIG_CREATE_NEW_USER_ON_THE_FLY = "createNewUserOnEveryEndpoint";
|
||||
public boolean temporarilyEmulateV1bridge = false;
|
||||
public static final String CONFIG_EMULATE_V1 = "temporarilyEmulateV1bridge";
|
||||
public boolean permanentV1bridge = false;
|
||||
|
||||
/** Pairing timeout in seconds */
|
||||
public int pairingTimeout = 60;
|
||||
/**
|
||||
* The field discoveryIps was named discoveryIp in the frontend for some time and thus user probably
|
||||
* have it in their local config saved under the non plural version.
|
||||
*/
|
||||
public @Nullable String discoveryIp;
|
||||
public int discoveryHttpPort = 0;
|
||||
/** Comma separated list of tags */
|
||||
public String restrictToTagsSwitches = "Switchable";
|
||||
/** Comma separated list of tags */
|
||||
public String restrictToTagsColorLights = "ColorLighting";
|
||||
/** Comma separated list of tags */
|
||||
public String restrictToTagsWhiteLights = "Lighting";
|
||||
/** Comma separated list of tags */
|
||||
public String ignoreItemsWithTags = "internal";
|
||||
|
||||
public static final String CONFIG_UUID = "uuid";
|
||||
public String uuid = "";
|
||||
public String devicename = "openHAB";
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.util.Dictionary;
|
||||
import java.util.Hashtable;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.ws.rs.ApplicationPath;
|
||||
import javax.ws.rs.HttpMethod;
|
||||
import javax.ws.rs.container.ContainerRequestContext;
|
||||
import javax.ws.rs.container.ContainerRequestFilter;
|
||||
import javax.ws.rs.container.ContainerResponseContext;
|
||||
import javax.ws.rs.container.ContainerResponseFilter;
|
||||
import javax.ws.rs.container.PreMatching;
|
||||
import javax.ws.rs.core.Application;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.glassfish.jersey.servlet.ServletProperties;
|
||||
import org.openhab.io.hueemulation.internal.rest.ConfigurationAccess;
|
||||
import org.openhab.io.hueemulation.internal.rest.LightsAndGroups;
|
||||
import org.openhab.io.hueemulation.internal.rest.Rules;
|
||||
import org.openhab.io.hueemulation.internal.rest.Scenes;
|
||||
import org.openhab.io.hueemulation.internal.rest.Schedules;
|
||||
import org.openhab.io.hueemulation.internal.rest.Sensors;
|
||||
import org.openhab.io.hueemulation.internal.rest.StatusResource;
|
||||
import org.openhab.io.hueemulation.internal.rest.UserManagement;
|
||||
import org.openhab.io.hueemulation.internal.upnp.UpnpServer;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicyOption;
|
||||
import org.osgi.service.event.Event;
|
||||
import org.osgi.service.event.EventAdmin;
|
||||
import org.osgi.service.event.EventConstants;
|
||||
import org.osgi.service.event.EventHandler;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides a Hue compatible HTTP REST API on /api.
|
||||
* <p>
|
||||
* References all different rest endpoints implemented as JAX-RS annotated classes in the sub-package "rest".
|
||||
* Those are very modular and have (almost) no inter-dependencies. The UPnP related part is encapsulated in
|
||||
* the also referenced {@link UpnpServer}.
|
||||
* <p>
|
||||
* openHAB items via the {@link org.openhab.core.items.ItemRegistry} are for example mapped to
|
||||
* /api/{username}/lights and /api/{username}/groups in {@link LightsAndGroups}.
|
||||
* <p>
|
||||
* The user management is realized in the {@link UserManagement} component, that is referenced by almost all
|
||||
* other components and is the only inter-component dependency.
|
||||
*
|
||||
* @author David Graeff - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = { HueEmulationService.class }, property = {
|
||||
"com.eclipsesource.jaxrs.publish=false" })
|
||||
public class HueEmulationService implements EventHandler {
|
||||
|
||||
public static final String CONFIG_PID = "org.openhab.hueemulation";
|
||||
public static final String RESTAPI_PATH = "/api";
|
||||
|
||||
@ApplicationPath(RESTAPI_PATH)
|
||||
public static class JerseyApplication extends Application {
|
||||
|
||||
}
|
||||
|
||||
@PreMatching
|
||||
public class RequestInterceptor implements ContainerRequestFilter {
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext) {
|
||||
/**
|
||||
* Jetty returns 415 on any GET request if a client sends the Content-Type header.
|
||||
* This is a workaround - stripping it away in the preMatching stage.
|
||||
*/
|
||||
if (requestContext.getMethod() == HttpMethod.GET
|
||||
&& requestContext.getHeaders().containsKey(HttpHeader.CONTENT_TYPE.asString())) {
|
||||
requestContext.getHeaders().remove(HttpHeader.CONTENT_TYPE.asString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class LogAccessInterceptor implements ContainerResponseFilter {
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
|
||||
if (!logger.isDebugEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("REST request {} {}", requestContext.getMethod(), requestContext.getUriInfo().getPath());
|
||||
logger.debug("REST response: {}", responseContext.getEntity());
|
||||
}
|
||||
}
|
||||
|
||||
private final ContainerRequestFilter requestCleaner = new RequestInterceptor();
|
||||
private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
|
||||
private final LogAccessInterceptor accessInterceptor = new LogAccessInterceptor();
|
||||
|
||||
//// Required services ////
|
||||
// Don't fail the service if the upnp server does not come up
|
||||
// That part is required for discovery only but does not affect already configured hue applications
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
|
||||
protected @Nullable UpnpServer discovery;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigurationAccess configurationAccess;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) LightsAndGroups lightItems;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) Sensors sensors;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) Scenes scenes;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) Schedules schedules;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) Rules rules;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) StatusResource statusResource;
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) HttpService httpService;
|
||||
private @NonNullByDefault({}) ServiceRegistration<?> eventHandler;
|
||||
|
||||
@Activate
|
||||
protected void activate(BundleContext bc) {
|
||||
Dictionary<String, Object> properties = new Hashtable<>();
|
||||
properties.put(EventConstants.EVENT_TOPIC, ConfigStore.EVENT_ADDRESS_CHANGED);
|
||||
eventHandler = bc.registerService(EventHandler.class, this, properties);
|
||||
if (cs.isReady()) {
|
||||
handleEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't restart the service on config change
|
||||
@Modified
|
||||
protected void modified() {
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
try {
|
||||
if (eventHandler != null) {
|
||||
eventHandler.unregister();
|
||||
}
|
||||
} catch (IllegalStateException ignore) {
|
||||
}
|
||||
try {
|
||||
httpService.unregister(RESTAPI_PATH);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We have a hard dependency on the {@link ConfigStore} and that it has initialized the Hue DataStore config
|
||||
* completely. That initialization happens asynchronously and therefore we cannot rely on OSGi activate/modified
|
||||
* state changes. Instead the {@link EventAdmin} is used and we listen for the
|
||||
* {@link ConfigStore#EVENT_ADDRESS_CHANGED} event that is fired as soon as the config is ready.
|
||||
*/
|
||||
@Override
|
||||
public void handleEvent(@Nullable Event event) {
|
||||
try { // Only receive this event once
|
||||
eventHandler.unregister();
|
||||
eventHandler = null;
|
||||
} catch (IllegalStateException ignore) {
|
||||
}
|
||||
|
||||
ResourceConfig resourceConfig = ResourceConfig.forApplicationClass(JerseyApplication.class);
|
||||
resourceConfig.property(ServerProperties.APPLICATION_NAME, "HueEmulation");
|
||||
// don't look for implementations described by META-INF/services/*
|
||||
resourceConfig.property(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true);
|
||||
// disable auto discovery on server, as it's handled via OSGI
|
||||
resourceConfig.property(ServerProperties.FEATURE_AUTO_DISCOVERY_DISABLE, true);
|
||||
|
||||
resourceConfig.property(ServerProperties.PROCESSING_RESPONSE_ERRORS_ENABLED, true);
|
||||
|
||||
resourceConfig.registerInstances(userManagement, configurationAccess, lightItems, sensors, scenes, schedules,
|
||||
rules, statusResource, accessInterceptor, requestCleaner);
|
||||
|
||||
try {
|
||||
Hashtable<String, String> initParams = new Hashtable<>();
|
||||
initParams.put("com.sun.jersey.api.json.POJOMappingFeature", "false");
|
||||
initParams.put(ServletProperties.PROVIDER_WEB_APP, "false");
|
||||
httpService.registerServlet(RESTAPI_PATH, new ServletContainer(resourceConfig), initParams, null);
|
||||
UpnpServer localDiscovery = discovery;
|
||||
if (localDiscovery == null) {
|
||||
logger.warn("The UPnP Server service has not been started!");
|
||||
} else if (!localDiscovery.upnpAnnouncementThreadRunning()) {
|
||||
localDiscovery.handleEvent(null);
|
||||
}
|
||||
statusResource.startUpnpSelfTest();
|
||||
logger.info("Hue Emulation service available under {}", RESTAPI_PATH);
|
||||
} catch (ServletException | NamespaceException e) {
|
||||
logger.warn("Could not start Hue Emulation service: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.ResponseBuilder;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponseSuccessSimple;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Network utility methods
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkUtils {
|
||||
/**
|
||||
* Try to get the ethernet interface MAC for the network interface that belongs to the given IP address.
|
||||
* Returns a default MAC on any failure.
|
||||
*
|
||||
* @param address IP address
|
||||
* @return A MAC of the form "00:00:88:00:bb:ee"
|
||||
*/
|
||||
static String getMAC(InetAddress address) {
|
||||
NetworkInterface networkInterface;
|
||||
final byte[] mac;
|
||||
try {
|
||||
networkInterface = NetworkInterface.getByInetAddress(address);
|
||||
if (networkInterface == null) {
|
||||
return "00:00:88:00:bb:ee";
|
||||
}
|
||||
mac = networkInterface.getHardwareAddress();
|
||||
if (mac == null) {
|
||||
return "00:00:88:00:bb:ee";
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
return "00:00:88:00:bb:ee";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
sb.append(String.format("%02X%s", mac[i], (i < mac.length - 1) ? ":" : ""));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds cors headers to the given response and returns it.
|
||||
*/
|
||||
public static ResponseBuilder ResponseWithCors(ResponseBuilder response) {
|
||||
return response.encoding(StandardCharsets.UTF_8.name()) //
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Headers", "origin, content-type, accept, authorization")
|
||||
.header("Access-Control-Allow-Credentials", "true")
|
||||
.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD")
|
||||
.header("Access-Control-Max-Age", "1209600");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a json response with the correct Hue error code
|
||||
*
|
||||
* @param gson A gson instance
|
||||
* @param uri The original uri of the request
|
||||
* @param type Any of HueResponse.*
|
||||
* @param message A message
|
||||
* @return
|
||||
*/
|
||||
public static Response singleError(Gson gson, UriInfo uri, int type, String message) {
|
||||
HueResponse e = new HueResponse(new HueErrorMessage(type, uri.getPath().replace("/api", ""), message));
|
||||
String str = gson.toJson(Collections.singleton(e), new TypeToken<List<?>>() {
|
||||
}.getType());
|
||||
int httpCode = 500;
|
||||
switch (type) {
|
||||
case HueResponse.UNAUTHORIZED:
|
||||
httpCode = 403;
|
||||
break;
|
||||
case HueResponse.METHOD_NOT_ALLOWED:
|
||||
httpCode = 405;
|
||||
break;
|
||||
case HueResponse.NOT_AVAILABLE:
|
||||
httpCode = 404;
|
||||
break;
|
||||
case HueResponse.ARGUMENTS_INVALID:
|
||||
case HueResponse.LINK_BUTTON_NOT_PRESSED:
|
||||
httpCode = 200;
|
||||
break;
|
||||
}
|
||||
return Response.status(httpCode).entity(str).build();
|
||||
}
|
||||
|
||||
public static Response singleSuccess(Gson gson, String message, String uriPart) {
|
||||
List<HueResponse> responses = new ArrayList<>();
|
||||
responses.add(new HueResponse(new HueSuccessGeneric(message, uriPart)));
|
||||
return Response.ok(gson.toJson(responses, new TypeToken<List<?>>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
|
||||
public static Response singleSuccess(Gson gson, String message) {
|
||||
List<HueResponseSuccessSimple> responses = new ArrayList<>();
|
||||
responses.add(new HueResponseSuccessSimple(message));
|
||||
return Response.ok(gson.toJson(responses, new TypeToken<List<?>>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
|
||||
public static Response successList(Gson gson, List<HueSuccessGeneric> successList) {
|
||||
List<HueResponse> responses = new ArrayList<>();
|
||||
for (HueSuccessGeneric s : successList) {
|
||||
if (s.isValid()) {
|
||||
responses.add(new HueResponse(s));
|
||||
}
|
||||
}
|
||||
return Response.ok(gson.toJson(responses, new TypeToken<List<?>>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.util.ModuleBuilder;
|
||||
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.changerequest.HueCommand;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Rule utility methods. The Hue scheduler and Hue rules support is based on the automation engine.
|
||||
* This class provides methods to convert between Hue entries and automation rules.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RuleUtils {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RuleUtils.class);
|
||||
public static Random random = new Random(); // public for test mock
|
||||
|
||||
/**
|
||||
* Splits the given base time (pattern "hh:mm[:ss]") on the colons and return the resulting array.
|
||||
* If an upper time is given (same pattern), a random number between base and upper time is chosen.
|
||||
*
|
||||
* @param baseTime A base time with the pattern hh:mm[:ss]
|
||||
* @param upperTime An optional upper time or null or an empty string
|
||||
* @return Time components (hour, minute, seconds).
|
||||
*/
|
||||
public static String[] computeRandomizedDayTime(String baseTime, @Nullable String upperTime) {
|
||||
if (!baseTime.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")) {
|
||||
throw new IllegalStateException("Time pattern incorrect. Must be 'hh:mm[:ss]'. " + baseTime);
|
||||
}
|
||||
|
||||
String randomizedTime[] = baseTime.split(":");
|
||||
|
||||
if (upperTime != null && !upperTime.isEmpty()) {
|
||||
String[] upperTimeParts = upperTime.split(":");
|
||||
if (!upperTime.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")
|
||||
|| randomizedTime.length != upperTimeParts.length) {
|
||||
throw new IllegalStateException("Random Time pattern incorrect. Must be 'hh:mm[:ss]'. " + upperTime);
|
||||
}
|
||||
for (int i = 0; i < randomizedTime.length; ++i) {
|
||||
int n = Integer.parseInt(randomizedTime[i]);
|
||||
int n2 = Integer.parseInt(upperTimeParts[i]);
|
||||
int diff = Math.abs(n2 - n); // Example: 12 and 14 -> diff = 2
|
||||
if (diff > 0) { // diff = rnd [0,3)
|
||||
diff = random.nextInt(diff + 1);
|
||||
}
|
||||
randomizedTime[i] = String.valueOf(n2 > n ? n + diff : n2 + diff);
|
||||
}
|
||||
}
|
||||
|
||||
return randomizedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a hue http address used in schedules and hue rules.
|
||||
*
|
||||
* @param ds A hue datastore to verify that referred lights/groups do exist
|
||||
* @param address Relative hue API address. Example: "/api/<username>/groups/1/action" or
|
||||
* "/api/<username>/lights/1/state"
|
||||
* @throws IllegalStateException Thrown if address is invalid
|
||||
*/
|
||||
@SuppressWarnings({ "unused", "null" })
|
||||
public static void validateHueHttpAddress(HueDataStore ds, String address) throws IllegalStateException {
|
||||
String[] validation = address.split("/");
|
||||
if (validation.length < 6 || !validation[0].isEmpty() || !validation[1].equals("api")) {
|
||||
throw new IllegalStateException("Given address invalid!");
|
||||
}
|
||||
if ("groups".equals(validation[3]) && "action".equals(validation[5])) {
|
||||
HueGroupEntry entry = ds.groups.get(validation[4]);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("Group does not exist: " + validation[4]);
|
||||
}
|
||||
} else if ("lights".equals(validation[3]) && "state".equals(validation[5])) {
|
||||
HueLightEntry entry = ds.lights.get(validation[4]);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("Light does not exist: " + validation[4]);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException("Can only handle groups and lights");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfigHttpAction {
|
||||
public String url = "";
|
||||
public String method = "";
|
||||
public String body = "";
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unused", "null" })
|
||||
public static @Nullable HueCommand httpActionToHueCommand(HueDataStore ds, Action a, @Nullable String ruleName) {
|
||||
ConfigHttpAction config = a.getConfiguration().as(ConfigHttpAction.class);
|
||||
|
||||
// Example: "/api/<username>/groups/1/action" or "/api/<username>/lights/1/state"
|
||||
String[] validation = config.url.split("/");
|
||||
if (validation.length < 6 || !validation[0].isEmpty() || !"api".equals(validation[1])) {
|
||||
LOGGER.warn("Hue Rule '{}': Given address in action {} invalid!", ruleName, a.getLabel());
|
||||
return null;
|
||||
}
|
||||
|
||||
if ("groups".equals(validation[3]) && "action".equals(validation[5])) {
|
||||
HueGroupEntry gentry = ds.groups.get(validation[4]);
|
||||
if (gentry == null) {
|
||||
LOGGER.warn("Hue Rule '{}': Given address in action {} invalid. Group does not exist: {}", ruleName,
|
||||
a.getLabel(), validation[4]);
|
||||
return null;
|
||||
}
|
||||
return new HueCommand(config.url, config.method, config.body);
|
||||
} else if ("lights".equals(validation[3]) && "state".equals(validation[5])) {
|
||||
HueLightEntry lentry = ds.lights.get(validation[4]);
|
||||
if (lentry == null) {
|
||||
LOGGER.warn("Hue Rule '{}': Given address in action {} invalid. Light does not exist: {}", ruleName,
|
||||
a.getLabel(), validation[4]);
|
||||
return null;
|
||||
}
|
||||
return new HueCommand(config.url, config.method, config.body);
|
||||
} else {
|
||||
LOGGER.warn("Hue Rule '{}': Given address in action {} invalid. Can only handle lights and groups, not {}",
|
||||
ruleName, a.getLabel(), validation[3]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Action createHttpAction(HueCommand command, String id) {
|
||||
final Configuration actionConfig = new Configuration();
|
||||
actionConfig.put("method", command.method);
|
||||
actionConfig.put("url", command.address);
|
||||
actionConfig.put("body", command.body);
|
||||
return ModuleBuilder.createAction().withId(id).withTypeUID("rules.HttpAction").withConfiguration(actionConfig)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Recurring pattern "W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]"
|
||||
private static Trigger createRecurringFromTimeString(String localtime) {
|
||||
Pattern timePattern = Pattern.compile("W(.*)/T(.*)A(.*)");
|
||||
Matcher m = timePattern.matcher(localtime);
|
||||
if (!m.matches() || m.groupCount() < 3) {
|
||||
throw new IllegalStateException("Recurring time pattern incorrect");
|
||||
}
|
||||
|
||||
String weekdays = m.group(1);
|
||||
String time = m.group(2);
|
||||
String randomize = m.group(3);
|
||||
|
||||
// Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday = 1
|
||||
int weekdaysBinaryEncoded = Integer.valueOf(weekdays);
|
||||
// For the last part of the cron expression ("day"):
|
||||
// A comma separated list of days starting with 0=sunday to 7=sunday
|
||||
List<String> cronWeekdays = new ArrayList<>();
|
||||
for (int bin = 64, c = 1; bin > 0; bin /= 2, c += 1) {
|
||||
if (weekdaysBinaryEncoded / bin == 1) {
|
||||
weekdaysBinaryEncoded = weekdaysBinaryEncoded % bin;
|
||||
cronWeekdays.add(String.valueOf(c));
|
||||
}
|
||||
}
|
||||
String hourMinSec[] = RuleUtils.computeRandomizedDayTime(time, randomize);
|
||||
|
||||
// Cron expression: min hour day month weekdays
|
||||
String cronExpression = hourMinSec[1] + " " + hourMinSec[0] + " * * " + String.join(",", cronWeekdays);
|
||||
|
||||
final Configuration triggerConfig = new Configuration();
|
||||
triggerConfig.put("cronExpression", cronExpression);
|
||||
return ModuleBuilder.createTrigger().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
|
||||
.withConfiguration(triggerConfig).build();
|
||||
}
|
||||
|
||||
// Timer pattern: R[nn]/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]
|
||||
private static Trigger createTimerFromTimeString(String localtime) {
|
||||
Pattern timePattern = Pattern.compile("R(.*)/PT(.*)A(.*)");
|
||||
Matcher m = timePattern.matcher(localtime);
|
||||
if (!m.matches() || m.groupCount() < 3) {
|
||||
throw new IllegalStateException("Timer pattern incorrect");
|
||||
}
|
||||
|
||||
String run = m.group(1);
|
||||
String time = m.group(2);
|
||||
String randomize = m.group(3);
|
||||
|
||||
final Configuration triggerConfig = new Configuration();
|
||||
if (!time.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")) {
|
||||
throw new IllegalStateException("Time pattern incorrect. Must be 'hh:mm[:ss]'. " + time);
|
||||
}
|
||||
triggerConfig.put("time", time);
|
||||
if (randomize.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")) {
|
||||
triggerConfig.put("randomizeTime", randomize);
|
||||
}
|
||||
if (!run.isEmpty()) {
|
||||
if (!run.matches("[0-9]{1,2}")) {
|
||||
throw new IllegalStateException("Run pattern incorrent. Must be a number'. " + run);
|
||||
} else {
|
||||
triggerConfig.put("repeat", run);
|
||||
}
|
||||
} else { // Infinite
|
||||
triggerConfig.put("repeat", "-1");
|
||||
}
|
||||
|
||||
return ModuleBuilder.createTrigger().withId("timertrigger").withTypeUID("timer.TimerTrigger")
|
||||
.withConfiguration(triggerConfig).build();
|
||||
}
|
||||
|
||||
// Absolute date/time pattern "[YYYY]:[MM]:[DD]T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]"
|
||||
private static Trigger createAbsoluteDateTimeFromTimeString(String localtime) {
|
||||
Pattern timePattern = Pattern.compile("(.*)T(.*)A(.*)");
|
||||
Matcher m = timePattern.matcher(localtime);
|
||||
if (!m.matches() || m.groupCount() < 3) {
|
||||
throw new IllegalStateException("Absolute date/time pattern incorrect");
|
||||
}
|
||||
|
||||
String date = m.group(1);
|
||||
String time = m.group(2);
|
||||
if (!date.matches("[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}")) {
|
||||
throw new IllegalStateException("Date pattern incorrect. Must be 'yyyy-mm-dd'. " + date);
|
||||
}
|
||||
if (!time.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")) {
|
||||
throw new IllegalStateException("Time pattern incorrect. Must be 'hh:mm[:ss]'. " + time);
|
||||
}
|
||||
final Configuration triggerConfig = new Configuration();
|
||||
|
||||
triggerConfig.put("date", date);
|
||||
triggerConfig.put("time", time);
|
||||
|
||||
time = m.group(3);
|
||||
if (time.matches("[0-9]{1,2}:[0-9]{1,2}(:[0-9]{1,2})?")) {
|
||||
triggerConfig.put("randomizeTime", time);
|
||||
}
|
||||
|
||||
return ModuleBuilder.createTrigger().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
|
||||
.withConfiguration(triggerConfig).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trigger based on the given time string.
|
||||
* According to <a href="https://developers.meethue.com/develop/hue-api/datatypes-and-time-patterns/">the Hue
|
||||
* documentation</a> this can be:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Absolute time [YYYY]-[MM]-[DD]T[hh]:[mm]:[ss] ([date]T[time])
|
||||
* <li>Randomized time [YYYY]:[MM]:[DD]T[hh]:[mm]:[ss]A[hh]:[mm]:[ss] ([date]T[time]A[time])
|
||||
* <li>Recurring times W[bbb]/T[hh]:[mm]:[ss]
|
||||
* <li>Every day of the week given by bbb at given time
|
||||
* <li>Recurring randomized times W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]
|
||||
* <li>Every weekday given by bbb at given left side time, randomized by right side time. Right side time has to be
|
||||
* smaller than 12 hours
|
||||
* <li>
|
||||
* <ul>
|
||||
* Timers
|
||||
* <li>PT[hh]:[mm]:[ss] Timer, expiring after given time
|
||||
* <li>PT[hh]:[mm]:[ss] Timer, expiring after given time
|
||||
* <li>PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss] Timer with random element
|
||||
* <li>R[nn]/PT[hh]:[mm]:[ss] Recurring timer
|
||||
* <li>R/PT[hh]:[mm]:[ss] Recurring timer
|
||||
* <li>R[nn]/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss] Recurring timer with random element
|
||||
* </ul>
|
||||
* </ul>
|
||||
*
|
||||
* @param localtimeParameter An absolute time or recurring time or timer pattern
|
||||
* @return A trigger based on {@link org.openhab.io.hueemulation.internal.automation.AbsoluteDateTimeTriggerHandler}
|
||||
* or {@link org.openhab.io.hueemulation.internal.automation.TimerTriggerHandler}
|
||||
*/
|
||||
public static Trigger createTriggerForTimeString(final String localtimeParameter) throws IllegalStateException {
|
||||
String localtime = localtimeParameter;
|
||||
Trigger ruleTrigger;
|
||||
|
||||
// Normalize timer patterns
|
||||
if (localtime.startsWith("PT")) {
|
||||
localtime = "R1/" + localtime;
|
||||
}
|
||||
if (!localtime.contains("A")) {
|
||||
localtime += "A";
|
||||
}
|
||||
|
||||
// Recurring pattern "W[bbb]/T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]"
|
||||
if (localtime.startsWith("W")) {
|
||||
ruleTrigger = createRecurringFromTimeString(localtime);
|
||||
} // Timer pattern: R[nn]/PT[hh]:[mm]:[ss]A[hh]:[mm]:[ss]
|
||||
else if (localtime.startsWith("R")) {
|
||||
ruleTrigger = createTimerFromTimeString(localtime);
|
||||
} // Absolute date/time pattern "[YYYY]:[MM]:[DD]T[hh]:[mm]:[ss]A[hh]:[mm]:[ss]"
|
||||
else {
|
||||
ruleTrigger = createAbsoluteDateTimeFromTimeString(localtime);
|
||||
}
|
||||
|
||||
return ruleTrigger;
|
||||
}
|
||||
|
||||
public static class TimerConfig {
|
||||
public String time = "";
|
||||
public String randomizeTime = "";
|
||||
public @Nullable Integer repeat;
|
||||
}
|
||||
|
||||
public static @Nullable String timeStringFromTrigger(List<Trigger> triggers) {
|
||||
Optional<Trigger> trigger;
|
||||
|
||||
trigger = triggers.stream().filter(p -> p.getId().equals("crontrigger")).findFirst();
|
||||
if (trigger.isPresent()) {
|
||||
String cronParts[] = ((String) trigger.get().getConfiguration().get("cronExpression")).split(" ");
|
||||
if (cronParts.length != 5) {
|
||||
LOGGER.warn("Cron trigger has no valid cron expression: {}", String.join(",", cronParts));
|
||||
return null;
|
||||
}
|
||||
// Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday = 1
|
||||
int weekdays = 0;
|
||||
String[] cronWeekdays = cronParts[4].split(",");
|
||||
for (String wdayT : cronWeekdays) {
|
||||
int wday = Integer.parseInt(wdayT);
|
||||
switch (wday) {
|
||||
case 0:
|
||||
case 7:
|
||||
weekdays += 1;
|
||||
break;
|
||||
case 1:
|
||||
weekdays += 64;
|
||||
break;
|
||||
case 2:
|
||||
weekdays += 32;
|
||||
break;
|
||||
case 3:
|
||||
weekdays += 16;
|
||||
break;
|
||||
case 4:
|
||||
weekdays += 8;
|
||||
break;
|
||||
case 5:
|
||||
weekdays += 4;
|
||||
break;
|
||||
case 6:
|
||||
weekdays += 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return String.format("W%d/T%s:%s:00", weekdays, cronParts[1], cronParts[0]);
|
||||
}
|
||||
|
||||
trigger = triggers.stream().filter(p -> p.getId().equals("timertrigger")).findFirst();
|
||||
if (trigger.isPresent()) {
|
||||
TimerConfig c = trigger.get().getConfiguration().as(TimerConfig.class);
|
||||
if (c.repeat == null) {
|
||||
return String.format(c.randomizeTime.isEmpty() ? "PT%s" : "PT%sA%s", c.time, c.randomizeTime);
|
||||
} else if (c.repeat == -1) {
|
||||
return String.format(c.randomizeTime.isEmpty() ? "R/PT%s" : "R/PT%sA%s", c.time, c.randomizeTime);
|
||||
} else {
|
||||
return String.format(c.randomizeTime.isEmpty() ? "R%d/PT%s" : "R%d/PT%sA%s", c.repeat, c.time,
|
||||
c.randomizeTime);
|
||||
}
|
||||
} else {
|
||||
trigger = triggers.stream().filter(p -> p.getId().equals("absolutetrigger")).findFirst();
|
||||
if (trigger.isPresent()) {
|
||||
String date = (String) trigger.get().getConfiguration().get("date");
|
||||
String time = (String) trigger.get().getConfiguration().get("time");
|
||||
String randomizeTime = (String) trigger.get().getConfiguration().get("randomizeTime");
|
||||
return String.format(randomizeTime == null ? "%sT%s" : "%sT%sA%s", date, time, randomizeTime);
|
||||
} else {
|
||||
LOGGER.warn("No recognised trigger type");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.io.hueemulation.internal.dto.AbstractHueState;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStateBulb;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb.ColorMode;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged;
|
||||
|
||||
/**
|
||||
* This utility class provides all kind of functions to convert between openHAB item states to Hue states and back
|
||||
* as well as applying a hue state change request to a hue state or openHAB item state.
|
||||
* <p>
|
||||
* It also provides methods to determine the hue type (plug, white bulb, coloured bulb), given an item.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class StateUtils {
|
||||
|
||||
/**
|
||||
* Compute the hue state from a given item state and a device type.
|
||||
*
|
||||
* @param itemState The item state
|
||||
* @param deviceType The device type
|
||||
* @return A hue light state
|
||||
*/
|
||||
public static AbstractHueState colorStateFromItemState(State itemState, @Nullable DeviceType deviceType) {
|
||||
if (deviceType == null) {
|
||||
return new HueStatePlug(false);
|
||||
}
|
||||
AbstractHueState state;
|
||||
switch (deviceType) {
|
||||
case ColorType:
|
||||
if (itemState instanceof HSBType) {
|
||||
state = new HueStateColorBulb((HSBType) itemState);
|
||||
} else if (itemState instanceof PercentType) {
|
||||
state = new HueStateColorBulb((PercentType) itemState, ((PercentType) itemState).intValue() > 0);
|
||||
} else if (itemState instanceof OnOffType) {
|
||||
OnOffType t = (OnOffType) itemState;
|
||||
state = new HueStateColorBulb(t == OnOffType.ON);
|
||||
} else {
|
||||
state = new HueStateColorBulb(new HSBType());
|
||||
}
|
||||
break;
|
||||
case WhiteType:
|
||||
case WhiteTemperatureType:
|
||||
if (itemState instanceof HSBType) {
|
||||
PercentType brightness = ((HSBType) itemState).getBrightness();
|
||||
state = new HueStateBulb(brightness, brightness.intValue() > 0);
|
||||
} else if (itemState instanceof PercentType) {
|
||||
PercentType brightness = (PercentType) itemState;
|
||||
state = new HueStateBulb(brightness, brightness.intValue() > 0);
|
||||
} else if (itemState instanceof OnOffType) {
|
||||
OnOffType t = (OnOffType) itemState;
|
||||
state = new HueStateBulb(t == OnOffType.ON);
|
||||
} else {
|
||||
state = new HueStateBulb(new PercentType(0), false);
|
||||
}
|
||||
break;
|
||||
case SwitchType:
|
||||
default:
|
||||
if (itemState instanceof OnOffType) {
|
||||
OnOffType t = (OnOffType) itemState;
|
||||
state = new HueStatePlug(t == OnOffType.ON);
|
||||
} else {
|
||||
state = new HueStatePlug(false);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes an openHAB item state, given a hue state.
|
||||
*
|
||||
* <p>
|
||||
* This only proxies to the respective call
|
||||
* on the concrete hue state implementation.
|
||||
*
|
||||
* @throws IllegalStateException Thrown if the concrete hue state is not yet handled by this method.
|
||||
*/
|
||||
public static State itemStateByHueState(AbstractHueState state) throws IllegalStateException {
|
||||
if (state instanceof HueStateColorBulb) {
|
||||
return state.as(HueStateColorBulb.class).toHSBType();
|
||||
} else if (state instanceof HueStateBulb) {
|
||||
return state.as(HueStateBulb.class).toBrightnessType();
|
||||
} else if (state instanceof HueStatePlug) {
|
||||
return state.as(HueStatePlug.class).toOnOffType();
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An openHAB state is usually also a command. Cast the state.
|
||||
*
|
||||
* @throws IllegalStateException Throws if the cast fails.
|
||||
*/
|
||||
public static Command commandByItemState(State state) throws IllegalStateException {
|
||||
if (state instanceof Command) {
|
||||
return (Command) state;
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes an openHAB command, given a hue state change request.
|
||||
*
|
||||
* @param changeRequest The change request
|
||||
*/
|
||||
public static @Nullable Command computeCommandByChangeRequest(HueStateChange changeRequest) {
|
||||
List<HueResponse> responses = new ArrayList<>();
|
||||
return computeCommandByState(responses, "", new HueStateColorBulb(false), changeRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the new received state from the REST PUT request.
|
||||
*
|
||||
* @param responses Creates a response entry for each success and each error. There is one entry per non-null field
|
||||
* of {@link HueStateChange} created.
|
||||
* @param prefix The response entry prefix, for example "/groups/mygroupid/state/"
|
||||
* @param state The current item state
|
||||
* @param newState A state change DTO
|
||||
* @return Return a command computed via the incoming state object.
|
||||
*/
|
||||
public static @Nullable Command computeCommandByState(List<HueResponse> responses, String prefix,
|
||||
AbstractHueState state, HueStateChange newState) {
|
||||
// Apply new state and collect success, error items
|
||||
Map<String, Object> successApplied = new TreeMap<>();
|
||||
List<String> errorApplied = new ArrayList<>();
|
||||
|
||||
Command command = null;
|
||||
if (newState.on != null) {
|
||||
try {
|
||||
state.as(HueStatePlug.class).on = newState.on;
|
||||
command = OnOffType.from(newState.on);
|
||||
successApplied.put("on", newState.on);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("on");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.bri != null) {
|
||||
try {
|
||||
state.as(HueStateBulb.class).bri = newState.bri;
|
||||
command = new PercentType((int) (newState.bri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
|
||||
successApplied.put("bri", newState.bri);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("bri");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.bri_inc != null) {
|
||||
try {
|
||||
int newBri = state.as(HueStateBulb.class).bri + newState.bri_inc;
|
||||
if (newBri < 0 || newBri > HueStateBulb.MAX_BRI) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
command = new PercentType((int) (newBri * 100.0 / HueStateBulb.MAX_BRI + 0.5));
|
||||
successApplied.put("bri", newState.bri);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("bri_inc");
|
||||
} catch (IllegalArgumentException e) {
|
||||
errorApplied.add("bri_inc");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.sat != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
c.sat = newState.sat;
|
||||
c.colormode = ColorMode.hs;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("sat", newState.sat);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("sat");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.sat_inc != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
int newV = c.sat + newState.sat_inc;
|
||||
if (newV < 0 || newV > HueStateColorBulb.MAX_SAT) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
c.colormode = ColorMode.hs;
|
||||
c.sat = newV;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("sat", newState.sat);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("sat_inc");
|
||||
} catch (IllegalArgumentException e) {
|
||||
errorApplied.add("sat_inc");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.hue != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
c.colormode = ColorMode.hs;
|
||||
c.hue = newState.hue;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("hue", newState.hue);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("hue");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.hue_inc != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
int newV = c.hue + newState.hue_inc;
|
||||
if (newV < 0 || newV > HueStateColorBulb.MAX_HUE) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
c.colormode = ColorMode.hs;
|
||||
c.hue = newV;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("hue", newState.hue);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("hue_inc");
|
||||
} catch (IllegalArgumentException e) {
|
||||
errorApplied.add("hue_inc");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.ct != null) {
|
||||
try {
|
||||
// We can't do anything here with a white color temperature.
|
||||
// The core ESH color type does not support setting it.
|
||||
|
||||
// Adjusting the color temperature implies setting the mode to ct
|
||||
if (state instanceof HueStateColorBulb) {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
c.sat = 0;
|
||||
c.colormode = ColorMode.ct;
|
||||
command = c.toHSBType();
|
||||
}
|
||||
successApplied.put("colormode", ColorMode.ct);
|
||||
successApplied.put("sat", 0);
|
||||
successApplied.put("ct", newState.ct);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("ct");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.ct_inc != null) {
|
||||
try {
|
||||
// We can't do anything here with a white color temperature.
|
||||
// The core ESH color type does not support setting it.
|
||||
|
||||
// Adjusting the color temperature implies setting the mode to ct
|
||||
if (state instanceof HueStateColorBulb) {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
if (c.colormode != ColorMode.ct) {
|
||||
c.sat = 0;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("colormode", c.colormode);
|
||||
}
|
||||
}
|
||||
successApplied.put("ct", newState.ct);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("ct_inc");
|
||||
}
|
||||
}
|
||||
|
||||
if (newState.transitiontime != null) {
|
||||
successApplied.put("transitiontime", newState.transitiontime); // Pretend that worked
|
||||
}
|
||||
if (newState.alert != null) {
|
||||
successApplied.put("alert", newState.alert); // Pretend that worked
|
||||
}
|
||||
if (newState.effect != null) {
|
||||
successApplied.put("effect", newState.effect); // Pretend that worked
|
||||
}
|
||||
if (newState.xy != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
c.colormode = ColorMode.xy;
|
||||
c.bri = state.as(HueStateBulb.class).bri;
|
||||
c.xy[0] = newState.xy.get(0);
|
||||
c.xy[1] = newState.xy.get(1);
|
||||
command = c.toHSBType();
|
||||
successApplied.put("xy", newState.xy);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("xy");
|
||||
}
|
||||
}
|
||||
if (newState.xy_inc != null) {
|
||||
try {
|
||||
HueStateColorBulb c = state.as(HueStateColorBulb.class);
|
||||
double newX = c.xy[0] + newState.xy_inc.get(0);
|
||||
double newY = c.xy[1] + newState.xy_inc.get(1);
|
||||
if (newX < 0 || newX > 1 || newY < 0 || newY > 1) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
c.colormode = ColorMode.xy;
|
||||
c.bri = state.as(HueStateBulb.class).bri;
|
||||
c.xy[0] = newX;
|
||||
c.xy[1] = newY;
|
||||
command = c.toHSBType();
|
||||
successApplied.put("xy", newState.xy_inc);
|
||||
} catch (ClassCastException e) {
|
||||
errorApplied.add("xy_inc");
|
||||
} catch (IllegalArgumentException e) {
|
||||
errorApplied.add("xy_inc");
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the response. The response consists of a list with an entry each for all
|
||||
// submitted change requests. If for example "on" and "bri" was send, 2 entries in the response are
|
||||
// expected.
|
||||
successApplied.forEach((t, v) -> {
|
||||
responses.add(new HueResponse(new HueSuccessResponseStateChanged(prefix + "/" + t, v)));
|
||||
});
|
||||
errorApplied.forEach(v -> {
|
||||
responses.add(
|
||||
new HueResponse(new HueErrorMessage(HueResponse.NOT_AVAILABLE, prefix + "/" + v, "Could not set")));
|
||||
});
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
public static @Nullable DeviceType determineTargetType(ConfigStore cs, Item element) {
|
||||
String category = element.getCategory();
|
||||
String type = element.getType();
|
||||
Set<String> tags = element.getTags();
|
||||
|
||||
// Determine type, heuristically
|
||||
DeviceType t = null;
|
||||
|
||||
// The user wants this item to be not exposed
|
||||
if (cs.ignoreItemsFilter.stream().anyMatch(tags::contains)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First consider the category
|
||||
if (category != null) {
|
||||
switch (category) {
|
||||
case "ColorLight":
|
||||
t = DeviceType.ColorType;
|
||||
break;
|
||||
case "Light":
|
||||
t = DeviceType.SwitchType;
|
||||
}
|
||||
}
|
||||
|
||||
// Then the tags
|
||||
if (cs.switchFilter.stream().anyMatch(tags::contains)) {
|
||||
t = DeviceType.SwitchType;
|
||||
}
|
||||
if (cs.whiteFilter.stream().anyMatch(tags::contains)) {
|
||||
t = DeviceType.WhiteTemperatureType;
|
||||
}
|
||||
if (cs.colorFilter.stream().anyMatch(tags::contains)) {
|
||||
t = DeviceType.ColorType;
|
||||
}
|
||||
|
||||
// Last but not least, the item type
|
||||
if (t == null) {
|
||||
switch (type) {
|
||||
case CoreItemFactory.COLOR:
|
||||
if (cs.colorFilter.isEmpty()) {
|
||||
t = DeviceType.ColorType;
|
||||
}
|
||||
break;
|
||||
case CoreItemFactory.DIMMER:
|
||||
case CoreItemFactory.ROLLERSHUTTER:
|
||||
if (cs.whiteFilter.isEmpty()) {
|
||||
t = DeviceType.WhiteTemperatureType;
|
||||
}
|
||||
break;
|
||||
case CoreItemFactory.SWITCH:
|
||||
if (cs.switchFilter.isEmpty()) {
|
||||
t = DeviceType.SwitchType;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the hue state from a given item state and a device type.
|
||||
* If the item state matches the last command. the hue state is adjusted
|
||||
* to use the values from the last hue state change. This is done to prevent
|
||||
* Alexa reporting device errors.
|
||||
*
|
||||
* @param itemState The item state
|
||||
* @param deviceType The device type
|
||||
* @param lastCommand The last command
|
||||
* @param lastHueChange The last hue state change
|
||||
* @return A hue light state
|
||||
*/
|
||||
public static AbstractHueState adjustedColorStateFromItemState(State itemState, @Nullable DeviceType deviceType,
|
||||
@Nullable Command lastCommand, @Nullable HueStateChange lastHueChange) {
|
||||
AbstractHueState hueState = colorStateFromItemState(itemState, deviceType);
|
||||
|
||||
if (lastCommand != null && lastHueChange != null) {
|
||||
if (lastCommand instanceof HSBType) {
|
||||
if (hueState instanceof HueStateColorBulb && itemState.as(HSBType.class).equals(lastCommand)) {
|
||||
HueStateColorBulb c = (HueStateColorBulb) hueState;
|
||||
|
||||
if (lastHueChange.bri != null) {
|
||||
c.bri = lastHueChange.bri;
|
||||
}
|
||||
if (lastHueChange.hue != null) {
|
||||
c.hue = lastHueChange.hue;
|
||||
}
|
||||
if (lastHueChange.sat != null) {
|
||||
c.sat = lastHueChange.sat;
|
||||
}
|
||||
// Although we can't set a colour temperature in OH
|
||||
// this keeps Alexa happy when asking to turn a light
|
||||
// to white.
|
||||
if (lastHueChange.ct != null) {
|
||||
c.ct = lastHueChange.ct;
|
||||
}
|
||||
}
|
||||
} else if (lastCommand instanceof PercentType) {
|
||||
if (hueState instanceof HueStateBulb && itemState.as(PercentType.class).equals(lastCommand)) {
|
||||
if (lastHueChange.bri != null) {
|
||||
((HueStateBulb) hueState).bri = lastHueChange.bri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hueState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.util.Dictionary;
|
||||
import java.util.Hashtable;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.osgi.service.cm.Configuration;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
|
||||
/**
|
||||
* Find all configuration admin interactions in this class
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WriteConfig {
|
||||
public static void setUUID(ConfigurationAdmin configAdmin, String uuid) {
|
||||
try {
|
||||
Configuration configuration = configAdmin.getConfiguration(HueEmulationService.CONFIG_PID, null);
|
||||
Dictionary<String, Object> dictionary = configuration.getProperties();
|
||||
if (dictionary == null) {
|
||||
dictionary = new Hashtable<>();
|
||||
}
|
||||
dictionary.put(HueEmulationConfig.CONFIG_UUID, uuid);
|
||||
configuration.update(dictionary); // This will restart the service (and call activate again)
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the link mode off state
|
||||
*/
|
||||
public static void unsetPairingMode(ConfigurationAdmin configAdmin) {
|
||||
try {
|
||||
org.osgi.service.cm.Configuration configuration = configAdmin
|
||||
.getConfiguration(HueEmulationService.CONFIG_PID, null);
|
||||
Dictionary<String, Object> dictionary = configuration.getProperties();
|
||||
dictionary.put(HueEmulationConfig.CONFIG_PAIRING_ENABLED, false);
|
||||
dictionary.put(HueEmulationConfig.CONFIG_CREATE_NEW_USER_ON_THE_FLY, false);
|
||||
dictionary.put(HueEmulationConfig.CONFIG_EMULATE_V1, false);
|
||||
configuration.update(dictionary);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
|
||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||
import org.openhab.core.scheduler.Scheduler;
|
||||
import org.openhab.core.scheduler.SchedulerRunnable;
|
||||
import org.openhab.core.automation.ModuleHandlerCallback;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
|
||||
import org.openhab.core.automation.handler.TriggerHandlerCallback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Extends the core provided time related module type by an absolute day/time trigger.
|
||||
* <p>
|
||||
* It allows to set a date and a time as separate configuration values (easier to manipulate from
|
||||
* other actions / rules etc) and also allows the user to setup a random factor
|
||||
* (presence simulation).
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class AbsoluteDateTimeTriggerHandler extends BaseTriggerModuleHandler implements SchedulerRunnable {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(AbsoluteDateTimeTriggerHandler.class);
|
||||
|
||||
public static final String MODULE_TYPE_ID = "timer.AbsoluteDateTimeTrigger";
|
||||
public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
|
||||
public static final String MODULE_CONTEXT_NAME = "MODULE";
|
||||
|
||||
public static final String CFG_DATE = "date";
|
||||
public static final String CFG_TIME = "time";
|
||||
public static final String CFG_TIME_RND = "randomizeTime";
|
||||
|
||||
private final Scheduler scheduler;
|
||||
private final Instant dateTime;
|
||||
private ScheduledCompletableFuture<?> schedule;
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final String TIME_FORMAT = "HH:mm:ss";
|
||||
private static final String DATETIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;
|
||||
private final DateTimeFormatter dateTimeformatter;
|
||||
|
||||
public AbsoluteDateTimeTriggerHandler(Trigger module, Scheduler scheduler) {
|
||||
super(module);
|
||||
this.scheduler = scheduler;
|
||||
dateTimeformatter = DateTimeFormatter.ofPattern(DATETIME_FORMAT);
|
||||
|
||||
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
||||
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(TIME_FORMAT);
|
||||
|
||||
// Take optional date into account
|
||||
String cfgDate = (String) module.getConfiguration().get(CFG_DATE);
|
||||
LocalDateTime dateTime = cfgDate == null || cfgDate.isEmpty() ? LocalDate.now().atStartOfDay()
|
||||
: LocalDateTime.from(dateFormatter.parse(cfgDate));
|
||||
|
||||
// Take optional time into account
|
||||
String cfgDTime = (String) module.getConfiguration().get(CFG_TIME);
|
||||
if (cfgDTime != null && !cfgDTime.isEmpty()) {
|
||||
TemporalAccessor temporalAccessor = timeFormatter.parse(cfgDTime);
|
||||
dateTime.plusHours(temporalAccessor.getLong(ChronoField.HOUR_OF_DAY));
|
||||
dateTime.plusMinutes(temporalAccessor.getLong(ChronoField.MINUTE_OF_HOUR));
|
||||
}
|
||||
|
||||
this.dateTime = dateTime.atZone(ZoneId.systemDefault()).toInstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setCallback(ModuleHandlerCallback callback) {
|
||||
super.setCallback(callback);
|
||||
scheduleJob();
|
||||
}
|
||||
|
||||
private void scheduleJob() {
|
||||
schedule = scheduler.at(this, dateTime);
|
||||
logger.debug("Scheduled absolute date/time '{}' for trigger '{}'.", dateTimeformatter.format(dateTime),
|
||||
module.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
super.dispose();
|
||||
if (schedule != null) {
|
||||
schedule.cancel(true);
|
||||
logger.debug("cancelled job for trigger '{}'.", module.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
((TriggerHandlerCallback) callback).triggered(module, null);
|
||||
schedule = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.handler.ActionHandler;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The action module type handled by this class allows to execute a http request (GET, POST, PUT, etc)
|
||||
* on a given address. Relative addresses are supported.
|
||||
* <p>
|
||||
* The optional mimetype and body configuration parameters allow to send arbitrary data with a request.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HttpActionHandler extends BaseModuleHandler<Action> implements ActionHandler {
|
||||
private final Logger logger = LoggerFactory.getLogger(HttpActionHandler.class);
|
||||
|
||||
public static final String MODULE_TYPE_ID = "rules.HttpAction";
|
||||
public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
|
||||
public static final String MODULE_CONTEXT_NAME = "MODULE";
|
||||
|
||||
public static final String CFG_METHOD = "method";
|
||||
public static final String CFG_URL = "url";
|
||||
public static final String CFG_BODY = "body";
|
||||
public static final String CFG_MIMETYPE = "mimetype";
|
||||
public static final String CFG_TIMEOUT = "timeout";
|
||||
|
||||
private static class Config {
|
||||
HttpMethod method = HttpMethod.GET;
|
||||
String url = "";
|
||||
String body = "";
|
||||
String mimetype = "application/json";
|
||||
int timeout = 5;
|
||||
}
|
||||
|
||||
private Config config = new Config();
|
||||
private HttpClient httpClient;
|
||||
|
||||
public HttpActionHandler(final Action module, HttpClientFactory httpFactory) {
|
||||
super(module);
|
||||
|
||||
this.config = module.getConfiguration().as(Config.class);
|
||||
if (config.url.isEmpty()) {
|
||||
throw new IllegalArgumentException("URL not set!");
|
||||
}
|
||||
// convert relative path to absolute one
|
||||
String url = config.url;
|
||||
if (url.startsWith("/")) {
|
||||
config.url = "http://localhost:" + Integer.getInteger("org.osgi.service.http.port", 8080).toString() + url;
|
||||
}
|
||||
|
||||
httpClient = httpFactory.createHttpClient("HttpActionHandler_" + module.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> execute(Map<String, Object> context) {
|
||||
try {
|
||||
Request request = httpClient.newRequest(URI.create(config.url)).method(config.method)
|
||||
.timeout(config.timeout, TimeUnit.SECONDS);
|
||||
if (config.method == HttpMethod.POST || config.method == HttpMethod.PUT) {
|
||||
request.content(new StringContentProvider(config.body), config.mimetype);
|
||||
}
|
||||
request.send();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException e) {
|
||||
logger.warn("Failed to send http request", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.Module;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
|
||||
import org.openhab.core.automation.handler.ModuleHandler;
|
||||
import org.openhab.core.automation.handler.ModuleHandlerFactory;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Triggers, Conditions, Actions specific to the Hue emulation service
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = ModuleHandlerFactory.class)
|
||||
public class HueHandlerFactory extends BaseModuleHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HueHandlerFactory.class);
|
||||
|
||||
private static final Collection<String> TYPES = Arrays
|
||||
.asList(new String[] { HueRuleConditionHandler.MODULE_TYPE_ID });
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore configStore;
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getTypes() {
|
||||
return TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
|
||||
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
|
||||
String moduleTypeUID = module.getTypeUID();
|
||||
if (HueRuleConditionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Condition) {
|
||||
return new HueRuleConditionHandler((Condition) module, configStore.ds);
|
||||
} else {
|
||||
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandler;
|
||||
import org.openhab.core.automation.handler.ConditionHandler;
|
||||
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.HueRuleEntry.Operator;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSensorEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This condition is parameterized with Hue rule condition arguments. A Hue rule works
|
||||
* on the Hue datastore and considers lights / groups / sensors that are available there.
|
||||
* <p>
|
||||
* Implementation details: The predicate function for the condition is computed in the constructor of
|
||||
* this condition. It will only be called and evaluated for isSatisfied.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueRuleConditionHandler extends BaseModuleHandler<Condition> implements ConditionHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HueRuleConditionHandler.class);
|
||||
|
||||
public static final String MODULE_TYPE_ID = "hue.ruleCondition";
|
||||
public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
|
||||
public static final String MODULE_CONTEXT_NAME = "MODULE";
|
||||
|
||||
public static final String CFG_ADDRESS = "address";
|
||||
public static final String CFG_OP = "operator";
|
||||
public static final String CFG_VALUE = "value";
|
||||
|
||||
private static final String TIME_FORMAT = "HH:mm:ss";
|
||||
|
||||
protected final HueRuleEntry.Condition config;
|
||||
protected String itemUID;
|
||||
|
||||
protected Predicate<State> predicate;
|
||||
private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(TIME_FORMAT);
|
||||
private final Pattern timePattern = Pattern.compile("W(.*)/T(.*)/T(.*)");
|
||||
// weekdays range from Monday to Sunday (1-7). The first entry is not used
|
||||
private final boolean weekDaysAllowed[] = { false, false, false, false, false, false, false, false };
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
public HueRuleConditionHandler(Condition module, HueDataStore ds) {
|
||||
super(module);
|
||||
config = module.getConfiguration().as(HueRuleEntry.Condition.class);
|
||||
|
||||
// pattern: "/sensors/2/state/buttonevent" or "/config/localtime"
|
||||
String validation[] = config.address.split("/");
|
||||
String uid = validation[2];
|
||||
|
||||
if ("groups".equals(validation[1]) && "action".equals(validation[3])) {
|
||||
HueGroupEntry entry = ds.groups.get(uid);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("Group does not exist: " + uid);
|
||||
}
|
||||
itemUID = entry.groupItem.getUID();
|
||||
} else if ("lights".equals(validation[1]) && "state".equals(validation[3])) {
|
||||
HueLightEntry entry = ds.lights.get(uid);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("Light does not exist: " + uid);
|
||||
}
|
||||
itemUID = entry.item.getUID();
|
||||
} else if ("sensors".equals(validation[1]) && "state".equals(validation[3])) {
|
||||
HueSensorEntry entry = ds.sensors.get(uid);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("Sensor does not exist: " + uid);
|
||||
}
|
||||
itemUID = entry.item.getUID();
|
||||
} else if ("config".equals(validation[1]) && "localtime".equals(validation[2])) {
|
||||
// Item not used
|
||||
itemUID = "";
|
||||
} else {
|
||||
throw new IllegalStateException("Can only handle groups and lights");
|
||||
}
|
||||
|
||||
if (itemUID == null) {
|
||||
throw new IllegalStateException("Can only handle groups and lights");
|
||||
}
|
||||
|
||||
final String value = config.value;
|
||||
switch (config.operator) {
|
||||
case eq:
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("Equal operator requires a value!");
|
||||
}
|
||||
predicate = state -> {
|
||||
if (state instanceof Number) {
|
||||
return Integer.valueOf(value) == ((Number) state).intValue();
|
||||
} else if (state instanceof OnOffType) {
|
||||
return Boolean.valueOf(value) == (((OnOffType) state) == OnOffType.ON);
|
||||
} else if (state instanceof OpenClosedType) {
|
||||
return Boolean.valueOf(value) == (((OpenClosedType) state) == OpenClosedType.OPEN);
|
||||
}
|
||||
return state.toFullString().equals(value);
|
||||
};
|
||||
break;
|
||||
case gt:
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("GreaterThan operator requires a value!");
|
||||
} else {
|
||||
final Integer integer = Integer.valueOf(value);
|
||||
|
||||
predicate = state -> {
|
||||
if (state instanceof Number) {
|
||||
return integer < ((Number) state).intValue();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
case lt:
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("LowerThan operator requires a value!");
|
||||
} else {
|
||||
final Integer integer = Integer.valueOf(value);
|
||||
|
||||
predicate = state -> {
|
||||
if (state instanceof Number) {
|
||||
return integer > ((Number) state).intValue();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
break;
|
||||
case in:
|
||||
case not_in:
|
||||
if (value == null) {
|
||||
throw new IllegalStateException("InRange operator requires a value!");
|
||||
}
|
||||
|
||||
Matcher m = timePattern.matcher(!value.startsWith("W") ? "W127/" + value : value);
|
||||
if (!m.matches() || m.groupCount() < 3) {
|
||||
throw new IllegalStateException(
|
||||
"Time pattern incorrect for in/not_in hue rule condition: " + value);
|
||||
}
|
||||
|
||||
final LocalTime timeStart = LocalTime.from(timeFormatter.parse(m.group(2)));
|
||||
final LocalTime timeEnd = LocalTime.from(timeFormatter.parse(m.group(3)));
|
||||
|
||||
// Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday = 1
|
||||
int weekdaysBinaryEncoded = Integer.valueOf(m.group(1));
|
||||
List<String> cronWeekdays = new ArrayList<>();
|
||||
for (int bin = 64, c = 1; bin > 0; bin /= 2, c += 1) {
|
||||
if (weekdaysBinaryEncoded / bin == 1) {
|
||||
weekdaysBinaryEncoded = weekdaysBinaryEncoded % bin;
|
||||
weekDaysAllowed[c] = true;
|
||||
}
|
||||
}
|
||||
|
||||
predicate = state -> {
|
||||
LocalDateTime now = getNow();
|
||||
int dow = now.get(ChronoField.DAY_OF_WEEK);
|
||||
LocalTime localTime = now.toLocalTime();
|
||||
return weekDaysAllowed[dow] && localTime.isAfter(timeStart) && localTime.isBefore(timeEnd)
|
||||
&& config.operator == Operator.in;
|
||||
};
|
||||
break;
|
||||
default:
|
||||
predicate = s -> true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For test injection
|
||||
protected LocalDateTime getNow() {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public boolean isSatisfied(Map<String, Object> context) {
|
||||
switch (config.operator) {
|
||||
case in:
|
||||
case not_in:
|
||||
return predicate.test(OnOffType.ON);
|
||||
default:
|
||||
}
|
||||
|
||||
State state = (State) context.get("newState");
|
||||
State oldState = (State) context.get("oldState");
|
||||
|
||||
if (state == null || oldState == null) {
|
||||
logger.warn("Expected a state and oldState input or an in/not_in operator!");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (config.operator) {
|
||||
case ddx: // Item changes always satisfies the "hue change" and "hue change delay" condition
|
||||
case dx:
|
||||
return true;
|
||||
case eq:
|
||||
case gt:
|
||||
case lt:
|
||||
return predicate.test(state);
|
||||
case not_stable: // state changed?
|
||||
return (!state.toFullString().equals(oldState.toFullString()));
|
||||
case stable: // state stable?
|
||||
return (state.toFullString().equals(oldState.toFullString()));
|
||||
case unknown:
|
||||
default:
|
||||
logger.warn("Operator {} not handled! ", config.operator.name());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.handler.ActionHandler;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandler;
|
||||
|
||||
/**
|
||||
* This action module type allows to remove a rule from the rule registry.
|
||||
* <p>
|
||||
* This is very useful for rules that should execute only once etc.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RemoveRuleActionHandler extends BaseModuleHandler<Action> implements ActionHandler {
|
||||
public static final String MODULE_TYPE_ID = "rules.RemoveRuleAction";
|
||||
public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
|
||||
public static final String MODULE_CONTEXT_NAME = "MODULE";
|
||||
|
||||
public static final String CFG_REMOVE_UID = "removeuid";
|
||||
private final String ruleUID;
|
||||
|
||||
private RuleRegistry ruleRegistry;
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
public RemoveRuleActionHandler(final Action module, RuleRegistry ruleRegistry) {
|
||||
super(module);
|
||||
this.ruleRegistry = ruleRegistry;
|
||||
final Configuration config = module.getConfiguration();
|
||||
if (config.getProperties().isEmpty()) {
|
||||
throw new IllegalArgumentException("'Configuration' can not be empty.");
|
||||
}
|
||||
|
||||
ruleUID = (String) config.get(CFG_REMOVE_UID);
|
||||
if (ruleUID == null) {
|
||||
throw new IllegalArgumentException("'ruleUIDs' property must not be null.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Map<String, Object> execute(Map<String, Object> context) {
|
||||
ruleRegistry.remove(ruleUID);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Module;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
|
||||
import org.openhab.core.automation.handler.ModuleHandler;
|
||||
import org.openhab.core.automation.handler.ModuleHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This factory is responsible for rule and http related module types.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = ModuleHandlerFactory.class)
|
||||
public class RulesHandlerFactory extends BaseModuleHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RulesHandlerFactory.class);
|
||||
|
||||
private static final Collection<String> TYPES = Arrays
|
||||
.asList(new String[] { RemoveRuleActionHandler.MODULE_TYPE_ID, HttpActionHandler.MODULE_TYPE_ID });
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) HttpClientFactory httpFactory;
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getTypes() {
|
||||
return TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
|
||||
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
|
||||
String moduleTypeUID = module.getTypeUID();
|
||||
if (RemoveRuleActionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Action) {
|
||||
return new RemoveRuleActionHandler((Action) module, ruleRegistry);
|
||||
} else if (HttpActionHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Action) {
|
||||
return new HttpActionHandler((Action) module, httpFactory);
|
||||
} else {
|
||||
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.scheduler.Scheduler;
|
||||
import org.openhab.core.automation.Module;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.handler.BaseModuleHandlerFactory;
|
||||
import org.openhab.core.automation.handler.ModuleHandler;
|
||||
import org.openhab.core.automation.handler.ModuleHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This factory is responsible for timer related module types.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(immediate = true, service = ModuleHandlerFactory.class)
|
||||
public class TimerModuleExHandlerFactory extends BaseModuleHandlerFactory {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(TimerModuleExHandlerFactory.class);
|
||||
|
||||
private static final Collection<String> TYPES = Arrays
|
||||
.asList(new String[] { AbsoluteDateTimeTriggerHandler.MODULE_TYPE_ID, TimerTriggerHandler.MODULE_TYPE_ID });
|
||||
|
||||
@Reference
|
||||
private @NonNullByDefault({}) Scheduler scheduler;
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getTypes() {
|
||||
return TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ModuleHandler internalCreate(Module module, String ruleUID) {
|
||||
logger.trace("create {} -> {}", module.getId(), module.getTypeUID());
|
||||
String moduleTypeUID = module.getTypeUID();
|
||||
if (AbsoluteDateTimeTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) {
|
||||
return new AbsoluteDateTimeTriggerHandler((Trigger) module, scheduler);
|
||||
} else if (TimerTriggerHandler.MODULE_TYPE_ID.equals(moduleTypeUID) && module instanceof Trigger) {
|
||||
return new TimerTriggerHandler((Trigger) module, scheduler);
|
||||
} else {
|
||||
logger.error("The module handler type '{}' is not supported.", moduleTypeUID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.scheduler.ScheduledCompletableFuture;
|
||||
import org.openhab.core.scheduler.Scheduler;
|
||||
import org.openhab.core.automation.ModuleHandlerCallback;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.handler.BaseTriggerModuleHandler;
|
||||
import org.openhab.core.automation.handler.TriggerHandlerCallback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This trigger module time allows a trigger that is setup with a time (hours:minutes:seconds).
|
||||
* As soon as that time has run up, it will trigger.
|
||||
* <p>
|
||||
* A random factor and repeat times can also be configured.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TimerTriggerHandler extends BaseTriggerModuleHandler implements Callable<Duration> {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(TimerTriggerHandler.class);
|
||||
|
||||
public static final String MODULE_TYPE_ID = "timer.TimerTrigger";
|
||||
public static final String CALLBACK_CONTEXT_NAME = "CALLBACK";
|
||||
public static final String MODULE_CONTEXT_NAME = "MODULE";
|
||||
|
||||
public static final String CFG_REPEAT = "repeat";
|
||||
public static final String CFG_TIME = "time";
|
||||
public static final String CFG_TIME_RND = "randomizeTime";
|
||||
|
||||
private final Scheduler scheduler;
|
||||
private final Duration duration;
|
||||
private @Nullable ScheduledCompletableFuture<?> schedule;
|
||||
|
||||
private static class Config {
|
||||
int repeat = 1;
|
||||
String time = "";
|
||||
String randomizeTime = "";
|
||||
}
|
||||
|
||||
Config config;
|
||||
|
||||
public TimerTriggerHandler(Trigger module, Scheduler scheduler) {
|
||||
super(module);
|
||||
this.scheduler = scheduler;
|
||||
config = module.getConfiguration().as(Config.class);
|
||||
|
||||
String[] fields = config.time.split(":");
|
||||
Duration d1 = Duration.parse(String.format("P%dH%dM%sS", fields[0], fields[1], fields[2]));
|
||||
|
||||
// Take optional random time (a range-like parameter) into account
|
||||
if (!config.randomizeTime.isEmpty()) {
|
||||
fields = config.randomizeTime.split(":");
|
||||
Duration d2 = Duration.parse(String.format("P%dH%dM%sS", fields[0], fields[1], fields[2]));
|
||||
// The random time must be later a bigger value than time
|
||||
if (d1.compareTo(d2) >= 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
// Compute the difference, turn in to seconds, get a random second value between 0 and that upper bound
|
||||
// and then add it to the base time
|
||||
Duration difference = d2.minus(d1);
|
||||
duration = d1.plus(Duration.ofSeconds(randomSeconds(difference.getSeconds())));
|
||||
} else {
|
||||
duration = d1;
|
||||
}
|
||||
}
|
||||
|
||||
protected long randomSeconds(long maximum) {
|
||||
return Math.abs(new Random().nextLong()) % maximum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setCallback(ModuleHandlerCallback callback) {
|
||||
super.setCallback(callback);
|
||||
if (config.repeat != 0) {
|
||||
scheduleJob();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleJob() {
|
||||
schedule = scheduler.after(this, duration);
|
||||
logger.debug("Scheduled timer to expire in '{}' for trigger '{}'.", duration, module.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
super.dispose();
|
||||
ScheduledCompletableFuture<?> future = schedule;
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
logger.debug("cancelled job for trigger '{}'.", module.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration call() {
|
||||
((TriggerHandlerCallback) callback).triggered(module, null);
|
||||
config.repeat -= 1;
|
||||
if (config.repeat == 0) {
|
||||
schedule = null;
|
||||
} else {
|
||||
scheduleJob();
|
||||
}
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
|
||||
|
||||
/**
|
||||
* A configuration holder class for the rule trigger handler
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueRuleTriggerConfig {
|
||||
public String address = "";
|
||||
public HueRuleEntry.Operator operator = HueRuleEntry.Operator.dx;
|
||||
public @Nullable String value;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* A configuration holder class for the item command handler
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ItemCommandActionConfig {
|
||||
public String itemName = "";
|
||||
public String command = "";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
/**
|
||||
* Hue API state object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class AbstractHueState {
|
||||
public boolean reachable = true;
|
||||
public String mode = "homeautomation";
|
||||
|
||||
public static enum AlertEnum {
|
||||
none,
|
||||
/** flashes light once */
|
||||
select,
|
||||
/** flashes repeatedly for 10 seconds. */
|
||||
lselect
|
||||
}
|
||||
|
||||
public String alert = AlertEnum.none.name();
|
||||
|
||||
public <T extends AbstractHueState> T as(Class<T> type) throws ClassCastException {
|
||||
return type.cast(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Hue API config object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueAuthorizedConfig extends HueUnauthorizedConfig {
|
||||
public String uuid = ""; // Example: 5673dfa7-272c-4315-9955-252cdd86131c
|
||||
|
||||
public String timeformat = "24h";
|
||||
public String timezone = ZonedDateTime.now().getOffset().getId().replace("Z", "+00:00");
|
||||
public String UTC = "2018-11-10T15:24:23";
|
||||
public String localtime = "2018-11-10T16:24:23";
|
||||
|
||||
public String devicename = "openHAB";
|
||||
|
||||
public String fwversion = "0x262e0500";
|
||||
|
||||
public boolean rfconnected = true;
|
||||
public int zigbeechannel = 15;
|
||||
public boolean linkbutton = false;
|
||||
public transient boolean createNewUserOnEveryEndpoint = false;
|
||||
public int panid = 19367;
|
||||
|
||||
public boolean dhcp = true;
|
||||
public String gateway = "192.168.0.1";
|
||||
public String ipaddress = ""; // Example: 192.168.0.46
|
||||
public String netmask = ""; // Example: 255.255.255.0
|
||||
public int networkopenduration = 60;
|
||||
|
||||
public String proxyaddress = "none";
|
||||
public int proxyport = 0;
|
||||
|
||||
public final Map<String, HueUserAuth> whitelist = new TreeMap<>();
|
||||
|
||||
public void makeV1bridge() {
|
||||
apiversion = "1.16.0";
|
||||
datastoreversion = "60";
|
||||
modelid = "BSB001";
|
||||
swversion = "01041302";
|
||||
fwversion = "0x262e0500";
|
||||
}
|
||||
|
||||
public void makeV2bridge() {
|
||||
apiversion = "1.22.0";
|
||||
datastoreversion = "60";
|
||||
modelid = "BSB002";
|
||||
swversion = "1901181309";
|
||||
fwversion = "0x262e0500";
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a json serializer that behaves like the default one, but updates the UTC and localtime fields
|
||||
* before each serializion.
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public static class Serializer implements JsonSerializer<HueAuthorizedConfig> {
|
||||
static class HueAuthorizedConfigHelper extends HueAuthorizedConfig {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(HueAuthorizedConfig src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
src.UTC = LocalDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
src.localtime = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
JsonElement jsonSubscription = context.serialize(src, HueAuthorizedConfigHelper.class);
|
||||
return jsonSubscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
/**
|
||||
* Hue Capabilities per endpoint
|
||||
* Enpoint: /api/{username}/capabilities
|
||||
* <p>
|
||||
* https://developers.meethue.com/develop/hue-api/10-capabilities-api/
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueCapability {
|
||||
public int available = 10;
|
||||
public int total = 10000;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Hue data store object. Contains all lights, configuration, user whitelist etc.
|
||||
* Is used as a data store but also as API DTO.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - Add groups,scenes,rules,sensors,resourcelinks and config entries
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueDataStore {
|
||||
public HueAuthorizedConfig config = new HueAuthorizedConfig();
|
||||
public TreeMap<String, HueLightEntry> lights = new TreeMap<>();
|
||||
public TreeMap<String, HueGroupEntry> groups = new TreeMap<>();
|
||||
public Map<String, HueSceneEntry> scenes = new TreeMap<>();
|
||||
public Map<String, HueRuleEntry> rules = new TreeMap<>();
|
||||
public Map<String, HueSensorEntry> sensors = new TreeMap<>();
|
||||
public Map<String, HueScheduleEntry> schedules = new TreeMap<>();
|
||||
public Map<Integer, Dummy> resourcelinks = Collections.emptyMap();
|
||||
public Map<String, HueCapability> capabilities = new TreeMap<>();
|
||||
|
||||
public HueDataStore() {
|
||||
resetGroupsAndLights();
|
||||
capabilities.put("lights", new HueCapability());
|
||||
capabilities.put("groups", new HueCapability());
|
||||
capabilities.put("scenes", new HueCapability());
|
||||
capabilities.put("rules", new HueCapability());
|
||||
capabilities.put("sensors", new HueCapability());
|
||||
capabilities.put("schedules", new HueCapability());
|
||||
capabilities.put("resourcelinks", new HueCapability());
|
||||
}
|
||||
|
||||
public void resetGroupsAndLights() {
|
||||
groups.clear();
|
||||
lights.clear();
|
||||
// There must be a group 0 all the time!
|
||||
groups.put("0", new HueGroupEntry("All lights", null, null));
|
||||
}
|
||||
|
||||
public void resetSensors() {
|
||||
sensors.clear();
|
||||
}
|
||||
|
||||
public static class Dummy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a unique group id.
|
||||
*/
|
||||
public String nextGroupID() {
|
||||
int nextId = groups.size();
|
||||
while (true) {
|
||||
String id = "hueemulation" + String.valueOf(nextId);
|
||||
if (!groups.containsKey(id)) {
|
||||
return id;
|
||||
}
|
||||
++nextId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Hue API group object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueGroupEntry {
|
||||
public static enum TypeEnum {
|
||||
LightGroup, // 1.4
|
||||
Luminaire, // 1.4
|
||||
LightSource, // 1.4
|
||||
Room, // 1.11
|
||||
Entertainment, // 1.22
|
||||
Zone // 1.30
|
||||
}
|
||||
|
||||
public AbstractHueState action = new HueStatePlug();
|
||||
|
||||
// The group type
|
||||
public String type = TypeEnum.LightGroup.name();
|
||||
|
||||
// A unique, editable name given to the group.
|
||||
public String name;
|
||||
|
||||
@SerializedName("class")
|
||||
public String roomclass = "Other";
|
||||
|
||||
// The IDs of the lights that are in the group.
|
||||
public List<String> lights = Collections.emptyList();
|
||||
public List<String> sensors = Collections.emptyList();
|
||||
|
||||
public transient @NonNullByDefault({}) GroupItem groupItem;
|
||||
public transient @Nullable DeviceType deviceType;
|
||||
|
||||
// For deserialisation
|
||||
HueGroupEntry() {
|
||||
name = "";
|
||||
}
|
||||
|
||||
public HueGroupEntry(String name, @Nullable GroupItem groupItem, @Nullable DeviceType deviceType) {
|
||||
this.name = name;
|
||||
this.groupItem = groupItem;
|
||||
this.deviceType = deviceType;
|
||||
}
|
||||
|
||||
public void updateItem(GroupItem element) {
|
||||
groupItem = element;
|
||||
}
|
||||
|
||||
/**
|
||||
* This custom serializer computes the {@link HueGroupEntry#lights} list, before serializing.
|
||||
* It does so, by looking up all item members of the references groupItem.
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public static class Serializer implements JsonSerializer<HueGroupEntry> {
|
||||
|
||||
private ConfigStore cs;
|
||||
|
||||
public Serializer(ConfigStore cs) {
|
||||
this.cs = cs;
|
||||
}
|
||||
|
||||
static class HueGroupHelper extends HueGroupEntry {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(HueGroupEntry product, Type type, JsonSerializationContext context) {
|
||||
GroupItem item = product.groupItem;
|
||||
if (item != null) {
|
||||
product.lights = item.getMembers().stream().map(gitem -> cs.mapItemUIDtoHueID(gitem))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
JsonElement jsonSubscription = context.serialize(product, HueGroupHelper.class);
|
||||
return jsonSubscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.library.items.StringItem;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
import org.openhab.io.hueemulation.internal.StateUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Hue API device object
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - Color lights and plugs
|
||||
* @author Florian Lentz - XY Support
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueLightEntry {
|
||||
public AbstractHueState state = new AbstractHueState();
|
||||
public final String type;
|
||||
public final String modelid;
|
||||
public final String uniqueid;
|
||||
public final String manufacturername;
|
||||
public final @Nullable String productname;
|
||||
public final String swversion;
|
||||
public final @Nullable String luminaireuniqueid = null;
|
||||
public final @Nullable String swconfigid;
|
||||
public final @Nullable String productid;
|
||||
public @Nullable Boolean friendsOfHue = true;
|
||||
public final @Nullable String colorGamut;
|
||||
public @Nullable Boolean hascolor = null;
|
||||
|
||||
public String name;
|
||||
/** Associated item UID */
|
||||
public @NonNullByDefault({}) transient GenericItem item;
|
||||
public transient DeviceType deviceType;
|
||||
public transient @Nullable Command lastCommand = null;
|
||||
public transient @Nullable HueStateChange lastHueChange = null;
|
||||
|
||||
public static class Config {
|
||||
public final String archetype = "classicbulb";
|
||||
public final String function = "functional";
|
||||
public final String direction = "omnidirectional";
|
||||
}
|
||||
|
||||
public Config config = new Config();
|
||||
|
||||
public static class Streaming {
|
||||
public boolean renderer = false;
|
||||
public boolean proxy = false;
|
||||
}
|
||||
|
||||
public static class Capabilities {
|
||||
public boolean certified = false;
|
||||
public final Streaming streaming = new Streaming();
|
||||
public final Object control = new Object();
|
||||
}
|
||||
|
||||
public Capabilities capabilities = new Capabilities();
|
||||
|
||||
private HueLightEntry() {
|
||||
this(new StringItem(""), "", DeviceType.SwitchType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hue device.
|
||||
*
|
||||
* @param item The associated item
|
||||
* @param uniqueid The unique id
|
||||
* @param deviceType The device type decides which capabilities this device has
|
||||
*/
|
||||
public HueLightEntry(GenericItem item, String uniqueid, DeviceType deviceType) {
|
||||
String label = item.getLabel();
|
||||
this.item = item;
|
||||
this.deviceType = deviceType;
|
||||
this.uniqueid = uniqueid;
|
||||
switch (deviceType) {
|
||||
case ColorType:
|
||||
this.name = label != null ? label : "";
|
||||
this.type = "Extended Color light";
|
||||
this.modelid = "LCT010";
|
||||
this.colorGamut = "C";
|
||||
this.manufacturername = "Philips";
|
||||
this.swconfigid = "F921C859";
|
||||
this.swversion = "1.15.2_r19181";
|
||||
this.productid = "Philips-LCT010-1-A19ECLv4";
|
||||
this.productname = null;
|
||||
this.hascolor = true;
|
||||
this.capabilities.certified = true;
|
||||
break;
|
||||
case WhiteType:
|
||||
/** Hue White A19 - 3nd gen - white, 2700K only */
|
||||
this.name = label != null ? label : "";
|
||||
this.type = "Dimmable light";
|
||||
this.modelid = "LWB006";
|
||||
this.colorGamut = null;
|
||||
this.manufacturername = "Philips";
|
||||
this.swconfigid = null;
|
||||
this.swversion = "66012040";
|
||||
this.productid = null;
|
||||
this.hascolor = false;
|
||||
this.productname = null;
|
||||
this.capabilities.certified = true;
|
||||
break;
|
||||
case WhiteTemperatureType:
|
||||
this.name = label != null ? label : "";
|
||||
this.type = "Color temperature light";
|
||||
this.modelid = "LTW001";
|
||||
this.colorGamut = "2200K-6500K";
|
||||
this.manufacturername = "Philips";
|
||||
this.swconfigid = null;
|
||||
this.swversion = "66012040";
|
||||
this.productid = null;
|
||||
this.hascolor = false;
|
||||
this.productname = null;
|
||||
this.capabilities.certified = true;
|
||||
break;
|
||||
case SwitchType:
|
||||
default:
|
||||
/**
|
||||
* Pretend to be an OSRAM plug, there is no native Philips Hue plug on the market.
|
||||
* Those are supported by most of the external apps and Alexa.
|
||||
*/
|
||||
this.name = label != null ? label : "";
|
||||
this.type = "On/off light";
|
||||
this.modelid = "Plug 01";
|
||||
this.colorGamut = null;
|
||||
this.manufacturername = "OSRAM";
|
||||
this.productname = "On/Off plug";
|
||||
this.swconfigid = null;
|
||||
this.swversion = "V1.04.12";
|
||||
this.productid = null;
|
||||
this.hascolor = false;
|
||||
this.friendsOfHue = null;
|
||||
break;
|
||||
}
|
||||
|
||||
state = StateUtils.colorStateFromItemState(item.getState(), deviceType);
|
||||
}
|
||||
|
||||
/**
|
||||
* This custom serializer updates the light state and label, before serializing.
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public static class Serializer implements JsonSerializer<HueLightEntry> {
|
||||
|
||||
static class HueDeviceHelper extends HueLightEntry {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(HueLightEntry product, Type type, JsonSerializationContext context) {
|
||||
product.state = StateUtils.adjustedColorStateFromItemState(product.item.getState(), product.deviceType,
|
||||
product.lastCommand, product.lastHueChange);
|
||||
String label = product.item.getLabel();
|
||||
if (label != null) {
|
||||
product.name = label;
|
||||
}
|
||||
|
||||
JsonElement jsonSubscription = context.serialize(product, HueDeviceHelper.class);
|
||||
return jsonSubscription;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the associated openHAB item of this hue device with the given once
|
||||
* and also synchronizes/updates the color information of this hue device with the item.
|
||||
*
|
||||
* @param element A replace item
|
||||
*/
|
||||
public void updateItem(GenericItem element) {
|
||||
item = element;
|
||||
state = StateUtils.colorStateFromItemState(item.getState(), deviceType);
|
||||
|
||||
lastCommand = null;
|
||||
lastHueChange = null;
|
||||
|
||||
String label = element.getLabel();
|
||||
if (label != null) {
|
||||
name = label;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
/**
|
||||
* Hue API scan result object.
|
||||
* Enpoint: /api/{username}/lights/new
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueNewLights {
|
||||
enum ScanType {
|
||||
none,
|
||||
active
|
||||
}
|
||||
|
||||
/** Either none, active or a timestamp since the last search */
|
||||
public String lastscan = ScanType.none.name();
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Hue API rule object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueRuleEntry {
|
||||
// A unique, editable name given to the group.
|
||||
public String name = "";
|
||||
public String description = "";
|
||||
|
||||
public String owner = "";
|
||||
|
||||
public static enum Operator {
|
||||
unknown,
|
||||
eq, // equals, Used for bool and int.
|
||||
gt, // greater than, Allowed on int values.
|
||||
lt, // less than, Allowed on int values.
|
||||
dx, // value has changed, Time (timestamps) int and bool values. Only dx or ddx is allowed, but not both.
|
||||
ddx, // delayed value has changed
|
||||
stable, // Time (timestamps) int and bool values. An attribute has or has not changed for a given time.
|
||||
not_stable,
|
||||
in, // Current time is in or not in given time interval (only for /config/localtime, not UTC). “in” rule will be
|
||||
// triggered on starttime and “not in” rule will be triggered on endtime. Only one “in” operator is allowed
|
||||
// in a rule. Multiple “not in” operators are allowed in a rule.
|
||||
not_in
|
||||
}
|
||||
|
||||
/**
|
||||
* A complete condition could look like this:
|
||||
* <p>
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "address": "/sensors/2/state/buttonevent",
|
||||
* "operator": "eq",
|
||||
* "value": "16"
|
||||
* },
|
||||
* </pre>
|
||||
*/
|
||||
public static class Condition {
|
||||
/**
|
||||
* A hue resource address like "/config/localtime" or "/sensors/2/state/buttonevent"
|
||||
*/
|
||||
public String address = "";
|
||||
public Operator operator = Operator.unknown;
|
||||
/**
|
||||
* A value like "16"
|
||||
*/
|
||||
public @Nullable String value;
|
||||
|
||||
public Condition() {
|
||||
}
|
||||
|
||||
public Condition(String address, Operator operator, @Nullable String value) {
|
||||
this.address = address;
|
||||
this.value = value;
|
||||
this.operator = operator;
|
||||
}
|
||||
}
|
||||
|
||||
// The IDs of the lights that are in the group.
|
||||
public List<Condition> conditions = new ArrayList<>();
|
||||
public List<HueCommand> actions = new ArrayList<>();
|
||||
|
||||
HueRuleEntry() {
|
||||
name = "";
|
||||
}
|
||||
|
||||
public HueRuleEntry(@Nullable String name) {
|
||||
this.name = name != null ? name : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* This custom serializer (de)serialize the Condition class and translates between the enum values that have
|
||||
* an underbar and the json strings that have a whitespace instead.
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public static class SerializerCondition
|
||||
implements JsonSerializer<HueRuleEntry.Condition>, JsonDeserializer<HueRuleEntry.Condition> {
|
||||
@Override
|
||||
public JsonElement serialize(HueRuleEntry.Condition product, Type type, JsonSerializationContext context) {
|
||||
JsonObject jObj = new JsonObject();
|
||||
jObj.addProperty("address", product.address);
|
||||
String value = product.value;
|
||||
if (value != null) {
|
||||
jObj.addProperty("value", value);
|
||||
}
|
||||
jObj.addProperty("operator", product.operator.name().replace("_", " "));
|
||||
|
||||
return jObj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
JsonObject jObj = json.getAsJsonObject();
|
||||
Condition c = new Condition();
|
||||
|
||||
c.address = jObj.get("address").getAsString();
|
||||
if (jObj.has("value")) {
|
||||
c.value = jObj.get("value").getAsString();
|
||||
}
|
||||
String operator = jObj.get("operator").getAsString().replace(" ", "_");
|
||||
c.operator = Operator.valueOf(operator);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Hue API scene object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueSceneEntry {
|
||||
public static enum TypeEnum {
|
||||
LightScene, // 1.28
|
||||
GroupScene, // 1.28
|
||||
}
|
||||
|
||||
public TypeEnum type = TypeEnum.LightScene;
|
||||
|
||||
// A unique, editable name given to the group.
|
||||
public String name;
|
||||
public String description = "";
|
||||
|
||||
public String owner = "";
|
||||
public boolean recycle = false;
|
||||
public boolean locked = false;
|
||||
|
||||
final int version = 2;
|
||||
|
||||
public String appdata = "";
|
||||
public String picture = "";
|
||||
|
||||
// The IDs of the lights that are in the group.
|
||||
public @Nullable List<String> lights;
|
||||
public @Nullable String group;
|
||||
|
||||
HueSceneEntry() {
|
||||
name = "";
|
||||
}
|
||||
|
||||
public HueSceneEntry(@Nullable String name) {
|
||||
this.name = name != null ? name : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Hue API scene object with light states
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueSceneWithLightstates extends HueSceneEntry {
|
||||
|
||||
public Map<String, AbstractHueState> lightstates = new TreeMap<>();
|
||||
|
||||
HueSceneWithLightstates() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HueSceneWithLightstates(HueSceneEntry e) {
|
||||
this.type = e.type;
|
||||
this.name = e.name;
|
||||
this.description = e.description;
|
||||
this.owner = e.owner;
|
||||
this.recycle = e.recycle;
|
||||
this.locked = e.locked;
|
||||
this.appdata = e.appdata;
|
||||
this.picture = e.picture;
|
||||
this.lights = e.lights;
|
||||
this.group = e.group;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeScheduleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
|
||||
/**
|
||||
* Hue API scan result object.
|
||||
* Enpoint: /api/{username}/lights/new
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueScheduleEntry extends HueChangeScheduleEntry {
|
||||
/**
|
||||
* Assign default values to all fields that are otherwise nullable by the {@link HueChangeScheduleEntry}.
|
||||
*/
|
||||
public HueScheduleEntry() {
|
||||
command = new HueCommand();
|
||||
name = "";
|
||||
description = "";
|
||||
localtime = "";
|
||||
status = "disabled";
|
||||
recycle = false;
|
||||
autodelete = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* Hue API scene object
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueSensorEntry {
|
||||
// A unique, editable name given to the group.
|
||||
public String name;
|
||||
public String type;
|
||||
public String modelid;
|
||||
public String manufacturername = "openHab";
|
||||
public String swversion = "1.0";
|
||||
public Object config = new Object();
|
||||
public String uniqueid;
|
||||
|
||||
public final @NonNullByDefault({}) transient GenericItem item;
|
||||
|
||||
private HueSensorEntry() {
|
||||
item = null;
|
||||
name = "";
|
||||
type = "";
|
||||
modelid = "";
|
||||
uniqueid = "";
|
||||
}
|
||||
|
||||
public HueSensorEntry(GenericItem item) throws IllegalArgumentException {
|
||||
this.item = item;
|
||||
String label = item.getLabel();
|
||||
this.name = label != null ? label : item.getName();
|
||||
this.modelid = "openHAB_" + item.getType();
|
||||
this.uniqueid = item.getUID();
|
||||
switch (item.getType()) {
|
||||
case CoreItemFactory.CONTACT:
|
||||
this.type = "CLIPOpenClose"; // "open"
|
||||
break;
|
||||
case CoreItemFactory.ROLLERSHUTTER:
|
||||
case CoreItemFactory.DIMMER:
|
||||
this.type = "CLIPGenericStatus"; // "status" (int)
|
||||
break;
|
||||
case CoreItemFactory.NUMBER:
|
||||
if (item.hasTag("temperature")) {
|
||||
this.type = "CLIPTemperature"; // "temperature"
|
||||
} else {
|
||||
this.type = "CLIPLightLevel"; // "lightlevel" (int), "dark" (bool), "daylight" (bool)
|
||||
}
|
||||
break;
|
||||
case CoreItemFactory.COLOR:
|
||||
this.type = "CLIPLightLevel"; // "lightlevel" (int), "dark" (bool), "daylight" (bool)
|
||||
break;
|
||||
case CoreItemFactory.SWITCH:
|
||||
this.type = "CLIPGenericFlag"; // "flag" (bool)
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Item type not supported as sensor");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This custom serializer computes the {@link HueGroupEntry#lights} list, before serializing.
|
||||
* It does so, by looking up all item members of the references groupItem.
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
public static class Serializer implements JsonSerializer<HueSensorEntry> {
|
||||
|
||||
static class HueHelper extends HueSensorEntry {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(HueSensorEntry product, Type type, JsonSerializationContext context) {
|
||||
JsonElement json = context.serialize(product, HueHelper.class);
|
||||
JsonObject state = new JsonObject();
|
||||
State itemState = product.item.getState();
|
||||
switch (product.type) {
|
||||
case "CLIPOpenClose":
|
||||
if (itemState instanceof OpenClosedType) {
|
||||
state.addProperty("open", ((OpenClosedType) product.item.getState()) == OpenClosedType.OPEN);
|
||||
} else {
|
||||
state.addProperty("open", false);
|
||||
}
|
||||
break;
|
||||
case "CLIPGenericStatus":
|
||||
if (itemState instanceof DecimalType) {
|
||||
state.addProperty("status", ((DecimalType) product.item.getState()).intValue());
|
||||
} else {
|
||||
state.addProperty("status", 0);
|
||||
}
|
||||
break;
|
||||
case "CLIPTemperature":
|
||||
if (itemState instanceof DecimalType) {
|
||||
state.addProperty("temperature", ((DecimalType) product.item.getState()).intValue());
|
||||
} else {
|
||||
state.addProperty("status", 0);
|
||||
}
|
||||
break;
|
||||
case "CLIPLightLevel":
|
||||
if (itemState instanceof DecimalType) {
|
||||
state.addProperty("lightlevel", ((DecimalType) product.item.getState()).intValue());
|
||||
} else {
|
||||
state.addProperty("status", 0);
|
||||
}
|
||||
state.addProperty("dark", false);
|
||||
state.addProperty("daylight", false);
|
||||
break;
|
||||
case "CLIPGenericFlag":
|
||||
if (itemState instanceof OnOffType) {
|
||||
state.addProperty("flag", ((OnOffType) product.item.getState()) == OnOffType.ON);
|
||||
} else {
|
||||
state.addProperty("open", false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
json.getAsJsonObject().add("state", state);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
|
||||
/**
|
||||
* Hue API state object
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - "extended color light bulbs" support
|
||||
*
|
||||
*/
|
||||
public class HueStateBulb extends HueStatePlug {
|
||||
// https://github.com/openhab/openhab-addons/issues/2881
|
||||
// Apparently the maximum brightness is 254
|
||||
public static int MAX_BRI = 254;
|
||||
public int bri = 0;
|
||||
|
||||
/** white color temperature, 154 (cold) - 500 (warm) */
|
||||
public static int MAX_CT = 500;
|
||||
public int ct = 500;
|
||||
|
||||
protected HueStateBulb() {
|
||||
}
|
||||
|
||||
public HueStateBulb(boolean on) {
|
||||
super(on);
|
||||
this.bri = on ? MAX_BRI : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hue state with the given brightness percentage
|
||||
*
|
||||
* @param brightness Brightness percentage
|
||||
* @param on On value
|
||||
*/
|
||||
public HueStateBulb(PercentType brightness, boolean on) {
|
||||
super(on);
|
||||
this.bri = Math.max(1, (int) (brightness.intValue() * MAX_BRI / 100.0 + 0.5));
|
||||
}
|
||||
|
||||
public PercentType toBrightnessType() {
|
||||
int bri = this.bri * 100 / MAX_BRI;
|
||||
|
||||
if (!this.on) {
|
||||
bri = 0;
|
||||
}
|
||||
return new PercentType(bri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "on: " + on + ", brightness: " + bri + ", reachable: " + reachable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
|
||||
/**
|
||||
* Hue API state object
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - "extended color light bulbs" support
|
||||
* @author Florian Lentz - added xy support
|
||||
*
|
||||
*/
|
||||
public class HueStateColorBulb extends HueStateBulb {
|
||||
public static int MAX_HUE = 65535; // For extended color light bulbs
|
||||
public int hue = 0;
|
||||
public static int MAX_SAT = 254;
|
||||
public int sat = 0;
|
||||
|
||||
// color as array of xy-coordinates
|
||||
public double[] xy = { 0, 0 };
|
||||
|
||||
public String effect = "none";
|
||||
|
||||
/** time for transition in centiseconds. */
|
||||
public int transitiontime;
|
||||
|
||||
public static enum ColorMode {
|
||||
ct,
|
||||
hs,
|
||||
xy
|
||||
}
|
||||
|
||||
public ColorMode colormode = ColorMode.ct;
|
||||
|
||||
protected HueStateColorBulb() {
|
||||
}
|
||||
|
||||
public HueStateColorBulb(boolean on) {
|
||||
super(on);
|
||||
colormode = ColorMode.ct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hue state with the given brightness percentage
|
||||
*
|
||||
* @param brightness Brightness percentage
|
||||
* @param on On value
|
||||
*/
|
||||
public HueStateColorBulb(PercentType brightness, boolean on) {
|
||||
super(brightness, on);
|
||||
colormode = ColorMode.ct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a hue state with the given color information
|
||||
*
|
||||
* @param hsb Color information. Sets the hue state to "on" if brightness is > 0.
|
||||
*/
|
||||
public HueStateColorBulb(HSBType hsb) {
|
||||
super(hsb.getBrightness(), hsb.getBrightness().intValue() > 0);
|
||||
this.hue = (int) (hsb.getHue().intValue() * MAX_HUE / 360.0 + 0.5);
|
||||
this.sat = (int) (hsb.getSaturation().intValue() * MAX_SAT / 100.0 + 0.5);
|
||||
colormode = this.sat > 0 ? ColorMode.hs : ColorMode.ct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this HueState to a HSBType
|
||||
*/
|
||||
public HSBType toHSBType() {
|
||||
if (colormode == ColorMode.xy) {
|
||||
int i;
|
||||
double y = (this.bri) / 100.0d;
|
||||
double x = (y / this.xy[1]) * this.xy[0];
|
||||
double z = (y / this.xy[1]) * ((1.0d - this.xy[0]) - this.xy[1]);
|
||||
int r = (int) (Math.abs(
|
||||
((1.4628067016601562d * x) - (0.18406230211257935d * y)) - (0.2743605971336365d * z)) * 255.0d);
|
||||
int g = (int) (Math.abs(
|
||||
(((-x) * 0.5217933058738708d) + (1.4472380876541138d * y)) + (0.06772270053625107d * z)) * 255.0d);
|
||||
int b = (int) (Math.abs(
|
||||
((0.03493420034646988d * x) - (0.09689299762248993d * y)) + (1.288409948348999d * z)) * 255.0d);
|
||||
if (r < g) {
|
||||
i = r;
|
||||
} else {
|
||||
i = g;
|
||||
}
|
||||
double minValue = i;
|
||||
if (minValue >= b) {
|
||||
minValue = b;
|
||||
}
|
||||
double maxValue = (r > g ? r : g);
|
||||
if (maxValue <= b) {
|
||||
maxValue = b;
|
||||
}
|
||||
double delta = maxValue - minValue;
|
||||
if (maxValue <= 0.0d) {
|
||||
return new HSBType(new DecimalType(0), new PercentType(100),
|
||||
new PercentType((this.bri * 100) / MAX_BRI));
|
||||
}
|
||||
double h;
|
||||
if ((r) >= maxValue) {
|
||||
h = (g - b) / delta;
|
||||
} else if ((g) >= maxValue) {
|
||||
h = 2.0d + ((b - r) / delta);
|
||||
} else {
|
||||
h = 4.0d + ((r - g) / delta);
|
||||
}
|
||||
h *= 60.0d;
|
||||
if (h < 0.0d) {
|
||||
h += 360.0d;
|
||||
}
|
||||
double hueSat = Math.floor((delta / maxValue) * 254.0d);
|
||||
int percentSat = (int) ((100.0d * hueSat) / (MAX_SAT));
|
||||
|
||||
int bri = this.bri * 100 / MAX_BRI;
|
||||
if (!this.on) {
|
||||
bri = 0;
|
||||
}
|
||||
return new HSBType(new DecimalType((Math.floor(182.04d * h) * 360.0d) / (MAX_HUE)),
|
||||
new PercentType(percentSat), new PercentType(bri));
|
||||
|
||||
} else {
|
||||
int bri = this.bri * 100 / MAX_BRI;
|
||||
int sat = this.sat * 100 / MAX_SAT;
|
||||
int hue = this.hue * 360 / MAX_HUE;
|
||||
|
||||
if (!this.on) {
|
||||
bri = 0;
|
||||
}
|
||||
if (colormode == ColorMode.ct) {
|
||||
sat = 0;
|
||||
}
|
||||
return new HSBType(new DecimalType(hue), new PercentType(sat), new PercentType(bri));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String xyString = "{";
|
||||
for (double d : xy) {
|
||||
xyString += d + " ";
|
||||
}
|
||||
xyString += "}";
|
||||
return "on: " + on + ", brightness: " + bri + ", hue: " + hue + ", sat: " + sat + ", xy: " + xyString + ", ct: "
|
||||
+ ct + ", alert: " + alert + ", effect: " + effect + ", colormode: " + colormode + ", reachable: "
|
||||
+ reachable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
|
||||
/**
|
||||
* Hue API state object for plugs
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class HueStatePlug extends AbstractHueState {
|
||||
public boolean on;
|
||||
|
||||
protected HueStatePlug() {
|
||||
}
|
||||
|
||||
public HueStatePlug(boolean on) {
|
||||
this.on = on;
|
||||
}
|
||||
|
||||
public OnOffType toOnOffType() {
|
||||
return on ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "on: " + on + ", reachable: " + reachable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Hue API config object. Also accessible for non-authorized users.
|
||||
* Enpoint: /api/config
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueUnauthorizedConfig {
|
||||
public String apiversion = "1.18.0";
|
||||
public String bridgeid = ""; // Example: 00212EFFFF022F6E
|
||||
public String datastoreversion = "60";
|
||||
public String starterkitid = "";
|
||||
public String modelid = "BSB002";
|
||||
public String name = "openHAB Devices";
|
||||
|
||||
public String swversion = "2.5.46";
|
||||
public String mac = "b8:27:eb:1d:d8:0c";
|
||||
public boolean factorynew = false;
|
||||
public @Nullable String replacesbridgeid = null;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Hue user object. Used by {@link HueAuthorizedConfig}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueUserAuth {
|
||||
public String name = "";
|
||||
public String createDate = "";
|
||||
public String lastUseDate = "";
|
||||
|
||||
public String clientKey = "";
|
||||
|
||||
/**
|
||||
* For de-serialization.
|
||||
*/
|
||||
HueUserAuth() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*
|
||||
* @param apikey The hue "username"
|
||||
*/
|
||||
public HueUserAuth(String appName, String deviceName) {
|
||||
this.name = appName + "#" + deviceName;
|
||||
this.createDate = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.Identifiable;
|
||||
|
||||
/**
|
||||
* Hue user object. This is stored on disk for hue users, but not send via the rest api
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueUserAuthWithSecrets extends HueUserAuth implements Identifiable<String> {
|
||||
public String apiKey = "";
|
||||
|
||||
/**
|
||||
* For de-serialization.
|
||||
*/
|
||||
HueUserAuthWithSecrets() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user with credentials
|
||||
*/
|
||||
public HueUserAuthWithSecrets(String appName, String deviceName, String apiKey, String clientKey) {
|
||||
super(appName, deviceName);
|
||||
this.apiKey = apiKey;
|
||||
this.clientKey = clientKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUID() {
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Multiple endpoints support PUT changes, for example:
|
||||
* <ul>
|
||||
* <li>Config: Allows to change the bridge name, dhcp, portalservices, linkbutton
|
||||
* <li>Light: Allows to change the name
|
||||
* <li>Group: Allows to change the name
|
||||
* <li>Sensor: Allows to change the name
|
||||
* </ul>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueChangeRequest {
|
||||
public @Nullable String devicename;
|
||||
public @Nullable String name;
|
||||
public @Nullable Boolean dhcp;
|
||||
public @Nullable Boolean linkbutton;
|
||||
public @Nullable Boolean portalservices;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Hue API scan result object.
|
||||
* Enpoint: /api/{username}/scenes/ab341ef24/lights/1/state
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueChangeSceneEntry {
|
||||
public @Nullable String name;
|
||||
public @Nullable String description;
|
||||
|
||||
public boolean storelightstate = false;
|
||||
|
||||
public @Nullable List<String> lights;
|
||||
|
||||
public @Nullable Map<String, HueStateChange> lightstates;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Hue API scan result object.
|
||||
* Enpoint: /api/{username}/lights/new
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueChangeScheduleEntry {
|
||||
public @Nullable String name;
|
||||
public @Nullable String description;
|
||||
|
||||
public @Nullable String localtime;
|
||||
// Either "enabled" or "disabled"
|
||||
public @Nullable String status;
|
||||
public @Nullable Boolean autodelete;
|
||||
public @Nullable Boolean recycle;
|
||||
|
||||
public @Nullable HueCommand command;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Commands are http request data (method+url+body) used by schedules and rules.
|
||||
* <p>
|
||||
* Note: Rules use shorter address variants without the "/api/username" parts, which
|
||||
* makes them no valid relative urls!
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueCommand {
|
||||
public String address = "";
|
||||
public String method = "";
|
||||
public String body = "";
|
||||
|
||||
public boolean isValid() {
|
||||
return !address.isEmpty() && !method.isEmpty() && !body.isEmpty();
|
||||
}
|
||||
|
||||
public HueCommand() {
|
||||
}
|
||||
|
||||
public HueCommand(String address, String method, String body) {
|
||||
this.address = address;
|
||||
this.method = method;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Hue API create user object
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - Rewritten
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueCreateUser {
|
||||
/** ApplicationName#DeviceName */
|
||||
public String devicetype = "";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.changerequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A POST message on a light entpoint will contain this change message body.
|
||||
* Not all fields will be set and always need to be checked.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueStateChange {
|
||||
public Boolean on;
|
||||
public Integer bri;
|
||||
public Integer hue;
|
||||
public Integer sat;
|
||||
public String effect;
|
||||
public Integer ct;
|
||||
public String alert;
|
||||
public List<Double> xy;
|
||||
public Integer transitiontime;
|
||||
public Integer bri_inc;
|
||||
public Integer hue_inc;
|
||||
public Integer sat_inc;
|
||||
public List<Double> xy_inc;
|
||||
public Integer ct_inc;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Hue API response base type
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueResponse {
|
||||
public static final int UNAUTHORIZED = 1;
|
||||
public static final int INVALID_JSON = 2;
|
||||
public static final int NOT_AVAILABLE = 3;
|
||||
public static final int METHOD_NOT_ALLOWED = 4;
|
||||
public static final int ARGUMENTS_INVALID = 7;
|
||||
public static final int SENSOR_NOT_CLIP_SENSOR = 8;
|
||||
public static final int LINK_BUTTON_NOT_PRESSED = 101;
|
||||
public static final int INTERNAL_ERROR = 901;
|
||||
|
||||
public static final int RULE_ENGINE_FULL = 601; // The Rule Engine has reached its maximum capacity of 100 rules.
|
||||
public static final int CONDITION_ERROR = 607; // Rule conditions contain errors or operator combination is not
|
||||
// allowed
|
||||
public static final int ACTION_ERROR = 608; // Rule actions contain errors or multiple actions with the same
|
||||
// resource address.
|
||||
public static final int TOO_MANY_ITEMS = 11; // Too many items in the list (too many conditions or too many actions)
|
||||
|
||||
public final @Nullable HueErrorMessage error;
|
||||
public final @Nullable HueSuccessResponse success;
|
||||
|
||||
public HueResponse(HueErrorMessage error) {
|
||||
this.error = error;
|
||||
this.success = null;
|
||||
}
|
||||
|
||||
public HueResponse(HueSuccessResponse success) {
|
||||
this.error = null;
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public static class HueErrorMessage {
|
||||
public final String address;
|
||||
public final String description;
|
||||
public final int type;
|
||||
|
||||
public HueErrorMessage(int type, String address, String description) {
|
||||
this.type = type;
|
||||
this.address = address;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Hue API response base type
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueResponseSuccessSimple {
|
||||
public final String success;
|
||||
|
||||
public HueResponseSuccessSimple(String success) {
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
/**
|
||||
* This object describes the right hand side of "success".
|
||||
* The response looks like this:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "success":{
|
||||
* "id": "-the-id-"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueSuccessCreateGroup extends HueSuccessResponse {
|
||||
public String id;
|
||||
|
||||
public HueSuccessCreateGroup(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* This object describes the right hand side of "success".
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueSuccessGeneric extends HueSuccessResponse {
|
||||
public String message;
|
||||
public transient String key;
|
||||
|
||||
public HueSuccessGeneric(@Nullable Object message, String key) {
|
||||
this.message = message != null ? String.valueOf(message) : "";
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<HueSuccessGeneric> {
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public JsonElement serialize(HueSuccessGeneric product, Type type, JsonSerializationContext jsc) {
|
||||
JsonObject jObj = new JsonObject();
|
||||
jObj.addProperty(product.key, product.message);
|
||||
return jObj;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return !message.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
/**
|
||||
* Hue API response success base type
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueSuccessResponse {
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* This object describes the right hand side of "success".
|
||||
* The response looks like this:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "success":{
|
||||
* "username": "-the-username-"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class HueSuccessResponseCreateUser extends HueSuccessResponse {
|
||||
public String username;
|
||||
// For DTLS setup of the new hue entertain DTLS/UDP protocol
|
||||
// The PSK identity matches the “username”, and the PSK key matches the “clientkey”.
|
||||
public String clientkey;
|
||||
|
||||
public HueSuccessResponseCreateUser(String username, String clientkey) {
|
||||
this.username = username;
|
||||
this.clientkey = clientkey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dto.response;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
/**
|
||||
* This response object is a bit complicated. The response is required to look like this:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "success":{
|
||||
* "/1d49eeed-1fa7-434a-8e6c-70bb2cdc3e8f/lights/1/state/on": true
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* This object describes the right hand side of "success". The json key itself is the uri path
|
||||
* and the value is either a boolean, number or string.
|
||||
*
|
||||
* This is done with a custom serializer that creates the proper {@link JsonObject}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class HueSuccessResponseStateChanged extends HueSuccessResponse {
|
||||
private transient Object value;
|
||||
private transient String relURI;
|
||||
|
||||
public HueSuccessResponseStateChanged(String relURI, Object value) {
|
||||
this.relURI = relURI;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<HueSuccessResponseStateChanged> {
|
||||
@Override
|
||||
public JsonElement serialize(HueSuccessResponseStateChanged product, Type type, JsonSerializationContext jsc) {
|
||||
JsonObject jObj = new JsonObject();
|
||||
if (product.value instanceof Float) {
|
||||
jObj.addProperty(product.relURI, (Float) product.value);
|
||||
}
|
||||
if (product.value instanceof Double) {
|
||||
jObj.addProperty(product.relURI, (Double) product.value);
|
||||
}
|
||||
if (product.value instanceof Integer) {
|
||||
jObj.addProperty(product.relURI, (Integer) product.value);
|
||||
}
|
||||
if (product.value instanceof Boolean) {
|
||||
jObj.addProperty(product.relURI, (Boolean) product.value);
|
||||
}
|
||||
if (product.value instanceof String) {
|
||||
jObj.addProperty(product.relURI, (String) product.value);
|
||||
}
|
||||
return jObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueUnauthorizedConfig;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeRequest;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = {
|
||||
ConfigurationAccess.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class ConfigurationAccess {
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigurationAdmin configAdmin;
|
||||
|
||||
@GET
|
||||
@Path("config")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return the reduced configuration")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getReducedConfigApi() {
|
||||
return Response.ok(cs.gson.toJson(cs.ds.config, new TypeToken<HueUnauthorizedConfig>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return the full data store")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getAllApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/config")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return the configuration")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getFullConfigApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.config)).build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{username}/config")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return the reduced configuration")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response putFullConfigApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
final HueChangeRequest changes;
|
||||
changes = cs.gson.fromJson(body, HueChangeRequest.class);
|
||||
String devicename = changes.devicename;
|
||||
if (devicename != null) {
|
||||
cs.ds.config.devicename = devicename;
|
||||
}
|
||||
Boolean dhcp = changes.dhcp;
|
||||
if (dhcp != null) {
|
||||
cs.ds.config.dhcp = dhcp;
|
||||
}
|
||||
Boolean linkbutton = changes.linkbutton;
|
||||
if (linkbutton != null) {
|
||||
cs.setLinkbutton(linkbutton, cs.getConfig().createNewUserOnEveryEndpoint,
|
||||
cs.getConfig().temporarilyEmulateV1bridge);
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.config)).build();
|
||||
}
|
||||
|
||||
@Path("{username}/{var:.+}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response catchAll(@Context UriInfo uri) {
|
||||
HueResponse e = new HueResponse(
|
||||
new HueErrorMessage(HueResponse.INVALID_JSON, uri.getPath().replace("/api", ""), "Invalid request: "));
|
||||
String str = cs.gson.toJson(Collections.singleton(e), new TypeToken<List<?>>() {
|
||||
}.getType());
|
||||
return Response.status(404).entity(str).build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.events.ItemEventFactory;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.StateUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueNewLights;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeRequest;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Listens to the ItemRegistry for items that fulfill one of these criteria:
|
||||
* <ul>
|
||||
* <li>Type is any of SWITCH, DIMMER, COLOR, or Group
|
||||
* <li>The category is "ColorLight" for coloured lights or "Light" for switchables.
|
||||
* <li>The item is tagged, according to what is set with {@link #setFilterTags(Set, Set, Set)}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* A {@link HueLightEntry} instances is created for each found item.
|
||||
* Those are kept in the given {@link org.openhab.io.hueemulation.internal.dto.HueDataStore}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The HUE Rest API requires a unique string based ID for every listed light.
|
||||
* We are using item names here. Not all hue clients might be compatible with non
|
||||
* numeric Ics.ds. A solution could be an ItemMetaData provider and to store a
|
||||
* generated integer id for each item.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* </p>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
* @author Florian Schmidt - Removed base type restriction from Group items
|
||||
*/
|
||||
@Component(immediate = false, service = { LightsAndGroups.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class LightsAndGroups implements RegistryChangeListener<Item> {
|
||||
public static final String EXPOSE_AS_DEVICE_TAG = "huelight";
|
||||
private final Logger logger = LoggerFactory.getLogger(LightsAndGroups.class);
|
||||
private static final String ITEM_TYPE_GROUP = "Group";
|
||||
private static final Set<String> ALLOWED_ITEM_TYPES = Stream.of(CoreItemFactory.COLOR, CoreItemFactory.DIMMER,
|
||||
CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH, ITEM_TYPE_GROUP).collect(Collectors.toSet());
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL)
|
||||
protected volatile @Nullable EventPublisher eventPublisher;
|
||||
|
||||
/**
|
||||
* Registers to the {@link ItemRegistry} and enumerates currently existing items.
|
||||
*/
|
||||
@Activate
|
||||
protected void activate() {
|
||||
cs.ds.resetGroupsAndLights();
|
||||
|
||||
itemRegistry.removeRegistryChangeListener(this);
|
||||
itemRegistry.addRegistryChangeListener(this);
|
||||
|
||||
for (Item item : itemRegistry.getItems()) {
|
||||
added(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters from the {@link ItemRegistry}.
|
||||
*/
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
itemRegistry.removeRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void added(Item newElement) {
|
||||
if (!(newElement instanceof GenericItem)) {
|
||||
return;
|
||||
}
|
||||
GenericItem element = (GenericItem) newElement;
|
||||
|
||||
if (!(element instanceof GroupItem) && !ALLOWED_ITEM_TYPES.contains(element.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceType deviceType = StateUtils.determineTargetType(cs, element);
|
||||
if (deviceType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
|
||||
if (element instanceof GroupItem && !element.hasTag(EXPOSE_AS_DEVICE_TAG)) {
|
||||
GroupItem g = (GroupItem) element;
|
||||
HueGroupEntry group = new HueGroupEntry(g.getName(), g, deviceType);
|
||||
|
||||
// Restore group type and room class from tags
|
||||
for (String tag : g.getTags()) {
|
||||
if (tag.startsWith("huetype_")) {
|
||||
group.type = tag.split("huetype_")[1];
|
||||
} else if (tag.startsWith("hueroom_")) {
|
||||
group.roomclass = tag.split("hueroom_")[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Add group members
|
||||
group.lights = new ArrayList<>();
|
||||
for (Item item : g.getMembers()) {
|
||||
group.lights.add(cs.mapItemUIDtoHueID(item));
|
||||
}
|
||||
|
||||
cs.ds.groups.put(hueID, group);
|
||||
} else {
|
||||
HueLightEntry device = new HueLightEntry(element, cs.ds.config.uuid + "-" + hueID.toString(), deviceType);
|
||||
device.item = element;
|
||||
cs.ds.lights.put(hueID, device);
|
||||
updateGroup0();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The HUE API enforces a Group 0 that contains all lights.
|
||||
*/
|
||||
private void updateGroup0() {
|
||||
cs.ds.groups.get("0").lights = cs.ds.lights.keySet().stream().map(v -> String.valueOf(v))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removed(Item element) {
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
logger.debug("Remove item {}", hueID);
|
||||
cs.ds.lights.remove(hueID);
|
||||
cs.ds.groups.remove(hueID);
|
||||
updateGroup0();
|
||||
}
|
||||
|
||||
/**
|
||||
* The tags might have changed
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@Override
|
||||
public synchronized void updated(Item oldElement, Item newElement) {
|
||||
if (!(newElement instanceof GenericItem)) {
|
||||
return;
|
||||
}
|
||||
GenericItem element = (GenericItem) newElement;
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
|
||||
HueGroupEntry hueGroup = cs.ds.groups.get(hueID);
|
||||
if (hueGroup != null) {
|
||||
DeviceType t = StateUtils.determineTargetType(cs, element);
|
||||
if (t != null && element instanceof GroupItem) {
|
||||
hueGroup.updateItem((GroupItem) element);
|
||||
} else {
|
||||
cs.ds.groups.remove(hueID);
|
||||
}
|
||||
}
|
||||
|
||||
HueLightEntry hueDevice = cs.ds.lights.get(hueID);
|
||||
if (hueDevice == null) {
|
||||
// If the correct tags got added -> use the logic within added()
|
||||
added(element);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if type can still be determined (tags and category is still sufficient)
|
||||
DeviceType t = StateUtils.determineTargetType(cs, element);
|
||||
if (t == null) {
|
||||
removed(element);
|
||||
return;
|
||||
}
|
||||
|
||||
hueDevice.updateItem(element);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/lights")
|
||||
@ApiOperation(value = "Return all lights")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getAllLightsApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.lights)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/lights/new")
|
||||
@ApiOperation(value = "Return new lights since last scan. Returns an empty list for openHAB as we do not cache that information.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getNewLights(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(new HueNewLights())).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{username}/lights")
|
||||
@ApiOperation(value = "Starts a new scan for compatible items. This is usually not necessary, because we are observing the item registry.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewLights(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return NetworkUtils.singleSuccess(cs.gson, "Searching for new devices", "/lights");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/lights/{id}")
|
||||
@ApiOperation(value = "Return a light")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getLightApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "light id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.lights.get(id))).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@DELETE
|
||||
@Path("{username}/lights/{id}")
|
||||
@ApiOperation(value = "Deletes the item that is represented by this id")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The item got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeLightAPI(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueLightEntry hueDevice = cs.ds.lights.get(id);
|
||||
if (hueDevice == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Light does not exist");
|
||||
}
|
||||
|
||||
if (itemRegistry.remove(id) != null) {
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/lights/" + id + " deleted.");
|
||||
} else {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Light does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@PUT
|
||||
@Path("{username}/lights/{id}")
|
||||
@ApiOperation(value = "Rename a light")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response renameLightApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "light id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
HueLightEntry hueDevice = cs.ds.lights.get(id);
|
||||
if (hueDevice == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Light not existing");
|
||||
}
|
||||
|
||||
final HueChangeRequest changeRequest = cs.gson.fromJson(body, HueChangeRequest.class);
|
||||
|
||||
String name = changeRequest.name;
|
||||
if (name == null || name.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON, "Invalid request: No name set");
|
||||
}
|
||||
|
||||
hueDevice.item.setLabel(name);
|
||||
itemRegistry.update(hueDevice.item);
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, name, "/lights/" + id + "/name");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@PUT
|
||||
@Path("{username}/lights/{id}/state")
|
||||
@ApiOperation(value = "Set light state")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response setLightStateApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "light id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
HueLightEntry hueDevice = cs.ds.lights.get(id);
|
||||
if (hueDevice == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Light not existing");
|
||||
}
|
||||
|
||||
HueStateChange newState = cs.gson.fromJson(body, HueStateChange.class);
|
||||
if (newState == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No state change data received!");
|
||||
}
|
||||
|
||||
hueDevice.state = StateUtils.colorStateFromItemState(hueDevice.item.getState(), hueDevice.deviceType);
|
||||
|
||||
String itemUID = hueDevice.item.getUID();
|
||||
List<HueResponse> responses = new ArrayList<>();
|
||||
Command command = StateUtils.computeCommandByState(responses, "/lights/" + id + "/state", hueDevice.state,
|
||||
newState);
|
||||
|
||||
// If a command could be created, post it to the framework now
|
||||
if (command != null) {
|
||||
EventPublisher localEventPublisher = eventPublisher;
|
||||
if (localEventPublisher != null) {
|
||||
logger.debug("sending {} to {}", command, itemUID);
|
||||
localEventPublisher.post(ItemEventFactory.createCommandEvent(itemUID, command, "hueemulation"));
|
||||
} else {
|
||||
logger.warn("No event publisher. Cannot post item '{}' command!", itemUID);
|
||||
}
|
||||
hueDevice.lastCommand = command;
|
||||
hueDevice.lastHueChange = newState;
|
||||
}
|
||||
|
||||
return Response.ok(cs.gson.toJson(responses, new TypeToken<List<?>>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@PUT
|
||||
@Path("{username}/groups/{id}/action")
|
||||
@ApiOperation(value = "Initiate group action")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response setGroupActionApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "group id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
HueGroupEntry hueDevice = cs.ds.groups.get(id);
|
||||
GroupItem groupItem = hueDevice.groupItem;
|
||||
if (hueDevice == null || groupItem == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Group not existing");
|
||||
}
|
||||
|
||||
HueStateChange state = cs.gson.fromJson(body, HueStateChange.class);
|
||||
if (state == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No state change data received!");
|
||||
}
|
||||
|
||||
// First synchronize the internal state information with the framework
|
||||
hueDevice.action = StateUtils.colorStateFromItemState(groupItem.getState(), hueDevice.deviceType);
|
||||
|
||||
List<HueResponse> responses = new ArrayList<>();
|
||||
Command command = StateUtils.computeCommandByState(responses, "/groups/" + id + "/state/", hueDevice.action,
|
||||
state);
|
||||
|
||||
// If a command could be created, post it to the framework now
|
||||
if (command != null) {
|
||||
logger.debug("sending {} to {}", command, id);
|
||||
EventPublisher localEventPublisher = eventPublisher;
|
||||
if (localEventPublisher != null) {
|
||||
localEventPublisher
|
||||
.post(ItemEventFactory.createCommandEvent(groupItem.getUID(), command, "hueemulation"));
|
||||
} else {
|
||||
logger.warn("No event publisher. Cannot post item '{}' command!", groupItem.getUID());
|
||||
}
|
||||
}
|
||||
|
||||
return Response.ok(cs.gson.toJson(responses, new TypeToken<List<?>>() {
|
||||
}.getType())).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/groups")
|
||||
@ApiOperation(value = "Return all groups")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getAllGroupsApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.groups)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/groups/{id}")
|
||||
@ApiOperation(value = "Return a group")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getGroupApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "group id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.groups.get(id))).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@POST
|
||||
@Path("{username}/groups")
|
||||
@ApiOperation(value = "Create a new group")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewGroup(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueGroupEntry state = cs.gson.fromJson(body, HueGroupEntry.class);
|
||||
if (state == null || state.name.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No state change data received!");
|
||||
}
|
||||
|
||||
String groupid = cs.ds.nextGroupID();
|
||||
GroupItem groupItem = new GroupItem(groupid);
|
||||
|
||||
if (!HueGroupEntry.TypeEnum.LightGroup.name().equals(state.type)) {
|
||||
groupItem.addTag("huetype_" + state.type);
|
||||
}
|
||||
|
||||
if (HueGroupEntry.TypeEnum.Room.name().equals(state.type) && !state.roomclass.isEmpty()) {
|
||||
groupItem.addTag("hueroom_" + state.roomclass);
|
||||
}
|
||||
|
||||
List<Item> groupItems = new ArrayList<>();
|
||||
for (String id : state.lights) {
|
||||
Item item = itemRegistry.get(id);
|
||||
if (item == null) {
|
||||
logger.debug("Could not create group {}. Item {} not existing!", state.name, id);
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID,
|
||||
"Invalid request: Item not existing");
|
||||
}
|
||||
groupItem.addMember(item);
|
||||
}
|
||||
|
||||
itemRegistry.add(groupItem);
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, groupid, "id");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@DELETE
|
||||
@Path("{username}/groups/{id}")
|
||||
@ApiOperation(value = "Deletes the item that is represented by this id")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The item got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeGroupAPI(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueLightEntry hueDevice = cs.ds.lights.get(id);
|
||||
if (hueDevice == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Group does not exist");
|
||||
}
|
||||
|
||||
if (itemRegistry.remove(id) != null) {
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/groups/" + id + " deleted.");
|
||||
} else {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Group does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.util.ModuleBuilder;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.RuleUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Handles Hue rules via the automation subsystem and the corresponding REST interface
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { Rules.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class Rules implements RegistryChangeListener<Rule> {
|
||||
public static final String RULES_TAG = "hueemulation_rule";
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
|
||||
/**
|
||||
* Registers to the {@link RuleRegistry} and enumerates currently existing rules.
|
||||
*/
|
||||
@Activate
|
||||
public void activate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
ruleRegistry.addRegistryChangeListener(this);
|
||||
|
||||
for (Rule item : ruleRegistry.getAll()) {
|
||||
added(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(Rule rule) {
|
||||
if (!rule.getTags().contains(RULES_TAG)) {
|
||||
return;
|
||||
}
|
||||
HueRuleEntry entry = new HueRuleEntry(rule.getName());
|
||||
String desc = rule.getDescription();
|
||||
if (desc != null) {
|
||||
entry.description = desc;
|
||||
}
|
||||
|
||||
rule.getConditions().stream().filter(c -> c.getTypeUID().equals("hue.ruleCondition")).forEach(c -> {
|
||||
HueRuleEntry.Condition condition = c.getConfiguration().as(HueRuleEntry.Condition.class);
|
||||
// address with pattern "/sensors/2/state/buttonevent"
|
||||
String[] parts = condition.address.split("/");
|
||||
if (parts.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.conditions.add(condition);
|
||||
});
|
||||
|
||||
rule.getActions().stream().filter(a -> a.getTypeUID().equals("rules.HttpAction")).forEach(a -> {
|
||||
HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
|
||||
if (command == null) {
|
||||
return;
|
||||
}
|
||||
// Remove the "/api/{user}" part
|
||||
String[] parts = command.address.split("/");
|
||||
command.address = "/" + String.join("/", Arrays.copyOfRange(parts, 3, parts.length));
|
||||
entry.actions.add(command);
|
||||
});
|
||||
|
||||
cs.ds.rules.put(rule.getUID(), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Rule element) {
|
||||
cs.ds.rules.remove(element.getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Rule oldElement, Rule element) {
|
||||
removed(oldElement);
|
||||
added(element);
|
||||
}
|
||||
|
||||
protected static Map.Entry<Trigger, Condition> hueConditionToAutomation(String id, HueRuleEntry.Condition condition,
|
||||
ItemRegistry itemRegistry) {
|
||||
// pattern: "/sensors/2/state/buttonevent"
|
||||
String[] parts = condition.address.split("/");
|
||||
if (parts.length < 3) {
|
||||
throw new IllegalStateException("Condition address invalid: " + condition.address);
|
||||
}
|
||||
|
||||
final Configuration triggerConfig = new Configuration();
|
||||
|
||||
String itemName = parts[2];
|
||||
|
||||
Item item = itemRegistry.get(itemName);
|
||||
if (item == null) {
|
||||
throw new IllegalStateException("Item of address does not exist: " + itemName);
|
||||
}
|
||||
|
||||
triggerConfig.put("itemName", itemName);
|
||||
|
||||
// There might be multiple triggers for the same item. Due to the map, we are only creating one though
|
||||
|
||||
Trigger trigger = ModuleBuilder.createTrigger().withId(id).withTypeUID("core.ItemStateChangeTrigger")
|
||||
.withConfiguration(triggerConfig).build();
|
||||
|
||||
// Connect the outputs of the trigger with the inputs of the condition
|
||||
Map<String, String> inputs = new TreeMap<>();
|
||||
inputs.put("newState", id);
|
||||
inputs.put("oldState", id);
|
||||
|
||||
// Config for condition
|
||||
final Configuration conditionConfig = new Configuration();
|
||||
conditionConfig.put("operator", condition.operator.name());
|
||||
conditionConfig.put("address", condition.address);
|
||||
String value = condition.value;
|
||||
if (value != null) {
|
||||
conditionConfig.put("value", value);
|
||||
}
|
||||
|
||||
Condition conditon = ModuleBuilder.createCondition().withId(id + "-condition").withTypeUID("hue.ruleCondition")
|
||||
.withConfiguration(conditionConfig).withInputs(inputs).build();
|
||||
|
||||
return new AbstractMap.SimpleEntry<>(trigger, conditon);
|
||||
}
|
||||
|
||||
protected static RuleBuilder createHueRuleConditions(List<HueRuleEntry.Condition> hueConditions,
|
||||
RuleBuilder builder, List<Trigger> oldTriggers, List<Condition> oldConditions, ItemRegistry itemRegistry) {
|
||||
// Preserve all triggers, conditions that are not part of hue rules
|
||||
Map<String, Trigger> triggers = new TreeMap<>();
|
||||
triggers.putAll(oldTriggers.stream().filter(a -> !a.getTypeUID().equals("core.ItemStateChangeTrigger"))
|
||||
.collect(Collectors.toMap(e -> e.getId(), e -> e)));
|
||||
|
||||
Map<String, Condition> conditions = new TreeMap<>();
|
||||
conditions.putAll(oldConditions.stream().filter(a -> !a.getTypeUID().equals("hue.ruleCondition"))
|
||||
.collect(Collectors.toMap(e -> e.getId(), e -> e)));
|
||||
|
||||
for (HueRuleEntry.Condition condition : hueConditions) {
|
||||
String id = condition.address.replace("/", "-");
|
||||
Entry<Trigger, Condition> entry = hueConditionToAutomation(id, condition, itemRegistry);
|
||||
triggers.put(id, entry.getKey());
|
||||
conditions.put(id, entry.getValue());
|
||||
}
|
||||
|
||||
builder.withTriggers(new ArrayList<>(triggers.values())).withConditions(new ArrayList<>(conditions.values()));
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected static List<Action> createActions(String uid, List<HueCommand> hueActions, List<Action> oldActions,
|
||||
String apikey) {
|
||||
// Preserve all actions that are not "rules.HttpAction"
|
||||
List<Action> actions = new ArrayList<>(oldActions);
|
||||
actions.removeIf(a -> a.getTypeUID().equals("rules.HttpAction"));
|
||||
|
||||
for (HueCommand command : hueActions) {
|
||||
command.address = "/api/" + apikey + command.address;
|
||||
actions.add(RuleUtils.createHttpAction(command, command.address.replace("/", "-")));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/rules")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return all rules")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getRulesApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.rules)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/rules/{id}")
|
||||
@ApiOperation(value = "Return a rule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getRuleApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "rule id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.rules.get(id))).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{username}/rules/{id}")
|
||||
@ApiOperation(value = "Deletes a rule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeRuleApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "Rule to remove") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
|
||||
}
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/rules/" + id + " deleted.");
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{username}/rules/{id}")
|
||||
@ApiOperation(value = "Set rule attributes")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response modifyRuleApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "rule id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
final HueRuleEntry changeRequest = cs.gson.fromJson(body, HueRuleEntry.class);
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Rule does not exist!");
|
||||
}
|
||||
|
||||
RuleBuilder builder = RuleBuilder.create(rule);
|
||||
|
||||
String temp;
|
||||
|
||||
temp = changeRequest.name;
|
||||
if (!temp.isEmpty()) {
|
||||
builder.withName(changeRequest.name);
|
||||
}
|
||||
|
||||
temp = changeRequest.description;
|
||||
if (!temp.isEmpty()) {
|
||||
builder.withDescription(temp);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!changeRequest.actions.isEmpty()) {
|
||||
builder.withActions(createActions(rule.getUID(), changeRequest.actions, rule.getActions(), username));
|
||||
}
|
||||
if (!changeRequest.conditions.isEmpty()) {
|
||||
builder = createHueRuleConditions(changeRequest.conditions, builder, rule.getTriggers(),
|
||||
rule.getConditions(), itemRegistry);
|
||||
}
|
||||
|
||||
ruleRegistry.add(builder.build());
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.successList(cs.gson, Arrays.asList( //
|
||||
new HueSuccessGeneric(changeRequest.name, "/rules/" + id + "/name"), //
|
||||
new HueSuccessGeneric(changeRequest.description, "/rules/" + id + "/description"), //
|
||||
new HueSuccessGeneric(changeRequest.actions.toString(), "/rules/" + id + "/actions"), //
|
||||
new HueSuccessGeneric(changeRequest.conditions.toString(), "/rules/" + id + "/conditions") //
|
||||
));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null" })
|
||||
@POST
|
||||
@Path("{username}/rules")
|
||||
@ApiOperation(value = "Create a new rule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewRule(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueRuleEntry newRuleData = cs.gson.fromJson(body, HueRuleEntry.class);
|
||||
if (newRuleData == null || newRuleData.name.isEmpty() || newRuleData.actions.isEmpty()
|
||||
|| newRuleData.conditions.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No name or actions or conditons!");
|
||||
}
|
||||
|
||||
String uid = UUID.randomUUID().toString();
|
||||
RuleBuilder builder = RuleBuilder.create(uid).withName(newRuleData.name);
|
||||
|
||||
String description = newRuleData.description;
|
||||
if (description != null) {
|
||||
builder.withDescription(description);
|
||||
}
|
||||
|
||||
try {
|
||||
builder.withActions(createActions(uid, newRuleData.actions, Collections.emptyList(), username));
|
||||
builder = createHueRuleConditions(newRuleData.conditions, builder, Collections.emptyList(),
|
||||
Collections.emptyList(), itemRegistry);
|
||||
ruleRegistry.add(builder.withTags(RULES_TAG).build());
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, uid, "id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemNotFoundException;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.util.ModuleBuilder;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.StateUtils;
|
||||
import org.openhab.io.hueemulation.internal.automation.dto.ItemCommandActionConfig;
|
||||
import org.openhab.io.hueemulation.internal.dto.AbstractHueState;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSceneWithLightstates;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeSceneEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueStateChange;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Handles Hue scenes via the automation subsystem and the corresponding REST interface
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { Scenes.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class Scenes implements RegistryChangeListener<Rule> {
|
||||
private final Logger logger = LoggerFactory.getLogger(Scenes.class);
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
|
||||
/**
|
||||
* Registers to the {@link RuleRegistry} and enumerates currently existing rules.
|
||||
*/
|
||||
@Activate
|
||||
public void activate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
ruleRegistry.addRegistryChangeListener(this);
|
||||
|
||||
for (Rule item : ruleRegistry.getAll()) {
|
||||
added(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(Rule scene) {
|
||||
if (!scene.getTags().contains("scene")) {
|
||||
return;
|
||||
}
|
||||
HueSceneEntry entry = new HueSceneEntry(scene.getName());
|
||||
String desc = scene.getDescription();
|
||||
if (desc != null) {
|
||||
entry.description = desc;
|
||||
}
|
||||
|
||||
List<String> items = new ArrayList<>();
|
||||
|
||||
for (Action a : scene.getActions()) {
|
||||
if (!a.getTypeUID().equals("core.ItemCommandAction")) {
|
||||
continue;
|
||||
}
|
||||
ItemCommandActionConfig config = a.getConfiguration().as(ItemCommandActionConfig.class);
|
||||
Item item;
|
||||
try {
|
||||
item = itemRegistry.getItem(config.itemName);
|
||||
} catch (ItemNotFoundException e) {
|
||||
logger.warn("Rule {} is referring to a non existing item {}", scene.getName(), config.itemName);
|
||||
continue;
|
||||
}
|
||||
if (scene.getActions().size() == 1 && item instanceof GroupItem) {
|
||||
entry.type = HueSceneEntry.TypeEnum.GroupScene;
|
||||
entry.group = cs.mapItemUIDtoHueID(item);
|
||||
} else {
|
||||
items.add(cs.mapItemUIDtoHueID(item));
|
||||
}
|
||||
}
|
||||
|
||||
if (!items.isEmpty()) {
|
||||
entry.lights = items;
|
||||
}
|
||||
|
||||
cs.ds.scenes.put(scene.getUID(), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Rule element) {
|
||||
cs.ds.scenes.remove(element.getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Rule oldElement, Rule element) {
|
||||
removed(oldElement);
|
||||
added(element);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/scenes")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return all scenes")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getScenesApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.scenes)).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unused", "null" })
|
||||
@GET
|
||||
@Path("{username}/scenes/{id}")
|
||||
@ApiOperation(value = "Return a scene")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getSceneApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "scene id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
HueSceneEntry sceneEntry = cs.ds.scenes.get(id);
|
||||
if (sceneEntry == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
|
||||
}
|
||||
HueSceneWithLightstates s = new HueSceneWithLightstates(sceneEntry);
|
||||
for (String itemID : s.lights) {
|
||||
Item item;
|
||||
try {
|
||||
item = itemRegistry.getItem(itemID);
|
||||
} catch (ItemNotFoundException e) {
|
||||
logger.warn("Scene {} is referring to a non existing item {}", sceneEntry.name, itemID);
|
||||
continue;
|
||||
}
|
||||
AbstractHueState state = StateUtils.colorStateFromItemState(item.getState(), null);
|
||||
s.lightstates.put(cs.mapItemUIDtoHueID(item), state);
|
||||
}
|
||||
|
||||
return Response.ok(cs.gson.toJson(s)).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{username}/scenes/{id}")
|
||||
@ApiOperation(value = "Deletes a scene")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeSceneApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "Scene to remove") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
|
||||
}
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/scenes/" + id + " deleted.");
|
||||
}
|
||||
|
||||
protected static Action actionFromState(String itemID, State state) {
|
||||
final Configuration actionConfig = new Configuration();
|
||||
actionConfig.put("itemName", itemID);
|
||||
actionConfig.put("command", StateUtils.commandByItemState(state).toFullString());
|
||||
return ModuleBuilder.createAction().withId(itemID).withTypeUID("core.ItemCommandAction")
|
||||
.withConfiguration(actionConfig).build();
|
||||
}
|
||||
|
||||
protected static Action actionFromState(String itemID, Command command) {
|
||||
final Configuration actionConfig = new Configuration();
|
||||
actionConfig.put("itemName", itemID);
|
||||
actionConfig.put("command", command.toFullString());
|
||||
return ModuleBuilder.createAction().withId(itemID).withTypeUID("core.ItemCommandAction")
|
||||
.withConfiguration(actionConfig).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Either assigns a new name, description, lights to a scene or directly assign
|
||||
* a new light state for an entry to a scene
|
||||
*/
|
||||
@PUT
|
||||
@Path("{username}/scenes/{id}")
|
||||
@ApiOperation(value = "Set scene attributes")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response modifySceneApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "scene id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
final HueChangeSceneEntry changeRequest = cs.gson.fromJson(body, HueChangeSceneEntry.class);
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
|
||||
}
|
||||
|
||||
RuleBuilder builder = RuleBuilder.create(rule);
|
||||
|
||||
String temp = changeRequest.name;
|
||||
if (temp != null) {
|
||||
builder.withName(temp);
|
||||
}
|
||||
temp = changeRequest.description;
|
||||
if (temp != null) {
|
||||
builder.withDescription(temp);
|
||||
}
|
||||
|
||||
List<String> lights = changeRequest.lights;
|
||||
if (changeRequest.storelightstate && lights != null) {
|
||||
@SuppressWarnings("null")
|
||||
@NonNullByDefault({})
|
||||
List<Action> actions = lights.stream().map(itemID -> itemRegistry.get(itemID)).filter(Objects::nonNull)
|
||||
.map(item -> actionFromState(item.getUID(), item.getState())).collect(Collectors.toList());
|
||||
builder.withActions(actions);
|
||||
}
|
||||
Map<String, HueStateChange> lightStates = changeRequest.lightstates;
|
||||
if (changeRequest.storelightstate && lightStates != null) {
|
||||
List<Action> actions = new ArrayList<>(rule.getActions());
|
||||
for (Map.Entry<String, HueStateChange> entry : lightStates.entrySet()) {
|
||||
// Remove existing action
|
||||
actions.removeIf(action -> action.getId().equals(entry.getKey()));
|
||||
// Assign new action
|
||||
Command command = StateUtils.computeCommandByChangeRequest(entry.getValue());
|
||||
if (command == null) {
|
||||
logger.warn("Failed to compute command for {}", body);
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Cannot compute command!");
|
||||
}
|
||||
actions.add(actionFromState(entry.getKey(), command));
|
||||
}
|
||||
builder.withActions(actions);
|
||||
}
|
||||
|
||||
try {
|
||||
ruleRegistry.add(builder.build());
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.successList(cs.gson, Arrays.asList( //
|
||||
new HueSuccessGeneric(changeRequest.name, "/scenes/" + id + "/name"), //
|
||||
new HueSuccessGeneric(changeRequest.description, "/scenes/" + id + "/description"), //
|
||||
new HueSuccessGeneric(changeRequest.lights != null ? String.join(",", changeRequest.lights) : null,
|
||||
"/scenes/" + id + "/lights") //
|
||||
));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null" })
|
||||
@POST
|
||||
@Path("{username}/scenes")
|
||||
@ApiOperation(value = "Create a new scene")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewScene(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueSceneEntry newScheduleData = cs.gson.fromJson(body, HueSceneEntry.class);
|
||||
if (newScheduleData == null || newScheduleData.name.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No name or localtime!");
|
||||
}
|
||||
|
||||
String uid = UUID.randomUUID().toString();
|
||||
RuleBuilder builder = RuleBuilder.create(uid).withName(newScheduleData.name).withTags("scene");
|
||||
|
||||
if (!newScheduleData.description.isEmpty()) {
|
||||
builder.withDescription(newScheduleData.description);
|
||||
}
|
||||
|
||||
List<String> lights = newScheduleData.lights;
|
||||
if (lights != null) {
|
||||
List<Action> actions = new ArrayList<>();
|
||||
for (String itemID : lights) {
|
||||
Item item = itemRegistry.get(itemID);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
actions.add(actionFromState(cs.mapItemUIDtoHueID(item), item.getState()));
|
||||
}
|
||||
builder.withActions(actions);
|
||||
}
|
||||
String groupid = newScheduleData.group;
|
||||
if (groupid != null) {
|
||||
Item groupItem = itemRegistry.get(groupid);
|
||||
if (groupItem == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, "Group does not exist!");
|
||||
}
|
||||
List<Action> actions = Collections
|
||||
.singletonList(actionFromState(cs.mapItemUIDtoHueID(groupItem), groupItem.getState()));
|
||||
builder.withActions(actions);
|
||||
}
|
||||
|
||||
try {
|
||||
ruleRegistry.add(builder.build());
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, uid, "id");
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{username}/scenes/{id}/lightstates/{lightid}")
|
||||
@ApiOperation(value = "Set scene attributes")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response modifySceneLightStateApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "scene id") String id,
|
||||
@PathParam("lightid") @ApiParam(value = "light id") String lightid, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
final HueStateChange changeRequest = cs.gson.fromJson(body, HueStateChange.class);
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Scene does not exist!");
|
||||
}
|
||||
|
||||
RuleBuilder builder = RuleBuilder.create(rule);
|
||||
|
||||
List<Action> actions = new ArrayList<>(rule.getActions());
|
||||
// Remove existing action
|
||||
actions.removeIf(action -> action.getId().equals(lightid));
|
||||
// Assign new action
|
||||
Command command = StateUtils.computeCommandByChangeRequest(changeRequest);
|
||||
if (command == null) {
|
||||
logger.warn("Failed to compute command for {}", body);
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Cannot compute command!");
|
||||
}
|
||||
|
||||
actions.add(actionFromState(lightid, command));
|
||||
|
||||
builder.withActions(actions);
|
||||
|
||||
try {
|
||||
ruleRegistry.add(builder.build());
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.successList(cs.gson, Arrays.asList( //
|
||||
new HueSuccessGeneric(changeRequest.on, "/scenes/" + id + "/lightstates/" + lightid + "/on"), //
|
||||
new HueSuccessGeneric(changeRequest.hue, "/scenes/" + id + "/lightstates/" + lightid + "/hue"), //
|
||||
new HueSuccessGeneric(changeRequest.sat, "/scenes/" + id + "/lightstates/" + lightid + "/sat"), //
|
||||
new HueSuccessGeneric(changeRequest.bri, "/scenes/" + id + "/lightstates/" + lightid + "/bri"), //
|
||||
new HueSuccessGeneric(changeRequest.transitiontime,
|
||||
"/scenes/" + id + "/lightstates/" + lightid + "/transitiontime")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.automation.Action;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleManager;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.Visibility;
|
||||
import org.openhab.core.automation.util.ModuleBuilder;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.RuleUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueDataStore;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueScheduleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeScheduleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessGeneric;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Enables the schedule part of the Hue REST API. Uses automation rules with GenericCronTrigger, TimerTrigger and
|
||||
* AbsoluteDateTimeTrigger depending on the schedule time pattern.
|
||||
* <p>
|
||||
* If the scheduled task should remove itself after completion, a RemoveRuleAction is used in the rule.
|
||||
* <p>
|
||||
* The actual command execution uses HttpAction.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { Schedules.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class Schedules implements RegistryChangeListener<Rule> {
|
||||
public static final String SCHEDULE_TAG = "hueemulation_schedule";
|
||||
private final Logger logger = LoggerFactory.getLogger(Schedules.class);
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) RuleManager ruleManager;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
|
||||
/**
|
||||
* Registers to the {@link RuleRegistry} and enumerates currently existing rules.
|
||||
*/
|
||||
@Activate
|
||||
public void activate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
ruleRegistry.addRegistryChangeListener(this);
|
||||
|
||||
for (Rule item : ruleRegistry.getAll()) {
|
||||
added(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
ruleRegistry.removeRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the registry when a rule got added (and when a rule got modified).
|
||||
* <p>
|
||||
* Converts the rule into a {@link HueScheduleEntry} object and add that to the hue datastore.
|
||||
*/
|
||||
@Override
|
||||
public void added(Rule rule) {
|
||||
if (!rule.getTags().contains(SCHEDULE_TAG)) {
|
||||
return;
|
||||
}
|
||||
HueScheduleEntry entry = new HueScheduleEntry();
|
||||
entry.name = rule.getName();
|
||||
entry.description = rule.getDescription();
|
||||
entry.autodelete = rule.getActions().stream().anyMatch(p -> p.getId().equals("autodelete"));
|
||||
entry.status = ruleManager.isEnabled(rule.getUID()) ? "enabled" : "disabled";
|
||||
|
||||
String timeStringFromTrigger = RuleUtils.timeStringFromTrigger(rule.getTriggers());
|
||||
if (timeStringFromTrigger == null) {
|
||||
logger.warn("Schedule from rule '{}' invalid!", rule.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
entry.localtime = timeStringFromTrigger;
|
||||
|
||||
for (Action a : rule.getActions()) {
|
||||
if (!a.getTypeUID().equals("rules.HttpAction")) {
|
||||
continue;
|
||||
}
|
||||
HueCommand command = RuleUtils.httpActionToHueCommand(cs.ds, a, rule.getName());
|
||||
if (command == null) {
|
||||
continue;
|
||||
}
|
||||
entry.command = command;
|
||||
}
|
||||
|
||||
cs.ds.schedules.put(rule.getUID(), entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removed(Rule element) {
|
||||
cs.ds.schedules.remove(element.getUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Rule oldElement, Rule element) {
|
||||
removed(oldElement);
|
||||
added(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new rule that executes a http rule action, triggered by the scheduled time
|
||||
*
|
||||
* @param uid A rule unique id.
|
||||
* @param builder A rule builder that will be used for creating the rule. It must have been created with the given
|
||||
* uid.
|
||||
* @param oldActions Old actions. Useful if `data` is only partially set and old actions should be preserved
|
||||
* @param data The configuration for the http action and trigger time is in here
|
||||
* @return A new rule with the given uid
|
||||
* @throws IllegalStateException If a required parameter is not set or if a light / group that is referred to is not
|
||||
* existing
|
||||
*/
|
||||
protected static Rule createRule(String uid, RuleBuilder builder, List<Action> oldActions,
|
||||
List<Trigger> oldTriggers, HueChangeScheduleEntry data, HueDataStore ds) throws IllegalStateException {
|
||||
HueCommand command = data.command;
|
||||
Boolean autodelete = data.autodelete;
|
||||
|
||||
String temp;
|
||||
|
||||
temp = data.name;
|
||||
if (temp != null) {
|
||||
builder.withName(temp);
|
||||
} else if (oldActions.isEmpty()) { // This is a new rule without a name yet
|
||||
throw new IllegalStateException("Name not set!");
|
||||
}
|
||||
|
||||
temp = data.description;
|
||||
if (temp != null) {
|
||||
builder.withDescription(temp);
|
||||
}
|
||||
|
||||
temp = data.localtime;
|
||||
if (temp != null) {
|
||||
builder.withTriggers(RuleUtils.createTriggerForTimeString(temp));
|
||||
} else if (oldTriggers.isEmpty()) { // This is a new rule without triggers yet
|
||||
throw new IllegalStateException("localtime not set!");
|
||||
}
|
||||
|
||||
List<Action> actions = new ArrayList<>(oldActions);
|
||||
|
||||
if (command != null) {
|
||||
RuleUtils.validateHueHttpAddress(ds, command.address);
|
||||
actions.removeIf(a -> a.getId().equals("command")); // Remove old command action if any and add new one
|
||||
actions.add(RuleUtils.createHttpAction(command, "command"));
|
||||
} else if (oldActions.isEmpty()) { // This is a new rule without an action yet
|
||||
throw new IllegalStateException("No command set!");
|
||||
}
|
||||
|
||||
if (autodelete != null) {
|
||||
// Remove action to remove rule after execution
|
||||
actions = actions.stream().filter(e -> !e.getId().equals("autodelete"))
|
||||
.collect(Collectors.toCollection(() -> new ArrayList<>()));
|
||||
if (autodelete) { // Add action to remove this rule again after execution
|
||||
final Configuration actionConfig = new Configuration();
|
||||
actionConfig.put("removeuid", uid);
|
||||
actions.add(ModuleBuilder.createAction().withId("autodelete").withTypeUID("rules.RemoveRuleAction")
|
||||
.withConfiguration(actionConfig).build());
|
||||
}
|
||||
}
|
||||
|
||||
builder.withActions(actions);
|
||||
|
||||
return builder.withVisibility(Visibility.VISIBLE).withTags(SCHEDULE_TAG).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/schedules")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@ApiOperation(value = "Return all schedules")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getSchedulesApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.schedules)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/schedules/{id}")
|
||||
@ApiOperation(value = "Return a schedule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getScheduleApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "schedule id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.schedules.get(id))).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{username}/schedules/{id}")
|
||||
@ApiOperation(value = "Deletes a schedule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeScheduleApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "Schedule to remove") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
|
||||
}
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/schedules/" + id + " deleted.");
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{username}/schedules/{id}")
|
||||
@ApiOperation(value = "Set schedule attributes")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response modifyScheduleApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "schedule id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
final HueChangeScheduleEntry changeRequest = cs.gson.fromJson(body, HueChangeScheduleEntry.class);
|
||||
|
||||
Rule rule = ruleRegistry.remove(id);
|
||||
if (rule == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Schedule does not exist!");
|
||||
}
|
||||
|
||||
RuleBuilder builder = RuleBuilder.create(rule);
|
||||
|
||||
try {
|
||||
ruleRegistry.add(
|
||||
createRule(rule.getUID(), builder, rule.getActions(), rule.getTriggers(), changeRequest, cs.ds));
|
||||
} catch (IllegalStateException e) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
return NetworkUtils.successList(cs.gson, Arrays.asList( //
|
||||
new HueSuccessGeneric(changeRequest.name, "/schedules/" + id + "/name"), //
|
||||
new HueSuccessGeneric(changeRequest.description, "/schedules/" + id + "/description"), //
|
||||
new HueSuccessGeneric(changeRequest.localtime, "/schedules/" + id + "/localtime"), //
|
||||
new HueSuccessGeneric(changeRequest.status, "/schedules/" + id + "/status"), //
|
||||
new HueSuccessGeneric(changeRequest.autodelete, "/schedules/1/autodelete"), //
|
||||
new HueSuccessGeneric(changeRequest.command, "/schedules/1/command") //
|
||||
));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null" })
|
||||
@POST
|
||||
@Path("{username}/schedules")
|
||||
@ApiOperation(value = "Create a new schedule")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewSchedule(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueScheduleEntry newScheduleData = cs.gson.fromJson(body, HueScheduleEntry.class);
|
||||
if (newScheduleData == null || newScheduleData.name.isEmpty() || newScheduleData.localtime.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No name or localtime!");
|
||||
}
|
||||
|
||||
String uid = UUID.randomUUID().toString();
|
||||
RuleBuilder builder = RuleBuilder.create(uid);
|
||||
|
||||
Rule rule;
|
||||
try {
|
||||
rule = createRule(uid, builder, Collections.emptyList(), Collections.emptyList(), newScheduleData, cs.ds);
|
||||
} catch (IllegalStateException e) { // No stacktrace required, we just need the exception message
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.ARGUMENTS_INVALID, e.getMessage());
|
||||
}
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, uid, "id");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueNewLights;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSensorEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeRequest;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Listens to the ItemRegistry and add all DecimalType, OnOffType, ContactType, DimmerType items
|
||||
* as sensors.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { Sensors.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class Sensors implements RegistryChangeListener<Item> {
|
||||
private final Logger logger = LoggerFactory.getLogger(Sensors.class);
|
||||
private static final Set<String> ALLOWED_ITEM_TYPES = Stream.of(CoreItemFactory.COLOR, CoreItemFactory.DIMMER,
|
||||
CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.SWITCH, CoreItemFactory.CONTACT, CoreItemFactory.NUMBER)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UserManagement userManagement;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
|
||||
/**
|
||||
* Registers to the {@link ItemRegistry} and enumerates currently existing items.
|
||||
* Call {@link #close(ItemRegistry)} when you are done with this object.
|
||||
*
|
||||
* Only call this after you have set the filter tags with {@link #setFilterTags(Set, Set, Set)}.
|
||||
*/
|
||||
@Activate
|
||||
protected void activate() {
|
||||
cs.ds.resetSensors();
|
||||
|
||||
itemRegistry.removeRegistryChangeListener(this);
|
||||
itemRegistry.addRegistryChangeListener(this);
|
||||
|
||||
for (Item item : itemRegistry.getItems()) {
|
||||
added(item);
|
||||
}
|
||||
logger.debug("Added as sensor: {}",
|
||||
cs.ds.sensors.values().stream().map(l -> l.name).collect(Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters from the {@link ItemRegistry}.
|
||||
*/
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
itemRegistry.removeRegistryChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void added(Item newElement) {
|
||||
if (!(newElement instanceof GenericItem)) {
|
||||
return;
|
||||
}
|
||||
GenericItem element = (GenericItem) newElement;
|
||||
|
||||
if (!ALLOWED_ITEM_TYPES.contains(element.getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
|
||||
HueSensorEntry sensor = new HueSensorEntry(element);
|
||||
cs.ds.sensors.put(hueID, sensor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removed(Item element) {
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
logger.debug("Remove item {}", hueID);
|
||||
cs.ds.sensors.remove(hueID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void updated(Item oldElement, Item newElement) {
|
||||
if (!(newElement instanceof GenericItem)) {
|
||||
return;
|
||||
}
|
||||
GenericItem element = (GenericItem) newElement;
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(element);
|
||||
|
||||
HueSensorEntry sensor = new HueSensorEntry(element);
|
||||
cs.ds.sensors.put(hueID, sensor);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/sensors")
|
||||
@ApiOperation(value = "Return all sensors")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getAllSensorsApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.sensors)).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/sensors/new")
|
||||
@ApiOperation(value = "Return new sensors since last scan. Returns an empty list for openHAB as we do not cache that information.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getNewSensors(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(new HueNewLights())).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{username}/sensors")
|
||||
@ApiOperation(value = "Starts a new scan for compatible items. This is usually not necessary, because we are observing the item registry.")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response postNewLights(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return NetworkUtils.singleSuccess(cs.gson, "Searching for new sensors", "/sensors");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/sensors/{id}")
|
||||
@ApiOperation(value = "Return a sensor")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getSensorApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "sensor id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.sensors.get(id))).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@GET
|
||||
@Path("{username}/sensors/{id}/config")
|
||||
@ApiOperation(value = "Return a sensor config. Always empty")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getSensorConfigApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "sensor id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueSensorEntry sensor = cs.ds.sensors.get(id);
|
||||
if (sensor == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Sensor does not exist");
|
||||
}
|
||||
|
||||
return Response.ok(cs.gson.toJson(sensor.config)).build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@DELETE
|
||||
@Path("{username}/sensors/{id}")
|
||||
@ApiOperation(value = "Deletes the sensor that is represented by this id")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The item got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeSensorAPI(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "id") String id) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
HueSensorEntry sensor = cs.ds.sensors.get(id);
|
||||
if (sensor == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Sensor does not exist");
|
||||
}
|
||||
|
||||
if (itemRegistry.remove(id) != null) {
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/sensors/" + id + " deleted.");
|
||||
} else {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Sensor does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
@PUT
|
||||
@Path("{username}/sensors/{id}")
|
||||
@ApiOperation(value = "Rename a sensor")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response renameLightApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "light id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
HueSensorEntry sensor = cs.ds.sensors.get(id);
|
||||
if (sensor == null) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.NOT_AVAILABLE, "Sensor not existing");
|
||||
}
|
||||
|
||||
final HueChangeRequest changeRequest = cs.gson.fromJson(body, HueChangeRequest.class);
|
||||
|
||||
String name = changeRequest.name;
|
||||
if (name == null || name.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON, "Invalid request: No name set");
|
||||
}
|
||||
|
||||
sensor.item.setLabel(name);
|
||||
itemRegistry.update(sensor.item);
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, name, "/sensors/" + id + "/name");
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("{username}/sensors/{id}/state")
|
||||
@ApiOperation(value = "Set sensor state")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response setSensorStateApi(@Context UriInfo uri, //
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "sensor id") String id, String body) {
|
||||
if (!userManagement.authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.SENSOR_NOT_CLIP_SENSOR,
|
||||
"Invalid request: Not a clip sensor");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.jupnp.UpnpService;
|
||||
import org.jupnp.controlpoint.ControlPoint;
|
||||
import org.jupnp.model.meta.Device;
|
||||
import org.jupnp.model.meta.DeviceDetails;
|
||||
import org.jupnp.model.meta.LocalDevice;
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.jupnp.registry.Registry;
|
||||
import org.jupnp.registry.RegistryListener;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.upnp.UpnpServer;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.component.annotations.ReferenceCardinality;
|
||||
import org.osgi.service.component.annotations.ReferencePolicyOption;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class is used by the status REST API for troubleshoot purposes.
|
||||
* <p>
|
||||
* The UPNP announcement is tested, the /description.xml reachability is checked,
|
||||
* and some statistics are gathered.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { StatusResource.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@Path("")
|
||||
@NonNullByDefault
|
||||
public class StatusResource implements RegistryListener {
|
||||
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
|
||||
protected @Nullable UpnpServer discovery;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) UpnpService upnpService;
|
||||
|
||||
private enum upnpStatus {
|
||||
service_not_registered,
|
||||
service_registered_but_no_UPnP_traffic_yet,
|
||||
upnp_announcement_thread_not_running,
|
||||
any_device_but_not_this_one_found,
|
||||
success
|
||||
}
|
||||
|
||||
private upnpStatus selfTestUpnpFound = upnpStatus.service_not_registered;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(StatusResource.class);
|
||||
|
||||
/**
|
||||
* This will register to the {@link UpnpService} registry to get notified of new UPNP devices and check all existing
|
||||
* devices if they match our UPNP announcement.
|
||||
*/
|
||||
public void startUpnpSelfTest() {
|
||||
Registry registry = upnpService.getRegistry();
|
||||
if (registry == null) {
|
||||
logger.warn("upnp service registry is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
selfTestUpnpFound = upnpStatus.service_registered_but_no_UPnP_traffic_yet;
|
||||
|
||||
for (RemoteDevice device : registry.getRemoteDevices()) {
|
||||
remoteDeviceAdded(registry, device);
|
||||
}
|
||||
for (LocalDevice device : registry.getLocalDevices()) {
|
||||
localDeviceAdded(registry, device);
|
||||
}
|
||||
|
||||
registry.addListener(this);
|
||||
|
||||
ControlPoint controlPoint = upnpService.getControlPoint();
|
||||
if (controlPoint != null) {
|
||||
controlPoint.search();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("status/link")
|
||||
@Produces("text/plain")
|
||||
public Response link(@Context UriInfo uri) {
|
||||
cs.setLinkbutton(!cs.ds.config.linkbutton, true, uri.getQueryParameters().containsKey("v1"));
|
||||
URI newuri = uri.getBaseUri().resolve("/api/status");
|
||||
return Response.seeOther(newuri).build();
|
||||
}
|
||||
|
||||
private static String toYesNo(boolean b) {
|
||||
return b ? "yes" : "no";
|
||||
}
|
||||
|
||||
private static String TD(String s) {
|
||||
return "<td>" + s + "</td>";
|
||||
}
|
||||
|
||||
private static String TR(String s) {
|
||||
return "<tr>" + s + "</tr>";
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("status")
|
||||
@Produces("text/html")
|
||||
public String getStatus() {
|
||||
UpnpServer localDiscovery = discovery;
|
||||
if (localDiscovery == null) { // Optional service wiring
|
||||
return "UPnP Server service not started!";
|
||||
}
|
||||
|
||||
String format = "<html><body><h1>Self test</h1>" + //
|
||||
"<p>To access any links you need be in pairing mode!</p>" + //
|
||||
"<p>Pairing mode: %s (%s) <a href='%s/api/status/link'>Enable</a> | <a href='%s/api/status/link?V1=true'>Enable with bridge V1 emulation</a></p>"
|
||||
+ //
|
||||
"%d published lights (see <a href='%s/api/testuser/lights'>%s/api/testuser/lights</a>)<br>" + //
|
||||
"%d published sensors (see <a href='%s/api/testuser/sensors'>%s/api/testuser/sensors</a>)<br>" + //
|
||||
"<h2>UPnP discovery test</h2>" + //
|
||||
"<p>%s</p>" + //
|
||||
"<table style='border:1px solid black'><tr><td>serial no</td><td>name</td></tr>%s</table>" + //
|
||||
"<h2>Reachability test</h2>" + //
|
||||
"<table style='border:1px solid black'><tr><td>URL</td><td>Responds?</td><td>Ours?</td></tr>%s</table>"
|
||||
+ //
|
||||
"<h2>Users</h2><ul>%s</ul></body></html>";
|
||||
|
||||
String users = cs.ds.config.whitelist.entrySet().stream().map(user -> "<li>" + user.getKey() + " <b>"
|
||||
+ user.getValue().name + "</b> <small>" + user.getValue().lastUseDate + "</small>")
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
String url = "http://" + cs.ds.config.ipaddress + ":" + String.valueOf(localDiscovery.getDefaultport());
|
||||
|
||||
String reachable = localDiscovery.selfTests().stream()
|
||||
.map(entry -> TR(TD(entry.address) + TD(toYesNo(entry.reachable)) + TD(toYesNo(entry.isOurs))))
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
Registry registry = upnpService.getRegistry();
|
||||
String upnps;
|
||||
if (registry != null) {
|
||||
upnps = registry.getRemoteDevices().stream().map(device -> getDetails(device))
|
||||
.map(details -> TR(TD(details.getSerialNumber()) + TD(details.getFriendlyName())))
|
||||
.collect(Collectors.joining("\n"));
|
||||
} else {
|
||||
upnps = TR(TD("service not available") + TD(""));
|
||||
}
|
||||
|
||||
if (!localDiscovery.upnpAnnouncementThreadRunning()) {
|
||||
selfTestUpnpFound = upnpStatus.upnp_announcement_thread_not_running;
|
||||
}
|
||||
|
||||
return String.format(format, cs.ds.config.linkbutton ? "On" : "Off",
|
||||
cs.getConfig().temporarilyEmulateV1bridge ? "V1" : "V2", url, url, //
|
||||
cs.ds.lights.size(), url, url, cs.ds.sensors.size(), url, url, //
|
||||
selfTestUpnpFound.name().replace('_', ' '), //
|
||||
upnps, reachable, users);
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) {
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
|
||||
if (selfTestUpnpFound == upnpStatus.success) {
|
||||
return;
|
||||
}
|
||||
checkForDevice(getDetails(device));
|
||||
}
|
||||
|
||||
private static DeviceDetails getDetails(@Nullable Device<?, ?, ?> device) {
|
||||
if (device != null) {
|
||||
DeviceDetails details = device.getDetails();
|
||||
if (details != null) {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
return new DeviceDetails(null, "", null, null, "", null, null, null, null, null);
|
||||
}
|
||||
|
||||
private void checkForDevice(DeviceDetails details) {
|
||||
selfTestUpnpFound = upnpStatus.any_device_but_not_this_one_found;
|
||||
try {
|
||||
if (cs.ds.config.bridgeid.equals(details.getSerialNumber())) {
|
||||
selfTestUpnpFound = upnpStatus.success;
|
||||
}
|
||||
} catch (Exception e) { // We really don't want the service to fail on any exception
|
||||
logger.warn("upnp service: adding services failed: {}", details.getFriendlyName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void remoteDeviceUpdated(Registry registry, RemoteDevice device) {
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
|
||||
UpnpServer localDiscovery = discovery;
|
||||
if (selfTestUpnpFound != upnpStatus.success || localDiscovery == null
|
||||
|| localDiscovery.upnpAnnouncementThreadRunning()) {
|
||||
return;
|
||||
}
|
||||
DeviceDetails details = getDetails(device);
|
||||
String serialNo = details.getSerialNumber();
|
||||
if (cs.ds.config.bridgeid.equals(serialNo)) {
|
||||
selfTestUpnpFound = upnpStatus.any_device_but_not_this_one_found;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void localDeviceAdded(Registry registry, LocalDevice device) {
|
||||
UpnpServer localDiscovery = discovery;
|
||||
if (selfTestUpnpFound == upnpStatus.success || localDiscovery == null
|
||||
|| localDiscovery.upnpAnnouncementThreadRunning()) {
|
||||
return;
|
||||
}
|
||||
checkForDevice(getDetails(device));
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void localDeviceRemoved(Registry registry, LocalDevice device) {
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public void beforeShutdown(Registry registry) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterShutdown() {
|
||||
selfTestUpnpFound = upnpStatus.service_not_registered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.common.registry.DefaultAbstractManagedProvider;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.NetworkUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueUserAuth;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueUserAuthWithSecrets;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCreateUser;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseCreateUser;
|
||||
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.reflect.TypeToken;
|
||||
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import io.swagger.annotations.ApiResponse;
|
||||
import io.swagger.annotations.ApiResponses;
|
||||
|
||||
/**
|
||||
* Manages users of this emulated HUE bridge. Stores users in the frameworks storage backend.
|
||||
* <p>
|
||||
* This is an OSGi component. Usage:
|
||||
*
|
||||
* <pre>
|
||||
* @Reference
|
||||
* UserManagement userManagment;
|
||||
* </pre>
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@Component(immediate = false, service = { UserManagement.class }, property = "com.eclipsesource.jaxrs.publish=false")
|
||||
@NonNullByDefault
|
||||
@Path("")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public class UserManagement extends DefaultAbstractManagedProvider<HueUserAuthWithSecrets, String> {
|
||||
private final Logger logger = LoggerFactory.getLogger(UserManagement.class);
|
||||
|
||||
protected final ConfigStore cs;
|
||||
|
||||
@Activate
|
||||
public UserManagement(final @Reference StorageService storageService, final @Reference ConfigStore cs) {
|
||||
super(storageService);
|
||||
this.cs = cs;
|
||||
|
||||
for (HueUserAuthWithSecrets userAuth : getAll()) {
|
||||
cs.ds.config.whitelist.put(userAuth.getUID(), userAuth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the username exists in the whitelist
|
||||
*/
|
||||
@SuppressWarnings("null")
|
||||
public boolean authorizeUser(String userName) {
|
||||
HueUserAuth userAuth = cs.ds.config.whitelist.get(userName);
|
||||
|
||||
if (cs.ds.config.linkbutton && cs.ds.config.createNewUserOnEveryEndpoint) {
|
||||
addUser(userName, userName, "On-the-go-user");
|
||||
userAuth = cs.ds.config.whitelist.get(userName);
|
||||
}
|
||||
|
||||
if (userAuth != null) {
|
||||
userAuth.lastUseDate = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
update((HueUserAuthWithSecrets) userAuth);
|
||||
}
|
||||
|
||||
return userAuth != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a user to the whitelist and persist the user file
|
||||
*
|
||||
* @param apiKey The hue "username" which is actually an API key
|
||||
* @param clientKey The UDP/DTLS client key
|
||||
* @param The user visible name
|
||||
*/
|
||||
private void addUser(String apiKey, String clientKey, String label) {
|
||||
if (cs.ds.config.whitelist.containsKey(apiKey)) {
|
||||
return;
|
||||
}
|
||||
logger.debug("APIKey {} added", apiKey);
|
||||
String l[] = label.split("#");
|
||||
HueUserAuthWithSecrets hueUserAuth = new HueUserAuthWithSecrets(l[0], l.length == 2 ? l[1] : "openhab", apiKey,
|
||||
clientKey);
|
||||
cs.ds.config.whitelist.put(apiKey, hueUserAuth);
|
||||
add(hueUserAuth);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
private synchronized void removeUser(String apiKey) {
|
||||
HueUserAuth userAuth = cs.ds.config.whitelist.remove(apiKey);
|
||||
if (userAuth != null) {
|
||||
logger.debug("APIKey {} removed", apiKey);
|
||||
}
|
||||
remove(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getStorageName() {
|
||||
return "hueEmulationUsers";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String keyToString(String key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
@GET
|
||||
public Response illegalGetUserAccessApi(@Context UriInfo uri) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.METHOD_NOT_ALLOWED, "Not Authorized");
|
||||
}
|
||||
|
||||
@POST
|
||||
@ApiOperation(value = "Create an API Key")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "API Key created"),
|
||||
@ApiResponse(code = 403, message = "Link button not pressed") })
|
||||
public Response createNewUser(@Context UriInfo uri, String body) {
|
||||
if (!cs.ds.config.linkbutton) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.LINK_BUTTON_NOT_PRESSED,
|
||||
"link button not pressed");
|
||||
}
|
||||
|
||||
final HueCreateUser userRequest;
|
||||
userRequest = cs.gson.fromJson(body, HueCreateUser.class);
|
||||
if (userRequest.devicetype.isEmpty()) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.INVALID_JSON,
|
||||
"Invalid request: No devicetype set");
|
||||
}
|
||||
|
||||
String apiKey = UUID.randomUUID().toString();
|
||||
String clientKey = UUID.randomUUID().toString();
|
||||
addUser(apiKey, clientKey, userRequest.devicetype);
|
||||
HueSuccessResponseCreateUser h = new HueSuccessResponseCreateUser(apiKey, clientKey);
|
||||
String result = cs.gson.toJson(Collections.singleton(new HueResponse(h)), new TypeToken<List<?>>() {
|
||||
}.getType());
|
||||
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/config/whitelist/{userid}")
|
||||
@ApiOperation(value = "Return a user")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getUserApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("userid") @ApiParam(value = "User ID") String userid) {
|
||||
if (!authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.config.whitelist.get(userid))).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{username}/config/whitelist")
|
||||
@ApiOperation(value = "Return all users")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
|
||||
public Response getAllUsersApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username) {
|
||||
if (!authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
return Response.ok(cs.gson.toJson(cs.ds.config.whitelist)).build();
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{username}/config/whitelist/{id}")
|
||||
@ApiOperation(value = "Deletes a user")
|
||||
@ApiResponses(value = { @ApiResponse(code = 200, message = "The user got removed"),
|
||||
@ApiResponse(code = 403, message = "Access denied") })
|
||||
public Response removeUserApi(@Context UriInfo uri,
|
||||
@PathParam("username") @ApiParam(value = "username") String username,
|
||||
@PathParam("id") @ApiParam(value = "User to remove") String id) {
|
||||
if (!authorizeUser(username)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED, "Not Authorized");
|
||||
}
|
||||
if (!username.equals(id)) {
|
||||
return NetworkUtils.singleError(cs.gson, uri, HueResponse.UNAUTHORIZED,
|
||||
"You can only remove yourself not someone else!");
|
||||
}
|
||||
|
||||
removeUser(username);
|
||||
|
||||
return NetworkUtils.singleSuccess(cs.gson, "/config/whitelist/" + username + " deleted.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.upnp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.Selector;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.io.hueemulation.internal.HueEmulationConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The upnp server runtime configuration. Based on a {@link HueEmulationConfig} and determined ip address and port.
|
||||
* This extends {@link Thread}, because a runtime configuration is always valid for exactly one thread to be started
|
||||
* once.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class HueEmulationConfigWithRuntime extends Thread implements Runnable {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(HueEmulationConfigWithRuntime.class);
|
||||
|
||||
final @NonNullByDefault({}) HueEmulationConfig config;
|
||||
final InetAddress address;
|
||||
final String addressString;
|
||||
final InetAddress multicastAddress;
|
||||
int port;
|
||||
|
||||
final CompletableFuture<@Nullable HueEmulationConfigWithRuntime> future = new CompletableFuture<>();
|
||||
final Consumer<HueEmulationConfigWithRuntime> r;
|
||||
|
||||
// IO
|
||||
public @Nullable Selector asyncIOselector;
|
||||
private boolean hasAlreadyBeenStarted = false;
|
||||
|
||||
HueEmulationConfigWithRuntime(Consumer<HueEmulationConfigWithRuntime> r, HueEmulationConfig config,
|
||||
String addrString, InetAddress MULTI_ADDR_IPV4, InetAddress MULTI_ADDR_IPV6) throws UnknownHostException {
|
||||
super("HueEmulation UPNP Server");
|
||||
this.r = r;
|
||||
this.config = config;
|
||||
|
||||
address = InetAddress.getByName(addrString);
|
||||
if (address instanceof Inet6Address) {
|
||||
addressString = "[" + address.getHostAddress().split("%")[0] + "]";
|
||||
multicastAddress = MULTI_ADDR_IPV6;
|
||||
} else {
|
||||
addressString = address.getHostAddress();
|
||||
multicastAddress = MULTI_ADDR_IPV4;
|
||||
}
|
||||
|
||||
port = config.discoveryHttpPort == 0 ? Integer.getInteger("org.osgi.service.http.port", 8080)
|
||||
: config.discoveryHttpPort;
|
||||
}
|
||||
|
||||
HueEmulationConfigWithRuntime(Consumer<HueEmulationConfigWithRuntime> r, @Nullable HueEmulationConfig config,
|
||||
InetAddress MULTI_ADDR_IPV4, InetAddress MULTI_ADDR_IPV6) throws UnknownHostException {
|
||||
super("HueEmulation UPNP Server");
|
||||
this.r = r;
|
||||
this.config = config;
|
||||
|
||||
address = InetAddress.getByName("localhost");
|
||||
if (address instanceof Inet6Address) {
|
||||
addressString = "[" + address.getHostAddress().split("%")[0] + "]";
|
||||
multicastAddress = MULTI_ADDR_IPV6;
|
||||
} else {
|
||||
addressString = address.getHostAddress();
|
||||
multicastAddress = MULTI_ADDR_IPV4;
|
||||
}
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
String getMulticastAddress() {
|
||||
if (multicastAddress instanceof Inet6Address) {
|
||||
return "[" + multicastAddress.getHostAddress().split("%")[0] + "]";
|
||||
} else {
|
||||
return multicastAddress.getHostAddress();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized CompletableFuture<@Nullable HueEmulationConfigWithRuntime> startNow(
|
||||
@Nullable HueEmulationConfigWithRuntime ignored) {
|
||||
if (hasAlreadyBeenStarted) {
|
||||
logger.debug("Cannot restart thread");
|
||||
return future;
|
||||
}
|
||||
hasAlreadyBeenStarted = true;
|
||||
super.start();
|
||||
return future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
r.accept(this);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
Selector selector = asyncIOselector;
|
||||
if (selector != null) {
|
||||
try {
|
||||
selector.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
join();
|
||||
} catch (InterruptedException ignore) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
asyncIOselector = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.upnp;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* A self test report includes the tested address, a success flag and
|
||||
* if the service at the given address is actually ours.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SelfTestReport {
|
||||
public final String address;
|
||||
public final boolean reachable;
|
||||
public final boolean isOurs;
|
||||
|
||||
SelfTestReport(String address, boolean testReport, boolean isOurs) {
|
||||
this.address = address;
|
||||
this.reachable = testReport;
|
||||
this.isOurs = isOurs;
|
||||
}
|
||||
|
||||
/**
|
||||
* A failed address is not reachable and not ours
|
||||
*/
|
||||
public static SelfTestReport failed(String address) {
|
||||
return new SelfTestReport(address, false, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.upnp;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.StandardSocketOptions;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedSelectorException;
|
||||
import java.nio.channels.DatagramChannel;
|
||||
import java.nio.channels.SelectionKey;
|
||||
import java.nio.channels.Selector;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.glassfish.jersey.client.ClientConfig;
|
||||
import org.glassfish.jersey.client.ClientProperties;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ConfigurationPolicy;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.osgi.service.event.Event;
|
||||
import org.osgi.service.event.EventAdmin;
|
||||
import org.osgi.service.event.EventConstants;
|
||||
import org.osgi.service.event.EventHandler;
|
||||
import org.osgi.service.http.HttpService;
|
||||
import org.osgi.service.http.NamespaceException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Advertises a Hue compatible bridge via UPNP and provides the announced /description.xml http endpoint.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author David Graeff - Rewritten
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@NonNullByDefault
|
||||
@Component(immediate = false, // Don't start the upnp server on its own. Must be pulled in by HueEmulationService.
|
||||
configurationPolicy = ConfigurationPolicy.IGNORE, property = {
|
||||
EventConstants.EVENT_TOPIC + "=" + ConfigStore.EVENT_ADDRESS_CHANGED,
|
||||
"com.eclipsesource.jaxrs.publish=false" }, //
|
||||
service = { UpnpServer.class, EventHandler.class })
|
||||
public class UpnpServer extends HttpServlet implements Consumer<HueEmulationConfigWithRuntime>, EventHandler {
|
||||
/**
|
||||
* Used by async IO. This is our context object class.
|
||||
*/
|
||||
static class ClientRecord {
|
||||
public @Nullable SocketAddress clientAddress;
|
||||
public ByteBuffer buffer = ByteBuffer.allocate(1000);
|
||||
}
|
||||
|
||||
public static final String DISCOVERY_FILE = "/description.xml";
|
||||
|
||||
// jUPNP shares port 1900, but since this is multicast, we can also bind to it
|
||||
public static final int UPNP_PORT = 1900;
|
||||
/**
|
||||
* Send a keep alive every 2 minutes
|
||||
*/
|
||||
private static final int CACHE_MSECS = 120 * 1000;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(UpnpServer.class);
|
||||
|
||||
public final InetAddress MULTI_ADDR_IPV4;
|
||||
public final InetAddress MULTI_ADDR_IPV6;
|
||||
private String[] stVersions = { "", "", "" };
|
||||
private String notifyMsg = "";
|
||||
|
||||
//// objects, set within activate()
|
||||
protected @NonNullByDefault({}) String xmlDoc;
|
||||
protected @NonNullByDefault({}) String xmlDocWithAddress;
|
||||
private @NonNullByDefault({}) String baseurl;
|
||||
|
||||
//// services
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
@Reference
|
||||
protected @NonNullByDefault({}) HttpService httpService;
|
||||
|
||||
public boolean overwriteReadyToFalse = false;
|
||||
|
||||
private HueEmulationConfigWithRuntime config;
|
||||
protected CompletableFuture<@Nullable HueEmulationConfigWithRuntime> configChangeFuture = CompletableFuture
|
||||
.completedFuture(config);
|
||||
|
||||
private List<SelfTestReport> selfTests = new ArrayList<>();
|
||||
private final Executor executor;
|
||||
|
||||
/**
|
||||
* Creates a server instance.
|
||||
* UPnP IPv4/v6 multicast addresses are determined.
|
||||
*/
|
||||
public UpnpServer() {
|
||||
this(ForkJoinPool.commonPool());
|
||||
}
|
||||
|
||||
public UpnpServer(Executor executor) {
|
||||
try {
|
||||
MULTI_ADDR_IPV4 = InetAddress.getByName("239.255.255.250");
|
||||
MULTI_ADDR_IPV6 = InetAddress.getByName("ff02::c");
|
||||
config = new HueEmulationConfigWithRuntime(this, null, MULTI_ADDR_IPV4, MULTI_ADDR_IPV6);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* this object is also a servlet for providing /description.xml, the UPnP discovery result
|
||||
*/
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
|
||||
if (xmlDocWithAddress == null || xmlDocWithAddress.isEmpty()) {
|
||||
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
try (PrintWriter out = resp.getWriter()) {
|
||||
resp.setContentType("application/xml");
|
||||
out.write(xmlDocWithAddress);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server to send UDP packets onto the network when requested by a Hue API compatible device.
|
||||
*
|
||||
* @param relativePath The URI path where the discovery xml document can be retrieved
|
||||
* @param config The hue datastore. Contains the bridgeid and uuid.
|
||||
* @param address IP to advertise for UPNP
|
||||
* @throws IOException
|
||||
* @throws NamespaceException
|
||||
* @throws ServletException
|
||||
*/
|
||||
@Activate
|
||||
protected void activate() {
|
||||
InputStream resourceAsStream = getClass().getClassLoader().getResourceAsStream("discovery.xml");
|
||||
if (resourceAsStream == null) {
|
||||
logger.warn("Could not start Hue Emulation service: discovery.xml not found");
|
||||
return;
|
||||
}
|
||||
try (InputStreamReader r = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8);
|
||||
BufferedReader br = new BufferedReader(r)) {
|
||||
xmlDoc = br.lines().collect(Collectors.joining("\n"));
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not start Hue Emulation UPNP server: {}", e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
httpService.unregister(DISCOVERY_FILE);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
|
||||
try {
|
||||
httpService.registerServlet(DISCOVERY_FILE, this, null, null);
|
||||
} catch (ServletException | NamespaceException e) {
|
||||
logger.warn("Could not start Hue Emulation UPNP server: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (cs.isReady() && !overwriteReadyToFalse) {
|
||||
handleEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void useAddressPort(HueEmulationConfigWithRuntime r) {
|
||||
final String urlBase = "http://" + r.addressString + ":" + r.port;
|
||||
this.baseurl = urlBase + DISCOVERY_FILE;
|
||||
|
||||
final String[] stVersions = { "upnp:rootdevice", "urn:schemas-upnp-org:device:basic:1",
|
||||
"uuid:" + config.config.uuid };
|
||||
for (int i = 0; i < stVersions.length; ++i) {
|
||||
this.stVersions[i] = String.format(
|
||||
"HTTP/1.1 200 OK\r\n" + "HOST: %s:%d\r\n" + "EXT:\r\n" + "CACHE-CONTROL: max-age=%d\r\n"
|
||||
+ "LOCATION: %s\r\n" + "SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/%s\r\n"
|
||||
+ "hue-bridgeid: %s\r\n" + "ST: %s\r\n" + "USN: uuid:%s\r\n\r\n",
|
||||
r.getMulticastAddress(), UPNP_PORT, CACHE_MSECS / 1000, baseurl, // host:port,
|
||||
// cache,location
|
||||
cs.ds.config.apiversion, cs.ds.config.bridgeid, // version, bridgeid
|
||||
stVersions[i], config.config.uuid);
|
||||
}
|
||||
|
||||
this.notifyMsg = String.format(
|
||||
"NOTIFY * HTTP/1.1\r\n" + "HOST: %s:%d\r\n" + "CACHE-CONTROL: max-age=%d\r\n" + "LOCATION: %s\r\n"
|
||||
+ "SERVER: Linux/3.14.0 UPnP/1.0 IpBridge/%s\r\nNTS: ssdp:alive\r\nNT: upnp:rootdevice\r\n"
|
||||
+ "USN: uuid:%s::upnp:rootdevice\r\n" + "hue-bridgeid: %s\r\n\r\n",
|
||||
r.getMulticastAddress(), UPNP_PORT, CACHE_MSECS / 1000, baseurl, // host:port, cache,location
|
||||
cs.ds.config.apiversion, config.config.uuid, cs.ds.config.bridgeid);// version, uuid, bridgeid
|
||||
|
||||
xmlDocWithAddress = String.format(xmlDoc, urlBase, r.addressString, cs.ds.config.bridgeid, cs.ds.config.uuid,
|
||||
cs.ds.config.devicename);
|
||||
}
|
||||
|
||||
protected @Nullable HueEmulationConfigWithRuntime performAddressTest(
|
||||
@Nullable HueEmulationConfigWithRuntime config) {
|
||||
if (config == null) {
|
||||
return null; // Config hasn't changed
|
||||
}
|
||||
|
||||
selfTests.clear();
|
||||
|
||||
ClientConfig configuration = new ClientConfig();
|
||||
configuration = configuration.property(ClientProperties.CONNECT_TIMEOUT, 1000);
|
||||
configuration = configuration.property(ClientProperties.READ_TIMEOUT, 1000);
|
||||
Client client = ClientBuilder.newClient(configuration);
|
||||
Response response;
|
||||
String url = "";
|
||||
try {
|
||||
boolean selfTestOnPort80;
|
||||
try {
|
||||
response = client.target("http://" + cs.ds.config.ipaddress + ":80" + DISCOVERY_FILE).request().get();
|
||||
selfTestOnPort80 = response.getStatus() == 200
|
||||
&& response.readEntity(String.class).contains(cs.ds.config.bridgeid);
|
||||
} catch (ProcessingException ignored) {
|
||||
selfTestOnPort80 = false;
|
||||
}
|
||||
|
||||
// Prefer port 80 if possible and if not overwritten by discoveryHttpPort
|
||||
config.port = config.config.discoveryHttpPort == 0 && selfTestOnPort80 ? 80 : config.port;
|
||||
|
||||
// Test on all assigned interface addresses on org.osgi.service.http.port as well as port 80
|
||||
// Other services might run on port 80, so we search for our bridge ID in the returned document.
|
||||
for (InetAddress address : cs.getDiscoveryIps()) {
|
||||
String ip = address.getHostAddress();
|
||||
if (address instanceof Inet6Address) {
|
||||
ip = "[" + ip.split("%")[0] + "]";
|
||||
}
|
||||
try {
|
||||
url = "http://" + ip + ":" + config.port + DISCOVERY_FILE;
|
||||
response = client.target(url).request().get();
|
||||
boolean isOurs = response.readEntity(String.class).contains(cs.ds.config.bridgeid);
|
||||
selfTests.add(new SelfTestReport(url, response.getStatus() == 200, isOurs));
|
||||
} catch (ProcessingException e) {
|
||||
logger.debug("Self test fail on {}: {}", url, e.getMessage());
|
||||
selfTests.add(SelfTestReport.failed(url));
|
||||
}
|
||||
try {
|
||||
url = "http://" + ip + DISCOVERY_FILE; // Port 80
|
||||
response = client.target(url).request().get();
|
||||
boolean isOurs = response.readEntity(String.class).contains(cs.ds.config.bridgeid);
|
||||
selfTests.add(new SelfTestReport(url, response.getStatus() == 200, isOurs));
|
||||
} catch (ProcessingException e) {
|
||||
logger.debug("Self test fail on {}: {}", url, e.getMessage());
|
||||
selfTests.add(SelfTestReport.failed(url));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return new runtime configuration based on {@link ConfigStore}s current configuration.
|
||||
* Return null if the configuration has not changed compared to {@link #config}.
|
||||
*
|
||||
* @throws IllegalStateException If the {@link ConfigStore}s IP is invalid this exception is thrown.
|
||||
*/
|
||||
protected @Nullable HueEmulationConfigWithRuntime createConfiguration(
|
||||
@Nullable HueEmulationConfigWithRuntime ignoredParameter) throws IllegalStateException {
|
||||
HueEmulationConfigWithRuntime r;
|
||||
try {
|
||||
r = new HueEmulationConfigWithRuntime(this, cs.getConfig(), cs.ds.config.ipaddress, MULTI_ADDR_IPV4,
|
||||
MULTI_ADDR_IPV6);
|
||||
} catch (UnknownHostException e) {
|
||||
logger.warn("The picked default IP address is not valid: {}", e.getMessage());
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given runtime configuration by stopping the current udp thread, shutting down the socket and restarting
|
||||
* the thread.
|
||||
*/
|
||||
protected @Nullable HueEmulationConfigWithRuntime applyConfiguration(
|
||||
@Nullable HueEmulationConfigWithRuntime newRuntimeConfig) {
|
||||
if (newRuntimeConfig == null) {
|
||||
return null;// Config hasn't changed
|
||||
}
|
||||
config.dispose();
|
||||
this.config = newRuntimeConfig;
|
||||
useAddressPort(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* We have a hard dependency on the {@link ConfigStore} and that it has initialized the Hue DataStore config
|
||||
* completely. That initialization happens asynchronously and therefore we cannot rely on OSGi activate/modified
|
||||
* state changes. Instead the {@link EventAdmin} is used and we listen for the
|
||||
* {@link ConfigStore#EVENT_ADDRESS_CHANGED} event that is fired as soon as the config is ready.
|
||||
* <p>
|
||||
* To be really sure that we are called here, this is also issued by the main service after it has received the
|
||||
* configuration ready event and depending on service start order we are also called by our own activate() method
|
||||
* when the configuration is already ready at that time.
|
||||
* <p>
|
||||
* Therefore this method is "synchronized" and chains a completable future for each call to re-evaluate the config
|
||||
* after the former future has finished.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void handleEvent(@Nullable Event event) {
|
||||
CompletableFuture<@Nullable HueEmulationConfigWithRuntime> root;
|
||||
// There is either already a running future, then chain a new one
|
||||
if (!configChangeFuture.isDone()) {
|
||||
root = configChangeFuture;
|
||||
} else { // Or there is none -> create a new one
|
||||
root = CompletableFuture.completedFuture(null);
|
||||
}
|
||||
configChangeFuture = root.thenApply(this::createConfiguration)
|
||||
.thenApplyAsync(this::performAddressTest, executor).thenApply(this::applyConfiguration)
|
||||
.thenCompose(config::startNow)
|
||||
.whenComplete((HueEmulationConfigWithRuntime config, @Nullable Throwable e) -> {
|
||||
if (e != null) {
|
||||
logger.warn("Upnp server: Address test failed", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the upnp server from running
|
||||
*/
|
||||
@Deactivate
|
||||
public void deactivate() {
|
||||
config.dispose();
|
||||
try {
|
||||
httpService.unregister(DISCOVERY_FILE);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRead(SelectionKey key) throws IOException {
|
||||
logger.trace("upnp thread handle received message");
|
||||
DatagramChannel channel = (DatagramChannel) key.channel();
|
||||
ClientRecord clntRec = (ClientRecord) key.attachment();
|
||||
clntRec.buffer.clear(); // Prepare buffer for receiving
|
||||
clntRec.clientAddress = channel.receive(clntRec.buffer);
|
||||
InetSocketAddress recAddress = (InetSocketAddress) clntRec.clientAddress;
|
||||
if (recAddress == null) { // Did we receive something?
|
||||
return;
|
||||
}
|
||||
String data = new String(clntRec.buffer.array(), StandardCharsets.UTF_8);
|
||||
if (!data.startsWith("M-SEARCH")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DatagramSocket sendSocket = new DatagramSocket()) {
|
||||
sendUPNPDatagrams(sendSocket, recAddress.getAddress(), recAddress.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUPNPDatagrams(DatagramSocket sendSocket, InetAddress address, int port) {
|
||||
logger.trace("upnp thread send announcement");
|
||||
for (String msg : stVersions) {
|
||||
DatagramPacket response = new DatagramPacket(msg.getBytes(), msg.length(), address, port);
|
||||
try {
|
||||
logger.trace("Sending to {}:{}", address.getHostAddress(), port);
|
||||
sendSocket.send(response);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send UPNP response: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendUPNPNotify(DatagramSocket sendSocket, InetAddress address, int port) {
|
||||
DatagramPacket response = new DatagramPacket(notifyMsg.getBytes(), notifyMsg.length(), address, port);
|
||||
try {
|
||||
logger.trace("Sending to {}:{}", address.getHostAddress(), port);
|
||||
sendSocket.send(response);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not send UPNP response: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(HueEmulationConfigWithRuntime threadContext) {
|
||||
logger.info("Hue Emulation UPNP server started on {}:{}", threadContext.addressString, threadContext.port);
|
||||
boolean hasIPv4 = false;
|
||||
boolean hasIPv6 = false;
|
||||
|
||||
try (Selector selector = Selector.open();
|
||||
DatagramChannel channelV4 = createBoundDataGramChannelOrNull(StandardProtocolFamily.INET);
|
||||
DatagramChannel channelV6 = createBoundDataGramChannelOrNull(StandardProtocolFamily.INET6)) {
|
||||
// Set global config to thread local config. Otherwise upnpAnnouncementThreadRunning() will report wrong
|
||||
// results.
|
||||
config = threadContext;
|
||||
threadContext.asyncIOselector = selector;
|
||||
|
||||
for (InetAddress address : cs.getDiscoveryIps()) {
|
||||
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(address);
|
||||
if (networkInterface == null) {
|
||||
continue;
|
||||
}
|
||||
if (address instanceof Inet4Address && channelV4 != null) {
|
||||
channelV4.join(MULTI_ADDR_IPV4, networkInterface);
|
||||
hasIPv4 = true;
|
||||
} else if (channelV6 != null) {
|
||||
channelV6.join(MULTI_ADDR_IPV6, networkInterface);
|
||||
hasIPv6 = true;
|
||||
}
|
||||
}
|
||||
if (!hasIPv4 && !hasIPv6) {
|
||||
logger.warn("Could not join upnp multicast network!");
|
||||
threadContext.future
|
||||
.completeExceptionally(new IllegalStateException("Could not join upnp multicast network!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasIPv4) {
|
||||
channelV4.configureBlocking(false);
|
||||
channelV4.register(selector, SelectionKey.OP_READ, new ClientRecord());
|
||||
try (DatagramSocket sendSocket = new DatagramSocket(new InetSocketAddress(config.address, 0))) {
|
||||
sendUPNPDatagrams(sendSocket, MULTI_ADDR_IPV4, UPNP_PORT);
|
||||
}
|
||||
}
|
||||
if (hasIPv6) {
|
||||
channelV6.configureBlocking(false);
|
||||
channelV6.register(selector, SelectionKey.OP_READ, new ClientRecord());
|
||||
try (DatagramSocket sendSocket = new DatagramSocket()) {
|
||||
sendUPNPDatagrams(sendSocket, MULTI_ADDR_IPV6, UPNP_PORT);
|
||||
}
|
||||
}
|
||||
|
||||
threadContext.future.complete(threadContext);
|
||||
Instant time = Instant.now();
|
||||
|
||||
while (selector.isOpen()) { // Run forever, receiving and echoing datagrams
|
||||
// Wait for task or until timeout expires
|
||||
selector.select(CACHE_MSECS);
|
||||
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
|
||||
while (keyIter.hasNext()) {
|
||||
SelectionKey key = keyIter.next();
|
||||
if (key.isReadable()) {
|
||||
handleRead(key);
|
||||
}
|
||||
keyIter.remove();
|
||||
}
|
||||
|
||||
if (time.plusMillis(CACHE_MSECS - 200).isBefore(Instant.now())) {
|
||||
logger.trace("upnp thread send periodic announcement");
|
||||
time = Instant.now();
|
||||
if (hasIPv4) {
|
||||
try (DatagramSocket sendSocket = new DatagramSocket(new InetSocketAddress(config.address, 0))) {
|
||||
sendUPNPNotify(sendSocket, MULTI_ADDR_IPV4, UPNP_PORT);
|
||||
}
|
||||
}
|
||||
if (hasIPv6) {
|
||||
try (DatagramSocket sendSocket = new DatagramSocket()) {
|
||||
sendUPNPNotify(sendSocket, MULTI_ADDR_IPV6, UPNP_PORT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ClosedSelectorException ignored) {
|
||||
} catch (IOException e) {
|
||||
logger.warn("Socket error with UPNP server", e);
|
||||
threadContext.future.completeExceptionally(e);
|
||||
} finally {
|
||||
threadContext.asyncIOselector = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private DatagramChannel createBoundDataGramChannelOrNull(StandardProtocolFamily family) throws IOException {
|
||||
try {
|
||||
return DatagramChannel.open(family).setOption(StandardSocketOptions.SO_REUSEADDR, true)
|
||||
.setOption(StandardSocketOptions.IP_MULTICAST_LOOP, true).bind(new InetSocketAddress(UPNP_PORT));
|
||||
} catch (UnsupportedOperationException uoe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The upnp server performs some self-tests
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<SelfTestReport> selfTests() {
|
||||
return selfTests;
|
||||
}
|
||||
|
||||
public String getBaseURL() {
|
||||
return baseurl;
|
||||
}
|
||||
|
||||
public int getDefaultport() {
|
||||
return config.port;
|
||||
}
|
||||
|
||||
public boolean upnpAnnouncementThreadRunning() {
|
||||
return config.asyncIOselector != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "timer.AbsoluteDateTimeTrigger",
|
||||
"label": "Absolute date/time Trigger",
|
||||
"description": "This triggers a rule based on a fixed date/time",
|
||||
"visibility": "VISIBLE",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "date",
|
||||
"type": "TEXT",
|
||||
"context": "date",
|
||||
"label": "Date",
|
||||
"description": "A date with the pattern yyyy-mm-dd",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "time",
|
||||
"type": "TEXT",
|
||||
"context": "time",
|
||||
"label": "Time",
|
||||
"description": "A time with the pattern hh:mm:ss",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "randomizeTime",
|
||||
"type": "TEXT",
|
||||
"context": "time",
|
||||
"label": "Randomized Time bound",
|
||||
"description": "An upper time bound with the pattern hh:mm:ss. If this is given, the trigger triggers on a random time between Time and Randomized Time",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "rules.HttpAction",
|
||||
"label": "Remove rule",
|
||||
"description": "Removes a rule permanently",
|
||||
"visibility": "VISIBLE",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "method",
|
||||
"type": "TEXT",
|
||||
"label": "Http method",
|
||||
"description": "The http method to be used",
|
||||
"required": true,
|
||||
"default": "GET",
|
||||
"limitToOptions": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "GET",
|
||||
"value": "GET"
|
||||
},
|
||||
{
|
||||
"label": "POST",
|
||||
"value": "POST"
|
||||
},
|
||||
{
|
||||
"label": "PUT",
|
||||
"value": "PUT"
|
||||
},
|
||||
{
|
||||
"label": "HEAD",
|
||||
"value": "HEAD"
|
||||
},
|
||||
{
|
||||
"label": "DELETE",
|
||||
"value": "DELETE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"type": "TEXT",
|
||||
"context": "url",
|
||||
"label": "URL",
|
||||
"description": "The url that the http request should be done on. Can be a relative one, starting with '/'. For example '/api/foo/bar'",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"type": "TEXT",
|
||||
"context": "body",
|
||||
"label": "Data",
|
||||
"description": "For post and put request you can send data with the request",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "mimetype",
|
||||
"type": "TEXT",
|
||||
"context": "body",
|
||||
"label": "Mimetype",
|
||||
"description": "For post and put request you can send data with the request. Set the mimetype of that date here.",
|
||||
"required": false,
|
||||
"limitToOptions": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Text",
|
||||
"value": "text/plain"
|
||||
},
|
||||
{
|
||||
"label": "Json",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"label": "XML",
|
||||
"value": "application/xml"
|
||||
},
|
||||
{
|
||||
"label": "Binary",
|
||||
"value": "application/octet-stream"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"type": "INTEGER",
|
||||
"label": "Timeout in sec",
|
||||
"description": "The timeout of this request in seconds.",
|
||||
"required": false,
|
||||
"default": "5"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "hue.ruleCondition",
|
||||
"label": "A condition especially for Hue rules",
|
||||
"description": "Depending on the operator and item state this condition will be satisfied or not",
|
||||
"visibility": "HIDDEN",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "operator",
|
||||
"type": "TEXT",
|
||||
"label": "Operator",
|
||||
"description": "The operator that compares either the hue bridge time or a light/sensor state with a given static value",
|
||||
"required": true,
|
||||
"limitToOptions": true,
|
||||
"options": [
|
||||
{
|
||||
"label": "Equal",
|
||||
"value": "eq"
|
||||
},
|
||||
{
|
||||
"label": "Greater than",
|
||||
"value": "gt"
|
||||
},
|
||||
{
|
||||
"label": "Lower than",
|
||||
"value": "lt"
|
||||
},
|
||||
{
|
||||
"label": "On item state change",
|
||||
"value": "dx"
|
||||
},
|
||||
{
|
||||
"label": "On item state change, delayed",
|
||||
"value": "ddx"
|
||||
},
|
||||
{
|
||||
"label": "Item has been updated but did not change",
|
||||
"value": "stable"
|
||||
},
|
||||
{
|
||||
"label": "Item has been updated and has changed",
|
||||
"value": "not_stable"
|
||||
},
|
||||
{
|
||||
"label": "The current time is in range of the given time",
|
||||
"value": "in"
|
||||
},
|
||||
{
|
||||
"label": "The current time is NOT in range of the given time",
|
||||
"value": "not_in"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "address",
|
||||
"type": "TEXT",
|
||||
"label": "Hue link address",
|
||||
"description": "A hue local link address like /sensors/2/state/buttonevent or /config/localtime",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "TEXT",
|
||||
"label": "An optional compare value",
|
||||
"description": "Only valid for eq,lg,lt operators and a number or boolean item type or in,not_in. For the later it must be in the format of W127/T12:12:12/T14:00:00 describing the valid weekdays (binary encoded), the start time and stop time.",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "rules.RemoveRuleAction",
|
||||
"label": "Remove rule",
|
||||
"description": "Removes a rule permanently",
|
||||
"visibility": "VISIBLE",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "removeuid",
|
||||
"type": "TEXT",
|
||||
"context": "rule",
|
||||
"label": "Rule",
|
||||
"description": "The rule that should be removed",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"triggers": [
|
||||
{
|
||||
"uid": "timer.TimerTrigger",
|
||||
"label": "Timer Trigger",
|
||||
"description": "This triggers a rule based on a timer",
|
||||
"visibility": "VISIBLE",
|
||||
"configDescriptions": [
|
||||
{
|
||||
"name": "time",
|
||||
"type": "TEXT",
|
||||
"context": "time",
|
||||
"label": "Duration",
|
||||
"description": "A duration before this timer expires with the pattern hh:mm:ss. The shortest duration is therefore 1 second, the longest is 99 hours.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "randomizeTime",
|
||||
"type": "TEXT",
|
||||
"context": "time",
|
||||
"label": "Duration Upper Bound",
|
||||
"description": "An optional upper bound duration before this timer expires with the pattern hh:mm:ss. A random duration between Duration and Duration Upper Bound will be chosen.",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "repeat",
|
||||
"type": "INTEGER",
|
||||
"label": "Repeat",
|
||||
"description": "You can make this timer a recurring timer by setting a value above 1. The default is 1. If you set a value below 0 like -1 this timer will be repeated indefinitely",
|
||||
"default": "1",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
|
||||
https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
<config-description uri="io:hueemulation">
|
||||
<parameter name="pairingEnabled" type="boolean">
|
||||
<label>Device Pairing</label>
|
||||
<description>Pairing must be enabled to connect a new device. Pairing is automatically disabled after the configured
|
||||
pairing time (usually 60 seconds).</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="pairingTimeout" type="integer" required="false">
|
||||
<label>Pairing Timeout</label>
|
||||
<description>Pairing is automatically disabled after the given time in seconds.</description>
|
||||
<default>60</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="createNewUserOnEveryEndpoint" type="boolean">
|
||||
<label>Pairing: Add Unknown User-keys</label>
|
||||
<description>Set this option to create new users on the fly during the next pairing mode period. This helps with
|
||||
Amazon Echo device discovery. This option is automatically switched off after the timeout.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="temporarilyEmulateV1bridge" type="boolean">
|
||||
<label>Pairing: Temporarily Emulate V1 Hue Bridge</label>
|
||||
<description>Some Amazon Echos only support V1 bridges (round hardware bridge). This option is only active during
|
||||
discovery and automatically switched off after the timeout.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="permanentV1bridge" type="boolean">
|
||||
<label>Permanently Emulate V1 Hue Bridge</label>
|
||||
<description>There is no obvious reason to not emulate the newer bridge all the time, but here is the option if you
|
||||
want the old (round Hue bridge) to be emulated.</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="restrictToTagsSwitches" type="text" required="false">
|
||||
<label>Switch Item Tags</label>
|
||||
<description>The HUE emulation can either publish Switch items if this is set to an empty string or filter items by
|
||||
tags. Use commas to separate multiple entries.</description>
|
||||
<default>Switchable</default>
|
||||
</parameter>
|
||||
<parameter name="restrictToTagsColorLights" type="text" required="false">
|
||||
<label>Color Item Tags</label>
|
||||
<description>The HUE emulation can either publish all Color items if this is set to an empty string or filter items
|
||||
by tags. Use commas to separate multiple entries.</description>
|
||||
<default>ColorLighting</default>
|
||||
</parameter>
|
||||
<parameter name="restrictToTagsWhiteLights" type="text" required="false">
|
||||
<label>White Item Tags</label>
|
||||
<description>The HUE emulation can either publish all Dimmer items if this is set to an empty string or filter items
|
||||
by tags. Use commas to separate multiple entries.</description>
|
||||
<default>Lighting</default>
|
||||
</parameter>
|
||||
<parameter name="ignoreItemsWithTags" type="text" required="false">
|
||||
<label>Ignore Items by Tag</label>
|
||||
<description>All items that are tagged with the given tags are ignore by the Hue Emulation Service. Use commas to
|
||||
separate multiple entries.</description>
|
||||
<default>internal</default>
|
||||
</parameter>
|
||||
<parameter name="discoveryIp" type="text" required="false">
|
||||
<label>Optional Discovery Address</label>
|
||||
<description>If your host has multiple IP addresses you may specify the IP(s) you would like to advertise in the UPNP
|
||||
discovery process. You may safely leave this empty on most systems. Use commas to separate multiple entries.</description>
|
||||
</parameter>
|
||||
<parameter name="discoveryHttpPort" type="integer" required="false">
|
||||
<label>Optional Discovery Web Port</label>
|
||||
<description>Some Hue applications require a different port (80) then what openHAB runs on by default (8080). This
|
||||
option will only advertise a different port then what we are listening on. Useful if you have an iptables rule
|
||||
redirect traffic from this port to the openHAB port.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="uuid" type="text" required="false">
|
||||
<label>Unique Bridge ID</label>
|
||||
<description>Each Hue bridge has a universal unique id (UUID) assigned. This is random generated if no value has been
|
||||
assigned. Note on Amazon Alexa Echo devices: It might help to change the UUID after you have changed item ids. The
|
||||
Echos will recognize this service as a new bridge.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>%s/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
|
||||
<friendlyName>openHAB Hue Emulation (%s)</friendlyName>
|
||||
<manufacturer>Royal Philips Electronics</manufacturer>
|
||||
<manufacturerURL>http://www.openhab.org</manufacturerURL>
|
||||
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
|
||||
<modelName>Philips hue bridge 2015</modelName>
|
||||
<modelNumber>BSB002</modelNumber>
|
||||
<modelURL>http://www.meethue.com</modelURL>
|
||||
<serialNumber>%s</serialNumber>
|
||||
<UDN>uuid:%s</UDN>
|
||||
<gatewayName>%s</gatewayName>
|
||||
<presentationURL>index.html</presentationURL>
|
||||
<iconList>
|
||||
<icon>
|
||||
<mimetype>image/png</mimetype>
|
||||
<height>48</height>
|
||||
<width>48</width>
|
||||
<depth>24</depth>
|
||||
<url>hue_logo_0.png</url>
|
||||
</icon>
|
||||
<icon>
|
||||
<mimetype>image/png</mimetype>
|
||||
<height>120</height>
|
||||
<width>120</width>
|
||||
<depth>24</depth>
|
||||
<url>hue_logo_3.png</url>
|
||||
</icon>
|
||||
</iconList>
|
||||
</device>
|
||||
</root>
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.automation;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.library.items.ContactItem;
|
||||
import org.openhab.core.library.items.NumberItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.util.ConditionBuilder;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
import org.openhab.io.hueemulation.internal.RuleUtils;
|
||||
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.HueSensorEntry;
|
||||
|
||||
/**
|
||||
* Test the {@link HueRuleConditionHandler}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RuleConditionHandlerTests {
|
||||
protected @NonNullByDefault({}) HueDataStore ds;
|
||||
|
||||
private static class HueRuleConditionHandlerEx extends HueRuleConditionHandler {
|
||||
LocalDateTime dt = LocalDateTime.of(2019, 04, 12, 12, 12, 12);
|
||||
|
||||
public HueRuleConditionHandlerEx(Condition module, HueDataStore ds) {
|
||||
super(module, ds);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LocalDateTime getNow() {
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ds = new HueDataStore();
|
||||
|
||||
ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
|
||||
|
||||
ds.sensors.put("2", new HueSensorEntry(new ContactItem("contact")));
|
||||
|
||||
ds.groups.put("10",
|
||||
new HueGroupEntry("name", new GroupItem("white", new NumberItem("number")), DeviceType.SwitchType));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
RuleUtils.random = new Random();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void itemNotExisting() {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("address", "/groups/9/action");
|
||||
configuration.put("operator", "dx");
|
||||
configuration.put("value", "");
|
||||
Condition c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
new HueRuleConditionHandler(c, ds);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itemAccept() {
|
||||
Condition c;
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("operator", "dx");
|
||||
configuration.put("value", "");
|
||||
|
||||
configuration.put("address", "/groups/10/action");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
new HueRuleConditionHandler(c, ds);
|
||||
|
||||
configuration.put("address", "/lights/1/state");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
new HueRuleConditionHandler(c, ds);
|
||||
|
||||
configuration.put("address", "/sensors/2/state");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
new HueRuleConditionHandler(c, ds);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeRangeAccept() {
|
||||
Condition c;
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("address", "/config/localtime");
|
||||
configuration.put("operator", "in");
|
||||
configuration.put("value", "T12:12:10/T12:12:50");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
HueRuleConditionHandlerEx subject = new HueRuleConditionHandlerEx(c, ds);
|
||||
assertThat(subject.isSatisfied(Collections.emptyMap()), is(true));
|
||||
|
||||
configuration.put("address", "/config/localtime");
|
||||
configuration.put("operator", "not_in");
|
||||
configuration.put("value", "W8/T12:12:10/T12:12:50");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandlerEx(c, ds);
|
||||
assertThat(subject.isSatisfied(Collections.emptyMap()), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalOperator() {
|
||||
Map<String, Object> context = new TreeMap<>();
|
||||
|
||||
HueRuleConditionHandler subject;
|
||||
Condition c;
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("operator", "eq");
|
||||
|
||||
context.put("newState", OnOffType.ON);
|
||||
context.put("oldState", OnOffType.OFF);
|
||||
configuration.put("value", "true");
|
||||
configuration.put("address", "/lights/1/state");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandler(c, ds);
|
||||
assertThat(subject.isSatisfied(context), is(true));
|
||||
|
||||
context.put("newState", OpenClosedType.OPEN);
|
||||
context.put("oldState", OpenClosedType.CLOSED);
|
||||
configuration.put("value", "true");
|
||||
configuration.put("address", "/sensors/2/state");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandler(c, ds);
|
||||
assertThat(subject.isSatisfied(context), is(true));
|
||||
|
||||
context.put("newState", new DecimalType(12));
|
||||
context.put("oldState", new DecimalType(0));
|
||||
configuration.put("value", "12");
|
||||
configuration.put("address", "/groups/10/action");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandler(c, ds);
|
||||
assertThat(subject.isSatisfied(context), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gtOperator() {
|
||||
Map<String, Object> context = new TreeMap<>();
|
||||
|
||||
HueRuleConditionHandler subject;
|
||||
Condition c;
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("operator", "gt");
|
||||
|
||||
context.put("newState", new DecimalType(12));
|
||||
context.put("oldState", new DecimalType(0));
|
||||
configuration.put("value", "10");
|
||||
configuration.put("address", "/groups/10/action");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandler(c, ds);
|
||||
assertThat(subject.isSatisfied(context), is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ltOperator() {
|
||||
Map<String, Object> context = new TreeMap<>();
|
||||
|
||||
HueRuleConditionHandler subject;
|
||||
Condition c;
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.put("operator", "lt");
|
||||
|
||||
context.put("newState", new DecimalType(12));
|
||||
context.put("oldState", new DecimalType(0));
|
||||
configuration.put("value", "15");
|
||||
configuration.put("address", "/groups/10/action");
|
||||
c = ConditionBuilder.create().withId("a").withTypeUID(HueRuleConditionHandler.MODULE_TYPE_ID)
|
||||
.withConfiguration(configuration).build();
|
||||
subject = new HueRuleConditionHandler(c, ds);
|
||||
assertThat(subject.isSatisfied(context), is(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.Dictionary;
|
||||
import java.util.Hashtable;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.client.ClientBuilder;
|
||||
|
||||
import org.openhab.core.events.EventPublisher;
|
||||
import org.openhab.core.items.MetadataRegistry;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.glassfish.grizzly.http.server.HttpServer;
|
||||
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
|
||||
import org.glassfish.jersey.logging.LoggingFeature;
|
||||
import org.glassfish.jersey.logging.LoggingFeature.Verbosity;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.ConfigStoreWithoutMetadata;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyMetadataRegistry;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyUsersStorage;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
|
||||
/**
|
||||
* We have no OSGi framework in the background. This class resolves
|
||||
* dependencies between the different classes and mocks common services like the configAdmin.
|
||||
* <p>
|
||||
* The {@link UserManagement} rest components is always
|
||||
* setup and started in this common test setup, because all other rest components require
|
||||
* user authentication.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class CommonSetup {
|
||||
|
||||
public UserManagement userManagement;
|
||||
|
||||
@Mock
|
||||
public EventPublisher eventPublisher;
|
||||
|
||||
public ConfigStore cs;
|
||||
|
||||
@Mock
|
||||
ConfigurationAdmin configAdmin;
|
||||
|
||||
@Mock
|
||||
ScheduledExecutorService scheduler;
|
||||
|
||||
@Mock
|
||||
org.osgi.service.cm.Configuration configAdminConfig;
|
||||
|
||||
@Mock
|
||||
NetworkAddressService networkAddressService;
|
||||
|
||||
MetadataRegistry metadataRegistry = new DummyMetadataRegistry();
|
||||
|
||||
StorageService storageService = new StorageService() {
|
||||
@Override
|
||||
public <T> Storage<T> getStorage(String name, ClassLoader classLoader) {
|
||||
return getStorage(name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> Storage<T> getStorage(String name) {
|
||||
if (name.equals("hueEmulationUsers")) {
|
||||
return (Storage<T>) new DummyUsersStorage();
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
};
|
||||
|
||||
public Client client;
|
||||
public HttpServer server;
|
||||
public String basePath;
|
||||
|
||||
public CommonSetup(boolean withMetadata) throws IOException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(configAdmin.getConfiguration(anyString())).thenReturn(configAdminConfig);
|
||||
when(configAdmin.getConfiguration(anyString(), any())).thenReturn(configAdminConfig);
|
||||
Dictionary<String, Object> mockProperties = new Hashtable<>();
|
||||
when(configAdminConfig.getProperties()).thenReturn(mockProperties);
|
||||
when(networkAddressService.getPrimaryIpv4HostAddress()).thenReturn("127.0.0.1");
|
||||
|
||||
// If anything is scheduled, immediately run it instead
|
||||
when(scheduler.schedule(any(Runnable.class), anyLong(), any())).thenAnswer(answer -> {
|
||||
((Runnable) answer.getArgument(0)).run();
|
||||
return null;
|
||||
});
|
||||
|
||||
if (withMetadata) {
|
||||
cs = new ConfigStore(networkAddressService, configAdmin, metadataRegistry, scheduler);
|
||||
} else {
|
||||
cs = new ConfigStoreWithoutMetadata(networkAddressService, configAdmin, scheduler);
|
||||
}
|
||||
cs.activate(Collections.singletonMap("uuid", "demouuid"));
|
||||
cs.switchFilter = Collections.singleton("Switchable");
|
||||
cs.whiteFilter = Collections.singleton("Switchable");
|
||||
cs.colorFilter = Collections.singleton("ColorLighting");
|
||||
|
||||
userManagement = Mockito.spy(new UserManagement(storageService, cs));
|
||||
|
||||
try (ServerSocket serverSocket = new ServerSocket()) {
|
||||
serverSocket.bind(new InetSocketAddress(0));
|
||||
basePath = "http://localhost:" + serverSocket.getLocalPort() + "/api";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the http server to serve all registered jax-rs resources. Also setup a client for testing, see
|
||||
* {@link #client}.
|
||||
*
|
||||
* @param rc A resource config. Add objects and object instance resources to your needs. Example:
|
||||
* "new ResourceConfig().registerInstances(configurationAccess)"
|
||||
*/
|
||||
public void start(ResourceConfig resourceConfig) {
|
||||
ResourceConfig rc = resourceConfig.registerInstances(userManagement).register(new LoggingFeature(
|
||||
Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), Level.OFF, Verbosity.HEADERS_ONLY, 10));
|
||||
|
||||
Logger log2 = Logger.getLogger("org.glassfish");
|
||||
log2.setLevel(Level.OFF);
|
||||
|
||||
server = GrizzlyHttpServerFactory.createHttpServer(URI.create(basePath), rc);
|
||||
client = ClientBuilder.newClient();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (client != null) {
|
||||
client.close();
|
||||
}
|
||||
if (server != null) {
|
||||
server.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.Metadata;
|
||||
import org.openhab.core.items.MetadataKey;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
|
||||
|
||||
/**
|
||||
* Tests for the metadata provided hue ID mapping
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ItemUIDtoHueIDMappingTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
|
||||
LightsAndGroups lightsAndGroups = new LightsAndGroups();
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(true);
|
||||
commonSetup.start(new ResourceConfig());
|
||||
|
||||
itemRegistry = new DummyItemRegistry();
|
||||
|
||||
lightsAndGroups.cs = commonSetup.cs;
|
||||
lightsAndGroups.eventPublisher = commonSetup.eventPublisher;
|
||||
lightsAndGroups.userManagement = commonSetup.userManagement;
|
||||
lightsAndGroups.itemRegistry = itemRegistry;
|
||||
lightsAndGroups.activate();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineHighestHueID() {
|
||||
ConfigStore cs = new ConfigStore(commonSetup.networkAddressService, commonSetup.configAdmin,
|
||||
commonSetup.metadataRegistry, mock(ScheduledExecutorService.class));
|
||||
|
||||
// Pretend there is a metadata entry for the imaginary item "demo1" with hueid 10
|
||||
commonSetup.metadataRegistry.add(new Metadata(new MetadataKey(ConfigStore.METAKEY, "demo1"), "10", null));
|
||||
cs.activate(Collections.singletonMap("uuid", "demouuid"));
|
||||
|
||||
assertThat(cs.getHighestAssignedHueID(), CoreMatchers.is(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapItemWithoutHueID() {
|
||||
ConfigStore cs = commonSetup.cs;
|
||||
assertThat(cs.getHighestAssignedHueID(), CoreMatchers.is(1));
|
||||
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.setCategory("Light");
|
||||
itemRegistry.add(item);
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(item);
|
||||
assertThat(hueID, CoreMatchers.is("2"));
|
||||
|
||||
HueLightEntry device = cs.ds.lights.get(hueID);
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
|
||||
assertThat(cs.getHighestAssignedHueID(), CoreMatchers.is(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mapItemWithHueID() {
|
||||
ConfigStore cs = commonSetup.cs;
|
||||
assertThat(cs.getHighestAssignedHueID(), CoreMatchers.is(1));
|
||||
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.setCategory("Light");
|
||||
commonSetup.metadataRegistry.add(new Metadata(new MetadataKey(ConfigStore.METAKEY, "switch1"), "10", null));
|
||||
itemRegistry.add(item);
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(item);
|
||||
assertThat(hueID, CoreMatchers.is("10"));
|
||||
|
||||
HueLightEntry device = cs.ds.lights.get(hueID);
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
|
||||
assertThat(cs.getHighestAssignedHueID(), CoreMatchers.is(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.events.Event;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.events.ItemCommandEvent;
|
||||
import org.openhab.core.library.items.ColorItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueGroupEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStateColorBulb;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueStatePlug;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
|
||||
|
||||
/**
|
||||
* Tests for {@link LightsAndGroups}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LightsAndGroupsTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
|
||||
LightsAndGroups subject = new LightsAndGroups();
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
itemRegistry = new DummyItemRegistry();
|
||||
|
||||
this.cs = commonSetup.cs;
|
||||
|
||||
subject.cs = cs;
|
||||
subject.eventPublisher = commonSetup.eventPublisher;
|
||||
subject.userManagement = commonSetup.userManagement;
|
||||
subject.itemRegistry = itemRegistry;
|
||||
subject.activate();
|
||||
|
||||
// Add simulated lights
|
||||
cs.ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
|
||||
cs.ds.lights.put("2", new HueLightEntry(new ColorItem("color"), "color", DeviceType.ColorType));
|
||||
cs.ds.lights.put("3", new HueLightEntry(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
|
||||
|
||||
// Add group item
|
||||
cs.ds.groups.put("10",
|
||||
new HueGroupEntry("name", new GroupItem("white", new SwitchItem("switch")), DeviceType.SwitchType));
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(subject));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addSwitchableByCategory() {
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.setCategory("Light");
|
||||
itemRegistry.add(item);
|
||||
HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addSwitchableByTag() {
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.addTag("Switchable");
|
||||
itemRegistry.add(item);
|
||||
HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ignoreByTag() {
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.addTags("Switchable", "internal"); // The ignore tag will win
|
||||
itemRegistry.add(item);
|
||||
HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device, is(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addGroupSwitchableByTag() {
|
||||
GroupItem item = new GroupItem("group1", new SwitchItem("switch1"));
|
||||
item.addTag("Switchable");
|
||||
itemRegistry.add(item);
|
||||
HueGroupEntry device = cs.ds.groups.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device.groupItem, is(item));
|
||||
assertThat(device.action, is(instanceOf(HueStatePlug.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addDeviceAsGroupSwitchableByTag() {
|
||||
GroupItem item = new GroupItem("group1", new SwitchItem("switch1"));
|
||||
item.addTag("Switchable");
|
||||
item.addTag("Huelight");
|
||||
itemRegistry.add(item);
|
||||
HueLightEntry device = cs.ds.lights.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addGroupWithoutTypeByTag() {
|
||||
GroupItem item = new GroupItem("group1", null);
|
||||
item.addTag("Switchable");
|
||||
|
||||
itemRegistry.add(item);
|
||||
|
||||
HueGroupEntry device = cs.ds.groups.get(cs.mapItemUIDtoHueID(item));
|
||||
assertThat(device.groupItem, is(item));
|
||||
assertThat(device.action, is(instanceOf(HueStatePlug.class)));
|
||||
assertThat(cs.ds.groups.get(cs.mapItemUIDtoHueID(item)).groupItem, is(item));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeGroupWithoutTypeAndTag() {
|
||||
String groupName = "group1";
|
||||
GroupItem item = new GroupItem(groupName, null);
|
||||
item.addTag("Switchable");
|
||||
itemRegistry.add(item);
|
||||
|
||||
String hueID = cs.mapItemUIDtoHueID(item);
|
||||
assertThat(cs.ds.groups.get(hueID), notNullValue());
|
||||
|
||||
subject.updated(item, new GroupItem(groupName, null));
|
||||
|
||||
assertThat(cs.ds.groups.get(hueID), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateSwitchable() {
|
||||
SwitchItem item = new SwitchItem("switch1");
|
||||
item.setLabel("labelOld");
|
||||
item.addTag("Switchable");
|
||||
itemRegistry.add(item);
|
||||
String hueID = cs.mapItemUIDtoHueID(item);
|
||||
HueLightEntry device = cs.ds.lights.get(hueID);
|
||||
assertThat(device.item, is(item));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
assertThat(device.name, is("labelOld"));
|
||||
|
||||
SwitchItem newitem = new SwitchItem("switch1");
|
||||
newitem.setLabel("labelNew");
|
||||
newitem.addTag("Switchable");
|
||||
subject.updated(item, newitem);
|
||||
device = cs.ds.lights.get(hueID);
|
||||
assertThat(device.item, is(newitem));
|
||||
assertThat(device.state, is(instanceOf(HueStatePlug.class)));
|
||||
assertThat(device.name, is("labelNew"));
|
||||
|
||||
// Update with an item that has no tags anymore -> should be removed
|
||||
SwitchItem newitemWithoutTag = new SwitchItem("switch1");
|
||||
newitemWithoutTag.setLabel("labelNew2");
|
||||
subject.updated(newitem, newitemWithoutTag);
|
||||
|
||||
device = cs.ds.lights.get(hueID);
|
||||
assertThat(device, nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeSwitchState() {
|
||||
assertThat(((HueStatePlug) cs.ds.lights.get("1").state).on, is(false));
|
||||
|
||||
String body = "{'on':true}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/1/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
assertThat(((HueStatePlug) cs.ds.lights.get("1").state).on, is(true));
|
||||
verify(commonSetup.eventPublisher).post(argThat((Event t) -> {
|
||||
assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeGroupItemSwitchState() {
|
||||
assertThat(((HueStatePlug) cs.ds.groups.get("10").action).on, is(false));
|
||||
|
||||
String body = "{'on':true}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/groups/10/action").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
assertThat(((HueStatePlug) cs.ds.groups.get("10").action).on, is(true));
|
||||
verify(commonSetup.eventPublisher).post(argThat((Event t) -> {
|
||||
assertThat(t.getPayload(), is("{\"type\":\"OnOff\",\"value\":\"ON\"}"));
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeOnValue() {
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
|
||||
|
||||
String body = "{'on':true}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
String entity = response.readEntity(String.class);
|
||||
assertThat(entity, is("[{\"success\":{\"/lights/2/state/on\":true}}]"));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeOnAndBriValues() {
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(1));
|
||||
|
||||
String body = "{'on':true,'bri':200}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeHueSatValues() {
|
||||
HueLightEntry hueDevice = cs.ds.lights.get("2");
|
||||
hueDevice.item.setState(OnOffType.ON);
|
||||
hueDevice.state.as(HueStateColorBulb.class).on = true;
|
||||
|
||||
String body = "{'hue':1000,'sat':50}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).hue, is(1000));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).sat, is(50));
|
||||
|
||||
verify(commonSetup.eventPublisher).post(argThat(ce -> assertHueValue((ItemCommandEvent) ce, 1000)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Amazon echos are setting ct only, if commanded to turn a light white.
|
||||
*/
|
||||
@Test
|
||||
public void changeCtValue() {
|
||||
HueLightEntry hueDevice = cs.ds.lights.get("2");
|
||||
hueDevice.item.setState(OnOffType.ON);
|
||||
hueDevice.state.as(HueStateColorBulb.class).on = true;
|
||||
|
||||
String body = "{'ct':500}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
body = response.readEntity(String.class);
|
||||
assertThat(body, containsString("success"));
|
||||
assertThat(body, containsString("ct"));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).ct, is(500));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).sat, is(0));
|
||||
|
||||
// Saturation is expected to be 0 -> white light
|
||||
verify(commonSetup.eventPublisher).post(argThat(ce -> assertSatValue((ItemCommandEvent) ce, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void switchOnWithXY() {
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(false));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(1));
|
||||
|
||||
String body = "{'on':true,'bri':200,'xy':[0.5119,0.4147]}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2/state").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).on, is(true));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).bri, is(200));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).xy[0], is(0.5119));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).xy[1], is(0.4147));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).colormode, is(HueStateColorBulb.ColorMode.xy));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getHue().intValue(),
|
||||
is((int) 27.47722590981918));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getSaturation().intValue(), is(88));
|
||||
assertThat(((HueStateColorBulb) cs.ds.lights.get("2").state).toHSBType().getBrightness().intValue(), is(78));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allLightsAndSingleLight() {
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights").request().get();
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
String body = response.readEntity(String.class);
|
||||
|
||||
assertThat(body, containsString("switch"));
|
||||
assertThat(body, containsString("color"));
|
||||
assertThat(body, containsString("white"));
|
||||
|
||||
// Single light access test
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/lights/2").request().get();
|
||||
assertEquals(200, response.getStatus());
|
||||
body = response.readEntity(String.class);
|
||||
assertThat(body, containsString("color"));
|
||||
}
|
||||
|
||||
private boolean assertHueValue(ItemCommandEvent ce, int hueValue) {
|
||||
assertThat(((HSBType) ce.getItemCommand()).getHue().intValue(), is(hueValue * 360 / HueStateColorBulb.MAX_HUE));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean assertSatValue(ItemCommandEvent ce, int satValue) {
|
||||
assertThat(((HSBType) ce.getItemCommand()).getSaturation().intValue(),
|
||||
is(satValue * 100 / HueStateColorBulb.MAX_SAT));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.library.items.ColorItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.automation.Condition;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.RuleUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueRuleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueRuleEntry.Operator;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyRuleRegistry;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Tests for various rules API endpoints.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RulesTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
|
||||
Rules subject = new Rules();
|
||||
LightsAndGroups lightsAndGroups = new LightsAndGroups();
|
||||
|
||||
private void addItemToReg(GenericItem item, State state, String tag, String label) {
|
||||
item.setState(state);
|
||||
item.setLabel(label);
|
||||
item.addTag(tag);
|
||||
itemRegistry.add(item);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
this.cs = commonSetup.cs;
|
||||
|
||||
itemRegistry = new DummyItemRegistry();
|
||||
ruleRegistry = new DummyRuleRegistry();
|
||||
|
||||
subject.cs = commonSetup.cs;
|
||||
subject.userManagement = commonSetup.userManagement;
|
||||
subject.ruleRegistry = ruleRegistry;
|
||||
subject.itemRegistry = itemRegistry;
|
||||
subject.activate();
|
||||
|
||||
// We need the LightsAndGroups class to convert registry entries into HueDatastore
|
||||
// light entries
|
||||
lightsAndGroups.cs = cs;
|
||||
lightsAndGroups.eventPublisher = commonSetup.eventPublisher;
|
||||
lightsAndGroups.userManagement = commonSetup.userManagement;
|
||||
lightsAndGroups.itemRegistry = itemRegistry;
|
||||
lightsAndGroups.activate();
|
||||
|
||||
addItemToReg(new SwitchItem("switch1"), OnOffType.ON, "Switchable", "name1");
|
||||
addItemToReg(new SwitchItem("switch2"), OnOffType.ON, "Switchable", "name2");
|
||||
addItemToReg(new ColorItem("color1"), HSBType.BLUE, "ColorLighting", "");
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(subject));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
RuleUtils.random = new Random();
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addUpdateRemoveScheduleToRegistry() {
|
||||
assertThat(cs.ds.lights.get("switch1"), is(notNullValue()));
|
||||
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/switch1/state", "PUT", "{'on':true}");
|
||||
HueRuleEntry.Condition condition = new HueRuleEntry.Condition("/lights/switch1/state/on", Operator.dx, null);
|
||||
|
||||
Entry<Trigger, Condition> triggerCond = Rules.hueConditionToAutomation(command.address.replace("/", "-"),
|
||||
condition, itemRegistry);
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Rules.RULES_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(triggerCond.getKey()).withConditions(triggerCond.getValue()).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
// Check hue entry
|
||||
HueRuleEntry entry = cs.ds.rules.get("demo1");
|
||||
assertThat(entry.conditions.get(0).address, is("/lights/switch1/state/on"));
|
||||
assertThat(entry.conditions.get(0).operator, is(Operator.dx));
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch1/state"));
|
||||
assertThat(entry.actions.get(0).method, is("PUT"));
|
||||
assertThat(entry.actions.get(0).body, is("{'on':true}"));
|
||||
|
||||
// Update
|
||||
command = new HueCommand("/api/testuser/lights/switch2/state", "PUT", "{'on':false}");
|
||||
rule = RuleBuilder.create("demo1").withName("name2").withTags(Rules.RULES_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(triggerCond.getKey()).withConditions(triggerCond.getValue()).build();
|
||||
ruleRegistry.update(rule);
|
||||
|
||||
entry = cs.ds.rules.get("demo1");
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch2/state"));
|
||||
assertThat(entry.actions.get(0).method, is("PUT"));
|
||||
assertThat(entry.actions.get(0).body, is("{'on':false}"));
|
||||
assertThat(entry.name, is("name2"));
|
||||
|
||||
// Remove
|
||||
|
||||
ruleRegistry.remove("demo1");
|
||||
entry = cs.ds.rules.get("demo1");
|
||||
assertThat(entry, nullValue());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void addGetRemoveRuleViaRest() {
|
||||
// 1. Create
|
||||
String body = "{\"name\":\"test name\",\"description\":\"\",\"owner\":\"\",\"conditions\":[{\"address\":\"/lights/switch1/state/on\",\"operator\":\"dx\"}],\"actions\":[{\"address\":\"/lights/switch1/state\",\"method\":\"PUT\",\"body\":\"{\\u0027on\\u0027:true}\"}]}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules").request()
|
||||
.post(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
|
||||
// 1.1 Check for entry
|
||||
Entry<String, HueRuleEntry> idAndEntry = cs.ds.rules.entrySet().stream().findAny().get();
|
||||
HueRuleEntry entry = idAndEntry.getValue();
|
||||
assertThat(entry.name, is("test name"));
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch1/state"));
|
||||
assertThat(entry.conditions.get(0).address, is("/lights/switch1/state/on"));
|
||||
|
||||
// 1.2 Check for rule
|
||||
Rule rule = ruleRegistry.get(idAndEntry.getKey());
|
||||
assertThat(rule.getName(), is("test name"));
|
||||
assertThat(rule.getActions().get(0).getId(), is("-api-testuser-lights-switch1-state"));
|
||||
assertThat(rule.getActions().get(0).getTypeUID(), is("rules.HttpAction"));
|
||||
|
||||
// 2. Get
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules/" + idAndEntry.getKey()).request()
|
||||
.get();
|
||||
assertEquals(200, response.getStatus());
|
||||
HueSceneEntry fromJson = new Gson().fromJson(response.readEntity(String.class), HueSceneEntry.class);
|
||||
assertThat(fromJson.name, is(idAndEntry.getValue().name));
|
||||
|
||||
// 3. Remove
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules/" + idAndEntry.getKey()).request()
|
||||
.delete();
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue(cs.ds.rules.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRuleViaRest() {
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/switch1/state", "PUT", "{'on':true}");
|
||||
HueRuleEntry.Condition condition = new HueRuleEntry.Condition("/lights/switch1/state/on", Operator.dx, null);
|
||||
|
||||
Entry<Trigger, Condition> triggerCond = Rules.hueConditionToAutomation(command.address.replace("/", "-"),
|
||||
condition, itemRegistry);
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Rules.RULES_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(triggerCond.getKey()).withConditions(triggerCond.getValue()).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
// Modify (just the name)
|
||||
String body = "{ 'name':'A new name'}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("name"));
|
||||
|
||||
Entry<String, HueRuleEntry> idAndEntry = cs.ds.rules.entrySet().stream().findAny().get();
|
||||
HueRuleEntry entry = idAndEntry.getValue();
|
||||
assertThat(entry.name, is("A new name"));
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch1/state"));
|
||||
assertThat(entry.conditions.get(0).address, is("/lights/switch1/state/on"));
|
||||
|
||||
// Reset
|
||||
rule = RuleBuilder.create("demo1").withName("test name").withTags(Rules.RULES_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(triggerCond.getKey()).withConditions(triggerCond.getValue()).build();
|
||||
|
||||
ruleRegistry.update(rule); // Reset rule
|
||||
|
||||
idAndEntry = cs.ds.rules.entrySet().stream().findAny().get();
|
||||
|
||||
// Modify (Change condition)
|
||||
body = "{\"conditions\":[{\"address\":\"/lights/switch1/state/on\",\"operator\":\"ddx\"}]}";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("conditions"));
|
||||
|
||||
idAndEntry = cs.ds.rules.entrySet().stream().findAny().get();
|
||||
entry = idAndEntry.getValue();
|
||||
assertThat(entry.name, is("test name")); // should not have changed
|
||||
assertThat(entry.conditions.get(0).operator, is(Operator.ddx));
|
||||
|
||||
// Modify (Change action)
|
||||
body = "{\"actions\":[{\"address\":\"/lights/switch2/state\",\"method\":\"PUT\",\"body\":\"{\\u0027on\\u0027:false}\"}]}";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("actions"));
|
||||
|
||||
idAndEntry = cs.ds.rules.entrySet().stream().findAny().get();
|
||||
entry = idAndEntry.getValue();
|
||||
assertThat(entry.name, is("test name")); // should not have changed
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch2/state"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAll() {
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/switch1/state", "PUT", "{'on':true}");
|
||||
HueRuleEntry.Condition condition = new HueRuleEntry.Condition("/lights/switch1/state/on", Operator.dx, null);
|
||||
|
||||
Entry<Trigger, Condition> triggerCond = Rules.hueConditionToAutomation(command.address.replace("/", "-"),
|
||||
condition, itemRegistry);
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Rules.RULES_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(triggerCond.getKey()).withConditions(triggerCond.getValue()).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/rules").request().get();
|
||||
Type type = new TypeToken<Map<String, HueRuleEntry>>() {
|
||||
}.getType();
|
||||
String body = response.readEntity(String.class);
|
||||
Map<String, HueRuleEntry> fromJson = new Gson().fromJson(body, type);
|
||||
HueRuleEntry entry = fromJson.get("demo1");
|
||||
assertThat(entry.name, is("test name"));
|
||||
assertThat(entry.actions.get(0).address, is("/lights/switch1/state"));
|
||||
assertThat(entry.conditions.get(0).address, is("/lights/switch1/state/on"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.GroupItem;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.library.items.ColorItem;
|
||||
import org.openhab.core.library.items.DimmerItem;
|
||||
import org.openhab.core.library.items.RollershutterItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyRuleRegistry;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Tests for various scene API endpoints.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SceneTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
|
||||
Scenes subject = new Scenes();
|
||||
|
||||
private void addItemToReg(GenericItem item, State state, String tag) {
|
||||
item.setState(state);
|
||||
item.addTag(tag);
|
||||
itemRegistry.add(item);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
this.cs = commonSetup.cs;
|
||||
|
||||
itemRegistry = new DummyItemRegistry();
|
||||
ruleRegistry = new DummyRuleRegistry();
|
||||
|
||||
subject.cs = commonSetup.cs;
|
||||
subject.userManagement = commonSetup.userManagement;
|
||||
subject.itemRegistry = itemRegistry;
|
||||
subject.ruleRegistry = ruleRegistry;
|
||||
subject.activate();
|
||||
|
||||
// Add simulated lights
|
||||
addItemToReg(new SwitchItem("switch1"), OnOffType.ON, "Switchable");
|
||||
addItemToReg(new ColorItem("color1"), HSBType.BLUE, "ColorLighting");
|
||||
addItemToReg(new DimmerItem("white1"), new PercentType(12), "Lighting");
|
||||
addItemToReg(new RollershutterItem("roller1"), new PercentType(12), "Lighting");
|
||||
addItemToReg(new DimmerItem("white1"), new PercentType(12), "Lighting");
|
||||
addItemToReg(new GroupItem("group1"), OnOffType.ON, "Switchable");
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(subject));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void addUpdateRemoveSceneToRegistry() {
|
||||
Rule rule = RuleBuilder.create("demo1").withTags("scene") //
|
||||
.withActions(Scenes.actionFromState("switch1", (Command) OnOffType.ON)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
HueSceneEntry sceneEntry = cs.ds.scenes.get("demo1");
|
||||
assertThat(sceneEntry.lights.get(0), CoreMatchers.is("switch1"));
|
||||
|
||||
// Update
|
||||
rule = RuleBuilder.create("demo1").withTags("scene") //
|
||||
.withActions(Scenes.actionFromState("white1", (Command) OnOffType.ON)).build();
|
||||
ruleRegistry.update(rule);
|
||||
|
||||
sceneEntry = cs.ds.scenes.get("demo1");
|
||||
assertThat(sceneEntry.lights.get(0), CoreMatchers.is("white1"));
|
||||
|
||||
// Remove
|
||||
|
||||
ruleRegistry.remove("demo1");
|
||||
sceneEntry = cs.ds.scenes.get("demo1");
|
||||
assertThat(sceneEntry, CoreMatchers.nullValue());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void addGetRemoveSceneViaRest() {
|
||||
// 1. Create
|
||||
String body = "{ 'name':'Cozy dinner', 'recycle':false, 'lights':['switch1','white1'], 'type':'LightScene'}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes").request()
|
||||
.post(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
|
||||
// 1.1 Check for scene entry
|
||||
Entry<String, HueSceneEntry> entry = cs.ds.scenes.entrySet().stream().findAny().get();
|
||||
assertThat(entry.getValue().name, is("Cozy dinner"));
|
||||
assertThat(entry.getValue().lights.get(0), is("switch1"));
|
||||
assertThat(entry.getValue().lights.get(1), is("white1"));
|
||||
|
||||
// 1.2 Check for rule
|
||||
Rule rule = ruleRegistry.get(entry.getKey());
|
||||
assertThat(rule.getName(), is("Cozy dinner"));
|
||||
assertThat(rule.getActions().get(0).getId(), is("switch1"));
|
||||
assertThat(rule.getActions().get(1).getId(), is("white1"));
|
||||
|
||||
// 2. Get
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes/" + entry.getKey()).request()
|
||||
.get();
|
||||
assertEquals(200, response.getStatus());
|
||||
HueSceneEntry fromJson = new Gson().fromJson(response.readEntity(String.class), HueSceneEntry.class);
|
||||
assertThat(fromJson.name, is(entry.getValue().name));
|
||||
|
||||
// 3. Remove
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes/" + entry.getKey()).request()
|
||||
.delete();
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue(cs.ds.scenes.isEmpty());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void updateSceneViaRest() {
|
||||
Rule rule = RuleBuilder.create("demo1").withTags("scene").withName("Some name") //
|
||||
.withActions(Scenes.actionFromState("switch1", (Command) OnOffType.ON)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
// 3. Modify (just the name)
|
||||
String body = "{ 'name':'A new name'}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("name"));
|
||||
|
||||
Entry<String, HueSceneEntry> sceneEntry = cs.ds.scenes.entrySet().stream().findAny().get();
|
||||
assertThat(sceneEntry.getValue().name, is("A new name"));
|
||||
assertThat(sceneEntry.getValue().lights.get(0), is("switch1")); // nothing else should have changed
|
||||
|
||||
// 3. Modify (just the lights)
|
||||
rule = RuleBuilder.create("demo1").withTags("scene").withName("Some name") //
|
||||
.withActions(Scenes.actionFromState("switch1", (Command) OnOffType.ON)).build();
|
||||
|
||||
ruleRegistry.update(rule); // Reset rule
|
||||
|
||||
sceneEntry = cs.ds.scenes.entrySet().stream().findAny().get();
|
||||
String uid = sceneEntry.getKey();
|
||||
|
||||
// Without store lights
|
||||
body = "{ 'lights':['white1']}";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("lights"));
|
||||
|
||||
sceneEntry = cs.ds.scenes.entrySet().stream().findAny().get();
|
||||
assertThat(sceneEntry.getValue().name, is("Some name")); // should not have changed
|
||||
assertThat(sceneEntry.getKey(), is(uid));
|
||||
assertThat(sceneEntry.getValue().lights.get(0), is("switch1")); // storelightstate not set, lights not changed
|
||||
|
||||
// With store lights
|
||||
body = "{ 'lights':['white1'], 'storelightstate':true }";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("lights"));
|
||||
|
||||
sceneEntry = cs.ds.scenes.entrySet().stream().findAny().get();
|
||||
assertThat(sceneEntry.getValue().lights.get(0), is("white1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAll() {
|
||||
Rule rule = RuleBuilder.create("demo1").withTags("scene") //
|
||||
.withActions(Scenes.actionFromState("switch1", (Command) OnOffType.ON)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/scenes").request().get();
|
||||
Type type = new TypeToken<Map<String, HueSceneEntry>>() {
|
||||
}.getType();
|
||||
Map<String, HueSceneEntry> fromJson = new Gson().fromJson(response.readEntity(String.class), type);
|
||||
assertTrue(fromJson.containsKey("demo1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.items.ColorItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleManager;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
import org.openhab.core.automation.Trigger;
|
||||
import org.openhab.core.automation.util.RuleBuilder;
|
||||
import org.openhab.core.automation.util.TriggerBuilder;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.DeviceType;
|
||||
import org.openhab.io.hueemulation.internal.RuleUtils;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueLightEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueSceneEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueScheduleEntry;
|
||||
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCommand;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyRuleRegistry;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Tests for various schedule API endpoints.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ScheduleTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
protected @NonNullByDefault({}) RuleRegistry ruleRegistry;
|
||||
|
||||
Schedules subject = new Schedules();
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
this.cs = commonSetup.cs;
|
||||
|
||||
ruleRegistry = new DummyRuleRegistry();
|
||||
|
||||
subject.cs = commonSetup.cs;
|
||||
subject.userManagement = commonSetup.userManagement;
|
||||
subject.ruleManager = mock(RuleManager.class);
|
||||
when(subject.ruleManager.isEnabled(anyString())).thenReturn(true);
|
||||
subject.ruleRegistry = ruleRegistry;
|
||||
subject.activate();
|
||||
|
||||
// Add simulated lights
|
||||
cs.ds.lights.put("1", new HueLightEntry(new SwitchItem("switch"), "switch", DeviceType.SwitchType));
|
||||
cs.ds.lights.put("2", new HueLightEntry(new ColorItem("color"), "color", DeviceType.ColorType));
|
||||
cs.ds.lights.put("3", new HueLightEntry(new ColorItem("white"), "white", DeviceType.WhiteTemperatureType));
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(subject));
|
||||
|
||||
// Mock random -> always return int=10 or the highest possible int if bounded
|
||||
Random random = mock(Random.class);
|
||||
doReturn(10).when(random).nextInt();
|
||||
doAnswer(a -> {
|
||||
Integer bound = a.getArgument(0);
|
||||
return bound - 1;
|
||||
}).when(random).nextInt(anyInt());
|
||||
RuleUtils.random = random;
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
RuleUtils.random = new Random();
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void addUpdateRemoveScheduleToRegistry() {
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
|
||||
String localtime = "2020-02-01T12:12:00";
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
// Check hue entry
|
||||
HueScheduleEntry sceneEntry = cs.ds.schedules.get("demo1");
|
||||
assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
|
||||
assertThat(sceneEntry.command.method, is("PUT"));
|
||||
assertThat(sceneEntry.command.body, is("{'on':true}"));
|
||||
assertThat(sceneEntry.localtime, is(localtime));
|
||||
|
||||
// Update
|
||||
localtime = "2021-03-01T17:12:00";
|
||||
rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
|
||||
ruleRegistry.update(rule);
|
||||
|
||||
sceneEntry = cs.ds.schedules.get("demo1");
|
||||
assertThat(sceneEntry.command.address, is("/api/testuser/lights/1/state"));
|
||||
assertThat(sceneEntry.localtime, is(localtime));
|
||||
|
||||
// Remove
|
||||
|
||||
ruleRegistry.remove("demo1");
|
||||
sceneEntry = cs.ds.schedules.get("demo1");
|
||||
assertThat(sceneEntry, nullValue());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void addGetRemoveScheduleViaRest() {
|
||||
// 1. Create
|
||||
String body = "{ 'name':'Wake up', 'description':'My wake up alarm', 'localtime':'2015-06-30T14:24:40'," + //
|
||||
"'command':{'address':'/api/testuser/lights/1/state','method':'PUT','body':'{\"on\":true}'} }";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules").request()
|
||||
.post(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("success"));
|
||||
|
||||
// 1.1 Check for entry
|
||||
Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
|
||||
assertThat(entry.getValue().name, is("Wake up"));
|
||||
assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state"));
|
||||
assertThat(entry.getValue().command.method, is("PUT"));
|
||||
assertThat(entry.getValue().command.body, is("{\"on\":true}"));
|
||||
assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
|
||||
|
||||
// 1.2 Check for rule
|
||||
Rule rule = ruleRegistry.get(entry.getKey());
|
||||
assertThat(rule.getName(), is("Wake up"));
|
||||
assertThat(rule.getActions().get(0).getId(), is("command"));
|
||||
assertThat(rule.getActions().get(0).getTypeUID(), is("rules.HttpAction"));
|
||||
|
||||
// 2. Get
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/" + entry.getKey()).request()
|
||||
.get();
|
||||
assertEquals(200, response.getStatus());
|
||||
HueSceneEntry fromJson = new Gson().fromJson(response.readEntity(String.class), HueSceneEntry.class);
|
||||
assertThat(fromJson.name, is(entry.getValue().name));
|
||||
|
||||
// 3. Remove
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/" + entry.getKey()).request()
|
||||
.delete();
|
||||
assertEquals(200, response.getStatus());
|
||||
assertTrue(cs.ds.schedules.isEmpty());
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Test
|
||||
public void updateScheduleViaRest() {
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/1/state", "PUT", "{'on':true}");
|
||||
String localtime = "2020-02-01T12:12:00";
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
// Modify (just the name)
|
||||
String body = "{ 'name':'A new name'}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("name"));
|
||||
|
||||
Entry<String, HueScheduleEntry> entry = cs.ds.schedules.entrySet().stream().findAny().get();
|
||||
assertThat(entry.getValue().name, is("A new name"));
|
||||
assertThat(entry.getValue().command.address, is("/api/testuser/lights/1/state")); // nothing else should have
|
||||
// changed
|
||||
assertThat(entry.getValue().localtime, is(localtime));
|
||||
|
||||
// Reset
|
||||
rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
|
||||
|
||||
ruleRegistry.update(rule); // Reset rule
|
||||
|
||||
entry = cs.ds.schedules.entrySet().stream().findAny().get();
|
||||
String uid = entry.getKey();
|
||||
|
||||
// Modify (Change time)
|
||||
body = "{ 'localtime':'2015-06-30T14:24:40'}";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("localtime"));
|
||||
|
||||
entry = cs.ds.schedules.entrySet().stream().findAny().get();
|
||||
assertThat(entry.getValue().name, is("test name")); // should not have changed
|
||||
assertThat(entry.getKey(), is(uid));
|
||||
assertThat(entry.getValue().localtime, is("2015-06-30T14:24:40"));
|
||||
|
||||
// Modify (Change command)
|
||||
body = "{ 'command':{'address':'/api/testuser/lights/2/state','method':'PUT','body':'{\"on\":true}'} }";
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules/demo1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.readEntity(String.class), containsString("command"));
|
||||
|
||||
entry = cs.ds.schedules.entrySet().stream().findAny().get();
|
||||
assertThat(entry.getValue().name, is("test name")); // should not have changed
|
||||
assertThat(entry.getKey(), is(uid));
|
||||
assertThat(entry.getValue().command.address, is("/api/testuser/lights/2/state"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAll() {
|
||||
HueCommand command = new HueCommand("/api/testuser/lights/1/state", "POST", "{'on':true}");
|
||||
String localtime = "2020-02-01T12:12:00";
|
||||
|
||||
Rule rule = RuleBuilder.create("demo1").withName("test name").withTags(Schedules.SCHEDULE_TAG) //
|
||||
.withActions(RuleUtils.createHttpAction(command, "command")) //
|
||||
.withTriggers(RuleUtils.createTriggerForTimeString(localtime)).build();
|
||||
|
||||
ruleRegistry.add(rule);
|
||||
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/schedules").request().get();
|
||||
Type type = new TypeToken<Map<String, HueSceneEntry>>() {
|
||||
}.getType();
|
||||
Map<String, HueSceneEntry> fromJson = new Gson().fromJson(response.readEntity(String.class), type);
|
||||
assertTrue(fromJson.containsKey("demo1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeStringToTrigger() {
|
||||
String timeString;
|
||||
Trigger trigger;
|
||||
Configuration configuration;
|
||||
|
||||
// absolute time
|
||||
timeString = "2020-02-01T12:12:00";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
|
||||
assertThat(configuration.get("date"), is("2020-02-01"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
|
||||
// absolute randomized time
|
||||
timeString = "2020-02-01T12:12:00A14:12:34";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.AbsoluteDateTimeTrigger"));
|
||||
assertThat(configuration.get("date"), is("2020-02-01"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
assertThat(configuration.get("randomizeTime"), is("14:12:34"));
|
||||
|
||||
// Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
|
||||
// Cron expression: min hour day month weekdays
|
||||
timeString = "W3/T12:15:17";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
|
||||
assertThat(configuration.get("cronExpression"), is("15 12 * * 6,7"));
|
||||
|
||||
// Recurring randomized times
|
||||
timeString = "W127/T12:15:17A14:12:34";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.GenericCronTrigger"));
|
||||
assertThat(configuration.get("cronExpression"), is("15 14 * * 1,2,3,4,5,6,7"));
|
||||
|
||||
// Timer, expiring after given time
|
||||
timeString = "PT12:12:00";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
|
||||
// Timer with random element
|
||||
timeString = "PT12:12:00A14:12:34";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
assertThat(configuration.get("randomizeTime"), is("14:12:34"));
|
||||
|
||||
// Timers, Recurring timer
|
||||
timeString = "R/PT12:12:00";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
assertThat(configuration.get("repeat"), is("-1"));
|
||||
|
||||
// Recurring timer with random element
|
||||
timeString = "R12/PT12:12:00A14:12:34";
|
||||
trigger = RuleUtils.createTriggerForTimeString(timeString);
|
||||
configuration = trigger.getConfiguration();
|
||||
|
||||
assertThat(trigger.getTypeUID(), is("timer.TimerTrigger"));
|
||||
assertThat(configuration.get("time"), is("12:12:00"));
|
||||
assertThat(configuration.get("randomizeTime"), is("14:12:34"));
|
||||
assertThat(configuration.get("repeat"), is("12"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void triggerToTimestring() {
|
||||
String timeString;
|
||||
Trigger trigger;
|
||||
Configuration configuration;
|
||||
|
||||
// absolute time
|
||||
configuration = new Configuration();
|
||||
configuration.put("date", "2020-02-01");
|
||||
configuration.put("time", "12:12:00");
|
||||
trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("2020-02-01T12:12:00"));
|
||||
|
||||
// absolute randomized time
|
||||
configuration = new Configuration();
|
||||
configuration.put("date", "2020-02-01");
|
||||
configuration.put("time", "12:12:00");
|
||||
configuration.put("randomizeTime", "14:12:34");
|
||||
trigger = TriggerBuilder.create().withId("absolutetrigger").withTypeUID("timer.AbsoluteDateTimeTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("2020-02-01T12:12:00A14:12:34"));
|
||||
|
||||
// Recurring times,Monday = 64, Tuesday = 32, Wednesday = 16, Thursday = 8, Friday = 4, Saturday = 2, Sunday= 1
|
||||
// Cron expression: min hour day month weekdays
|
||||
configuration = new Configuration();
|
||||
configuration.put("cronExpression", "15 12 * * 6,7");
|
||||
trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("W3/T12:15:00"));
|
||||
|
||||
// Recurring randomized times (not possible, the cron rule has no way to store that info)
|
||||
configuration = new Configuration();
|
||||
configuration.put("cronExpression", "15 14 * * 1,2,3,4,5,6,7");
|
||||
trigger = TriggerBuilder.create().withId("crontrigger").withTypeUID("timer.GenericCronTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("W127/T14:15:00"));
|
||||
|
||||
// Timer, expiring after given time
|
||||
configuration = new Configuration();
|
||||
configuration.put("time", "12:12:00");
|
||||
trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("PT12:12:00"));
|
||||
|
||||
// Timer with random element
|
||||
configuration = new Configuration();
|
||||
configuration.put("time", "12:12:00");
|
||||
configuration.put("randomizeTime", "14:12:34");
|
||||
trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("PT12:12:00A14:12:34"));
|
||||
|
||||
// Timers, Recurring timer
|
||||
configuration = new Configuration();
|
||||
configuration.put("time", "12:12:00");
|
||||
configuration.put("repeat", -1);
|
||||
trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("R/PT12:12:00"));
|
||||
|
||||
// Recurring timer with random element
|
||||
configuration = new Configuration();
|
||||
configuration.put("time", "12:12:00");
|
||||
configuration.put("randomizeTime", "14:12:34");
|
||||
configuration.put("repeat", 12);
|
||||
trigger = TriggerBuilder.create().withId("timertrigger").withTypeUID("timer.TimerTrigger")
|
||||
.withConfiguration(configuration).build();
|
||||
timeString = RuleUtils.timeStringFromTrigger(Collections.singletonList(trigger));
|
||||
|
||||
assertThat(timeString, is("R12/PT12:12:00A14:12:34"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.items.GenericItem;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.library.items.ColorItem;
|
||||
import org.openhab.core.library.items.ContactItem;
|
||||
import org.openhab.core.library.items.DimmerItem;
|
||||
import org.openhab.core.library.items.NumberItem;
|
||||
import org.openhab.core.library.items.RollershutterItem;
|
||||
import org.openhab.core.library.items.SwitchItem;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.HSBType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.OpenClosedType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.DummyItemRegistry;
|
||||
|
||||
/**
|
||||
* Tests for {@link Sensors}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SensorTests {
|
||||
protected @NonNullByDefault({}) CommonSetup commonSetup;
|
||||
protected @NonNullByDefault({}) ItemRegistry itemRegistry;
|
||||
protected @NonNullByDefault({}) ConfigStore cs;
|
||||
|
||||
Sensors subject = new Sensors();
|
||||
|
||||
private void addItemToReg(GenericItem item, State state, String label) {
|
||||
item.setState(state);
|
||||
item.setLabel(label);
|
||||
itemRegistry.add(item);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
itemRegistry = new DummyItemRegistry();
|
||||
|
||||
this.cs = commonSetup.cs;
|
||||
|
||||
subject.cs = cs;
|
||||
subject.userManagement = commonSetup.userManagement;
|
||||
subject.itemRegistry = itemRegistry;
|
||||
subject.activate();
|
||||
|
||||
// Add simulated sensor items
|
||||
addItemToReg(new SwitchItem("switch1"), OnOffType.ON, "name1");
|
||||
addItemToReg(new ContactItem("contact1"), OpenClosedType.OPEN, "");
|
||||
addItemToReg(new ColorItem("color1"), HSBType.BLUE, "");
|
||||
addItemToReg(new DimmerItem("white1"), new PercentType(12), "");
|
||||
addItemToReg(new RollershutterItem("roller1"), new PercentType(12), "");
|
||||
addItemToReg(new NumberItem("number1"), new DecimalType(12), "");
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(subject));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renameSensor() {
|
||||
assertThat(cs.ds.sensors.get("switch1").name, is("name1"));
|
||||
|
||||
String body = "{'name':'name2'}";
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/sensors/switch1").request()
|
||||
.put(Entity.json(body));
|
||||
assertEquals(200, response.getStatus());
|
||||
body = response.readEntity(String.class);
|
||||
assertThat(body, containsString("success"));
|
||||
assertThat(body, containsString("name"));
|
||||
assertThat(cs.ds.sensors.get("switch1").name, is("name2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allAndSingleSensor() {
|
||||
Response response = commonSetup.client.target(commonSetup.basePath + "/testuser/sensors").request().get();
|
||||
assertEquals(200, response.getStatus());
|
||||
|
||||
String body = response.readEntity(String.class);
|
||||
|
||||
assertThat(body, containsString("switch1"));
|
||||
assertThat(body, containsString("color1"));
|
||||
assertThat(body, containsString("white1"));
|
||||
|
||||
// Single light access test
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/testuser/sensors/switch1").request().get();
|
||||
assertEquals(200, response.getStatus());
|
||||
body = response.readEntity(String.class);
|
||||
assertThat(body, containsString("CLIPGenericFlag"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Dictionary;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.openhab.io.hueemulation.internal.HueEmulationConfig;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueUnauthorizedConfig;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
|
||||
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseCreateUser;
|
||||
import org.openhab.io.hueemulation.internal.rest.mocks.ConfigStoreWithoutMetadata;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Tests for various user management API endpoints.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class UsersAndConfigTests {
|
||||
|
||||
ConfigurationAccess configurationAccess = new ConfigurationAccess();
|
||||
|
||||
CommonSetup commonSetup;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
commonSetup = new CommonSetup(false);
|
||||
|
||||
configurationAccess.cs = commonSetup.cs;
|
||||
configurationAccess.userManagement = commonSetup.userManagement;
|
||||
configurationAccess.configAdmin = commonSetup.configAdmin;
|
||||
|
||||
commonSetup.start(new ResourceConfig().registerInstances(configurationAccess));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidUser() {
|
||||
assertFalse(commonSetup.userManagement.authorizeUser("blub"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validUser() {
|
||||
assertTrue(commonSetup.userManagement.authorizeUser("testuser"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configStoreRestartOnNoUUID() {
|
||||
ConfigStore configStore = new ConfigStoreWithoutMetadata(commonSetup.networkAddressService,
|
||||
commonSetup.configAdmin, commonSetup.scheduler);
|
||||
|
||||
// No uuid known yet
|
||||
assertThat(configStore.ds.config.uuid, is(""));
|
||||
configStore.activate(Collections.emptyMap());
|
||||
|
||||
assertThat(configStore.getConfig().uuid, not(is("")));
|
||||
// The config admin service was requested for the service config
|
||||
Mockito.verify(commonSetup.configAdminConfig).getProperties();
|
||||
Dictionary<String, Object> p = commonSetup.configAdminConfig.getProperties();
|
||||
// And the service config was updated
|
||||
assertThat(p.get(HueEmulationConfig.CONFIG_UUID), is(configStore.getConfig().uuid));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addUser() {
|
||||
// GET should fail
|
||||
assertEquals(405, commonSetup.client.target(commonSetup.basePath).request().get().getStatus());
|
||||
|
||||
String body = "{'username':'testuser','devicetype':'app#device'}";
|
||||
|
||||
Response response;
|
||||
HueResponse[] r;
|
||||
|
||||
// Post should create a user, except: if linkbutton not enabled
|
||||
response = commonSetup.client.target(commonSetup.basePath).request().post(Entity.json(body));
|
||||
assertThat(response.getStatus(), is(200));
|
||||
r = commonSetup.cs.gson.fromJson(response.readEntity(String.class), HueResponse[].class);
|
||||
assertNotNull(r[0].error);
|
||||
|
||||
// Post should create a user
|
||||
commonSetup.cs.ds.config.linkbutton = true;
|
||||
response = commonSetup.client.target(commonSetup.basePath).request().post(Entity.json(body));
|
||||
assertThat(response.getStatus(), is(200));
|
||||
|
||||
JsonElement e = new JsonParser().parse(response.readEntity(String.class)).getAsJsonArray().get(0);
|
||||
e = e.getAsJsonObject().get("success");
|
||||
HueSuccessResponseCreateUser rc = commonSetup.cs.gson.fromJson(e, HueSuccessResponseCreateUser.class);
|
||||
assertNotNull(rc);
|
||||
assertThat(commonSetup.cs.ds.config.whitelist.get(rc.username).name, is("app#device"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void UnauthorizedAccessTest() {
|
||||
// Unauthorized config
|
||||
Response response;
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/config").request().get();
|
||||
assertThat(response.getStatus(), is(200));
|
||||
HueUnauthorizedConfig config = new Gson().fromJson(response.readEntity(String.class),
|
||||
HueUnauthorizedConfig.class);
|
||||
assertThat(config.bridgeid, is(commonSetup.cs.ds.config.bridgeid));
|
||||
assertThat(config.name, is(commonSetup.cs.ds.config.name));
|
||||
|
||||
// Invalid user name
|
||||
response = commonSetup.client.target(commonSetup.basePath + "/invalid/config").request().get();
|
||||
assertThat(response.getStatus(), is(403));
|
||||
assertThat(response.readEntity(String.class), containsString("error"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest.mocks;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.io.hueemulation.internal.ConfigStore;
|
||||
import org.osgi.service.cm.ConfigurationAdmin;
|
||||
|
||||
/**
|
||||
* Usually the metadata registry is used to map item UIDs to integer hue IDs.
|
||||
* For tests we do not need this extra complexity and just map the item UID to the hue ID.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class ConfigStoreWithoutMetadata extends ConfigStore {
|
||||
|
||||
public ConfigStoreWithoutMetadata(NetworkAddressService networkAddressService, ConfigurationAdmin configAdmin,
|
||||
ScheduledExecutorService scheduler) {
|
||||
super(networkAddressService, configAdmin, null, scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineHighestAssignedHueID() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String mapItemUIDtoHueID(@Nullable Item item) {
|
||||
if (item == null) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return item.getUID();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest.mocks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.items.Item;
|
||||
import org.openhab.core.items.ItemRegistry;
|
||||
import org.openhab.core.items.RegistryHook;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DummyItemRegistry implements ItemRegistry {
|
||||
Map<String, Item> items = new TreeMap<>();
|
||||
List<RegistryChangeListener<Item>> listeners = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getAll() {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Item> stream() {
|
||||
return items.values().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Item get(String key) {
|
||||
return items.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item add(Item element) {
|
||||
Item put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Item> l : listeners) {
|
||||
l.added(element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Item update(Item element) {
|
||||
Item put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Item> l : listeners) {
|
||||
l.updated(put, element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Item remove(String key) {
|
||||
Item put = items.remove(key);
|
||||
for (RegistryChangeListener<Item> l : listeners) {
|
||||
l.removed(put);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item getItem(String name) {
|
||||
return items.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item getItemByPattern(String name) {
|
||||
return items.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getItems() {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getItemsOfType(String type) {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getItems(String pattern) {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getItemsByTag(String... tags) {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Item> getItemsByTagAndType(String type, String... tags) {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
|
||||
return (Collection<T>) items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Item remove(String itemName, boolean recursive) {
|
||||
Item put = items.remove(itemName);
|
||||
for (RegistryChangeListener<Item> l : listeners) {
|
||||
l.removed(put);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRegistryHook(RegistryHook<Item> hook) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegistryHook(RegistryHook<Item> hook) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest.mocks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.items.Metadata;
|
||||
import org.openhab.core.items.MetadataKey;
|
||||
import org.openhab.core.items.MetadataRegistry;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DummyMetadataRegistry implements MetadataRegistry {
|
||||
Map<MetadataKey, Metadata> items = new HashMap<>();
|
||||
List<RegistryChangeListener<Metadata>> listeners = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void addRegistryChangeListener(RegistryChangeListener<Metadata> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Metadata> getAll() {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Metadata> stream() {
|
||||
return items.values().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Metadata get(@Nullable MetadataKey key) {
|
||||
return items.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegistryChangeListener(RegistryChangeListener<Metadata> listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata add(Metadata element) {
|
||||
Metadata put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Metadata> l : listeners) {
|
||||
l.added(element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Metadata update(Metadata element) {
|
||||
Metadata put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Metadata> l : listeners) {
|
||||
l.updated(put, element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Metadata remove(MetadataKey key) {
|
||||
Metadata put = items.remove(key);
|
||||
for (RegistryChangeListener<Metadata> l : listeners) {
|
||||
l.removed(put);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternalNamespace(String namespace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeItemMetadata(String name) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest.mocks;
|
||||
|
||||
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.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||
import org.openhab.core.automation.Rule;
|
||||
import org.openhab.core.automation.RuleRegistry;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DummyRuleRegistry implements RuleRegistry {
|
||||
Map<String, Rule> items = new HashMap<>();
|
||||
List<RegistryChangeListener<Rule>> listeners = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void addRegistryChangeListener(RegistryChangeListener<Rule> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Rule> getAll() {
|
||||
return items.values();
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public Stream<Rule> stream() {
|
||||
return items.values().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Rule get(@Nullable String key) {
|
||||
return items.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRegistryChangeListener(RegistryChangeListener<Rule> listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Rule add(Rule element) {
|
||||
Rule put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Rule> l : listeners) {
|
||||
l.added(element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Rule update(Rule element) {
|
||||
Rule put = items.put(element.getUID(), element);
|
||||
for (RegistryChangeListener<Rule> l : listeners) {
|
||||
l.updated(put, element);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Rule remove(String key) {
|
||||
Rule put = items.remove(key);
|
||||
for (RegistryChangeListener<Rule> l : listeners) {
|
||||
l.removed(put);
|
||||
}
|
||||
return put;
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public Collection<Rule> getByTag(String tag) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@NonNullByDefault({})
|
||||
@Override
|
||||
public Collection<Rule> getByTags(String... tags) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.rest.mocks;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.openhab.io.hueemulation.internal.dto.HueUserAuthWithSecrets;
|
||||
|
||||
/**
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class DummyUsersStorage implements Storage<HueUserAuthWithSecrets> {
|
||||
Map<String, HueUserAuthWithSecrets> users = new TreeMap<>();
|
||||
|
||||
public DummyUsersStorage() {
|
||||
users.put("testuser", new HueUserAuthWithSecrets("appname", "devicename", "testuser", "clientkey"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HueUserAuthWithSecrets put(String key, @Nullable HueUserAuthWithSecrets value) {
|
||||
return users.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HueUserAuthWithSecrets remove(String key) {
|
||||
return users.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(String key) {
|
||||
return users.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HueUserAuthWithSecrets get(String key) {
|
||||
return users.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<@NonNull String> getKeys() {
|
||||
return users.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<@Nullable HueUserAuthWithSecrets> getValues() {
|
||||
return users.values();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.upnp;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.glassfish.grizzly.osgi.httpservice.HttpServiceImpl;
|
||||
import org.glassfish.grizzly.osgi.httpservice.OSGiMainHandler;
|
||||
import org.glassfish.grizzly.osgi.httpservice.util.Logger;
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.hamcrest.CoreMatchers;
|
||||
import org.junit.After;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.Mockito;
|
||||
import org.openhab.io.hueemulation.internal.rest.CommonSetup;
|
||||
import org.openhab.io.hueemulation.internal.rest.LightsAndGroups;
|
||||
import org.osgi.framework.Bundle;
|
||||
import org.osgi.util.tracker.ServiceTracker;
|
||||
|
||||
/**
|
||||
* Tests the upnp server part if the description.xml is available and if the udp thread comes online
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
public class UpnpTests {
|
||||
protected static CommonSetup commonSetup = null;
|
||||
protected UpnpServer subject;
|
||||
protected static OSGiMainHandler mainHttpHandler;
|
||||
private static HttpServiceImpl httpServiceImpl;
|
||||
private static String descriptionPath;
|
||||
|
||||
LightsAndGroups lightsAndGroups = new LightsAndGroups();
|
||||
|
||||
@BeforeClass
|
||||
public static void setupHttpParts() throws IOException {
|
||||
commonSetup = new CommonSetup(true);
|
||||
commonSetup.start(new ResourceConfig());
|
||||
|
||||
descriptionPath = commonSetup.basePath.replace("/api", "/description.xml");
|
||||
|
||||
Logger logger = new org.glassfish.grizzly.osgi.httpservice.util.Logger(mock(ServiceTracker.class));
|
||||
|
||||
mainHttpHandler = new OSGiMainHandler(logger, mock(Bundle.class));
|
||||
commonSetup.server.getServerConfiguration().addHttpHandler(mainHttpHandler, "/");
|
||||
|
||||
httpServiceImpl = new HttpServiceImpl(mock(Bundle.class), logger);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Executor executor = mock(Executor.class);
|
||||
Mockito.doAnswer(a -> {
|
||||
((Runnable) a.getArgument(0)).run();
|
||||
return null;
|
||||
}).when(executor).execute(ArgumentMatchers.any());
|
||||
subject = new UpnpServer(executor);
|
||||
subject.httpService = httpServiceImpl;
|
||||
subject.cs = commonSetup.cs;
|
||||
subject.overwriteReadyToFalse = true;
|
||||
subject.activate(); // don't execute handleEvent()
|
||||
subject.overwriteReadyToFalse = false;
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
subject.deactivate();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDownHttp() {
|
||||
mainHttpHandler.unregisterAll();
|
||||
commonSetup.dispose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void descriptionWithoutAddress() {
|
||||
Response response = commonSetup.client.target(descriptionPath).request().get();
|
||||
assertEquals(404, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void descriptionWithAddress()
|
||||
throws InterruptedException, ExecutionException, TimeoutException, IOException {
|
||||
HueEmulationConfigWithRuntime r = subject.createConfiguration(null);
|
||||
r = subject.performAddressTest(r);
|
||||
subject.applyConfiguration(r);
|
||||
Response response = commonSetup.client.target(descriptionPath).request().get();
|
||||
assertEquals(200, response.getStatus());
|
||||
String body = response.readEntity(String.class);
|
||||
assertThat(body, is(subject.xmlDocWithAddress));
|
||||
|
||||
if (r == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
// UDP thread started?
|
||||
r.startNow(r).get(5, TimeUnit.SECONDS);
|
||||
assertThat(subject.upnpAnnouncementThreadRunning(), is(true));
|
||||
|
||||
// Send M-SEARCH UPNP "packet" and check if the result contains our bridge ID
|
||||
try (DatagramSocket sendSocket = new DatagramSocket()) {
|
||||
sendSocket.setSoTimeout(700);
|
||||
byte[] bytes = "M-SEARCH".getBytes();
|
||||
sendSocket.send(new DatagramPacket(bytes, bytes.length, subject.MULTI_ADDR_IPV4, UpnpServer.UPNP_PORT));
|
||||
byte[] buffer = new byte[1000];
|
||||
DatagramPacket p = new DatagramPacket(buffer, buffer.length);
|
||||
sendSocket.receive(p);
|
||||
String received = new String(buffer);
|
||||
assertThat(received, CoreMatchers.startsWith("HTTP/1.1 200 OK"));
|
||||
assertThat(received, CoreMatchers.containsString("hue-bridgeid: DEMOUUID"));
|
||||
}
|
||||
|
||||
r.dispose();
|
||||
assertThat(subject.upnpAnnouncementThreadRunning(), is(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void handEventTest() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
subject.handleEvent(null);
|
||||
subject.configChangeFuture.get(5, TimeUnit.SECONDS);
|
||||
assertThat(subject.upnpAnnouncementThreadRunning(), is(true));
|
||||
|
||||
subject.deactivate();
|
||||
assertThat(subject.upnpAnnouncementThreadRunning(), is(false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user