added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,38 @@
<?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 excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.adorne</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>

View 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/openhab2-addons

View File

@@ -0,0 +1,113 @@
# Adorne Binding
The Adorne Binding integrates [Adorne Wi-Fi ready devices](https://www.legrand.us/adorne/products/wireless-whole-house-lighting-controls.aspx) (switches, dimmers, outlets) from [Legrand](https://legrand.com/).
Legrand attempted to provide a public API based on Samsung's ARTIK Cloud and the initial version of this binding was based on that API.
However, Samsung shut down ARTIK Cloud shortly after the release and Legrand has not offered a public API replacement since.
That leaves direct interaction with the Adorne Hub as the only control option.
Consequently the openHAB server and the Adorne Hub must be located on the same network.
The Adorne Hub supports a REST API, but unfortunately there is no documentation or official support from Legrand.
This binding's implementation of the REST API is motivated by the great work of [sbozarth](https://github.com/sbozarth/homebridge-lc7001) who figured out the API details.
## Supported Things
| Thing Type | Description |
|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| hub | The Adorne [Hub LC7001](https://www.legrand.us/adorne/products/wireless-whole-house-lighting-controls/lc7001.aspx) serves as the bridge to control all Adorne devices |
| switch | All Adorne switches and outlets |
| dimmer | All Adorne dimmers |
## Discovery
Auto-discovery is supported as long as the hub can be discovered using the default host and port.
If the hub requires custom host and/or port configuration manual setup is required.
Background discovery is not supported.
## Thing Configuration
### Hub
The hub offers two optional configuration parameters:
| Parameter | Description |
|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| host | The URL to reach the hub. The hub makes itself known through mDNS as `LCM1.local` and the host parameter defaults to this value. As long as the openHAB server and the hub are on the same broadcast domain for mDNS the host parameter doesn't need to be specified. |
| port | The port the hub communicates on. By default the hub answers on port 2112 and the port parameter defaults to this value. As long as the hub configuration hasn't been changed the port parameter doesn't need to be specified. |
### Devices
All devices share one required paramenter:
| Parameter | Description |
|-----------|--------------------------------------------------------------------------------|
| zoneId | The zone ID that is assigned by the hub to each device as a unique identifier. |
Legrand does not provide an easy way to look up a zone ID for a device.
However, zone IDs are simply assigned sequentially starting with 0 in the order devices are added to the hub.
So the first device will have zone ID 0, the next 1 and so on.
## Channels
| Channel Type ID | Item Type | Commands | Description | Thing Types Supporting This Channel |
|-----------------|-----------|----------|-------------------------|-------------------------------------|
| power | Switch | ON, OFF | Turn device on and off | switch, dimmer |
| brightness | Dimmer | 1-100 | Set device's brightness | dimmer |
Note that the brightness channel is limited to values from 1 to 100.
All other commands are ignored.
That means in particular that a dimmer can't be turned off by sending 0 to the brightness channel.
Also, if a dimmer is turned off (via the power channel) and the brightness is updated the dimmer will remain off.
Once the dimmer is turned on it will turn on with the updated brightness setting.
Consequently when a dimmer is turned on it always returns to the most recent brightness setting.
In other words power and brightness states are controlled independently.
This matches how power and brightness are managed on the physical dimmer itself.
To avoid confusion for the user any UI must ensure that only values from 1 to 100 are passed to the brightness channel.
A default slider allows a 0 value and should not be used since there will be no response when the user selects 0.
Common UI choices are Sliders or Setpoints with a minimum value of 1 and a maximum value of 100 (min/max values in Sliders are only supported as of openHAB 2.5).
## Example
This is a simple example that uses an Adorne switch and two dimmers.
Remember that the host and port parameter are not needed in most cases.
As discussed above care is taken that the brightness channel only allows values from 1 to 100 by specifying a min and max value in the sitemap for the dimmers.
For this example to run on an openHAB version older than 2.5 Bedroom 1's Slider must be removed in the sitemap since older versions don't support the min/max setting.
## demo.things
```
Bridge adorne:hub:home "Adorne Hub" [host="192.160.1.111", port=2113] {
switch bathroom "Bathroom" [zoneId=0]
dimmer bedroom1 "Bedroom1" [zoneId=1]
dimmer bedroom2 "Bedroom2" [zoneId=2]
}
```
## demo.items
```
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
Switch LightBedroomSwitch1 {channel="adorne:dimmer:home:bedroom1:power"}
Dimmer LightBedroomDimmer1 {channel="adorne:dimmer:home:bedroom1:brightness"}
Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}
```
## demo.sitemap
```
sitemap demo label="Adorne Binding Demo"
{
Frame label="Adorne Switch" {
Switch item=LightBathroom label="Bathroom" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
}
Frame label="Adorne Dimmer using Slider" {
Switch item=LightBedroomSwitch1 label="Bedroom 1" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
Slider item=LightBedroomDimmer1 label="Bedroom 1" icon="light-on" minValue=1 maxValue=100 step=1
}
Frame label="Adorne Dimmer using Setpoint" {
Switch item=LightBedroomSwitch2 label="Bedroom 2" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
Setpoint item=LightBedroomDimmer2 label="Bedroom 2" icon="light-on" minValue=1 maxValue=100 step=5
}
}
```

View File

@@ -0,0 +1,17 @@
<?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/maven-v4_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.binding.adorne</artifactId>
<name>openHAB Add-ons :: Bundles :: Adorne Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.adorne-${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-binding-adorne" description="Adorne Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.adorne/${project.version}</bundle>
</feature>
</features>

View File

@@ -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.binding.adorne.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AdorneBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneBindingConstants {
public static final String BINDING_ID = "adorne";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_HUB = new ThingTypeUID(BINDING_ID, "hub");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final ThingTypeUID THING_TYPE_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer");
// List of all Channel ids
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_BRIGHTNESS = "brightness";
}

View File

@@ -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.binding.adorne.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AdorneDeviceState} class defines a simple POJO representing the Adorne device state.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneDeviceState {
public final int zoneId;
public final String name;
public final ThingTypeUID deviceType;
public final boolean onOff;
public final int brightness;
public AdorneDeviceState(int zoneId, String name, ThingTypeUID deviceType, boolean onOff, int brightness) {
this.zoneId = zoneId;
this.name = name;
this.deviceType = deviceType;
this.onOff = onOff;
this.brightness = brightness;
}
}

View File

@@ -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.binding.adorne.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AdorneHubConfiguration} class represents the hub configuration options.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneHubConfiguration {
public String host = "LCM1.local";
public Integer port = 2112;
}

View File

@@ -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.binding.adorne.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link AdorneSwitchConfiguration} class represents the switch configuration options.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneSwitchConfiguration {
public @Nullable Integer zoneId;
}

View File

@@ -0,0 +1,132 @@
/**
* 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.binding.adorne.internal.discovery;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.util.UIDUtils;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AdorneDiscoveryService} discovers things for the Adorne hub and Adorne devices.
* Discovery is only supported if the hub is accessible via default host and port.
*
* @author Mark Theiding - Initial Contribution
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.adorne")
public class AdorneDiscoveryService extends AbstractDiscoveryService implements AdorneHubChangeNotify {
private final Logger logger = LoggerFactory.getLogger(AdorneDiscoveryService.class);
private static final int DISCOVERY_TIMEOUT_SECONDS = 10;
private static final String DISCOVERY_HUB_LABEL = "Adorne Hub";
private static final String DISCOVERY_ZONE_ID = "zoneId";
private @Nullable AdorneHubController adorneHubController;
/**
* Creates a AdorneDiscoveryService with disabled auto-discovery.
*/
public AdorneDiscoveryService() {
// Passing false as last argument to super constructor turns off background discovery
super(Collections.singleton(new ThingTypeUID(BINDING_ID, "-")), DISCOVERY_TIMEOUT_SECONDS, false);
// We create the hub controller with default host and port. In the future we could let users create hubs
// manually with custom host and port settings and then perform discovery here for those hubs.
adorneHubController = null;
}
/**
* Kick off discovery of all devices on the hub
*/
@Override
protected void startScan() {
logger.debug("Discovery scan started");
AdorneHubController adorneHubController = new AdorneHubController(new AdorneHubConfiguration(), scheduler,
this);
this.adorneHubController = adorneHubController;
// Hack - we wrap the ThingUID in an array to make it appear effectively final to the compiler throughout the
// chain of futures. Passing it through the chain as context would bloat the code.
ThingUID[] bridgeUID = new ThingUID[1];
// Future enhancement: Need a timeout for each future execution to recover from bugs in the hub controller, but
// Java8 doesn't yet offer that
adorneHubController.start().thenCompose(Void -> {
// We use the hub's MAC address as its unique identifier
return adorneHubController.getMACAddress();
}).thenCompose(macAddress -> {
String macAddressNoColon = macAddress.replace(':', '-'); // Colons are not allowed in ThingUIDs
bridgeUID[0] = new ThingUID(THING_TYPE_HUB, macAddressNoColon);
// We have fully discovered the hub
thingDiscovered(DiscoveryResultBuilder.create(bridgeUID[0]).withLabel(DISCOVERY_HUB_LABEL).build());
return adorneHubController.getZones();
}).thenAccept(zoneIds -> {
zoneIds.forEach(zoneId -> {
adorneHubController.getState(zoneId).thenAccept(state -> {
String id = UIDUtils.encode(state.name); // Strip zone ID's name to become a valid ThingUID
// We have fully discovered a new zone ID
thingDiscovered(DiscoveryResultBuilder
.create(new ThingUID(state.deviceType, bridgeUID[0], id.toLowerCase()))
.withLabel(state.name).withBridge(bridgeUID[0])
.withProperty(DISCOVERY_ZONE_ID, state.zoneId).build());
}).exceptionally(e -> {
logger.warn("Discovery of zone ID {} failed ({})", zoneId, e.getMessage());
return null;
});
});
adorneHubController.stopWhenCommandsServed(); // Shut down hub once all discovery requests have been served
}).exceptionally(e -> {
logger.warn("Discovery failed ({})", e.getMessage());
return null;
});
}
/**
* Notification to stop scanning
*/
@Override
protected void stopScan() {
super.stopScan();
AdorneHubController adorneHubController = this.adorneHubController;
if (adorneHubController != null) {
adorneHubController.stop();
this.adorneHubController = null;
logger.debug("Discovery timed out. Scan stopped.");
}
}
// Nothing to do on change notifications
@Override
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
}
@Override
public void connectionChangeNotify(boolean connected) {
}
}

View File

@@ -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.binding.adorne.internal.handler;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_BRIGHTNESS;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AdorneDimmerHandler} is responsible for handling commands, which are
* sent to one of the channels. It supports the brightness channel in addition to the inherited switch channel.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneDimmerHandler extends AdorneSwitchHandler {
private final Logger logger = LoggerFactory.getLogger(AdorneDimmerHandler.class);
public AdorneDimmerHandler(Thing thing) {
super(thing);
}
/**
* Handles refresh and percent commands for channel
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_BRIGHTNESS}
* It delegates all other commands to its parent class.
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
try {
if (channelUID.getId().equals(CHANNEL_BRIGHTNESS)) {
if (command instanceof RefreshType) {
refreshBrightness();
} else if (command instanceof PercentType) {
// Change the brightness through the hub controller
AdorneHubController adorneHubController = getAdorneHubController();
int level = ((PercentType) command).intValue();
if (level >= 1 && level <= 100) { // Ignore commands outside of the supported 1-100 range
adorneHubController.setBrightness(zoneId, level);
} else {
logger.debug("Ignored command to set brightness to level {}", level);
}
}
} else {
super.handleCommand(channelUID, command); // Parent can handle everything else
}
} catch (IllegalStateException e) {
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
// want the framework to handle it gracefully. No point to update the thing status, since the
// AdorneHubController already does that. So we are forced to swallow the exception here.
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
getThing().getLabel(), e.getMessage());
}
}
/**
* Refreshes the brightness of our thing to the actual state of the device.
*
*/
public void refreshBrightness() {
// Asynchronously get our brightness from the hub controller and update our state accordingly
AdorneHubController adorneHubController = getAdorneHubController();
adorneHubController.getState(zoneId).thenAccept(state -> {
updateState(CHANNEL_BRIGHTNESS, new PercentType(state.brightness));
logger.debug("Refreshed dimmer {} with brightness {}", getThing().getLabel(), state.brightness);
});
}
/**
* Refreshes all supported channels.
*
*/
@Override
public void refresh() {
super.refresh();
refreshBrightness();
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.binding.adorne.internal.handler;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
import java.util.Collections;
import java.util.Set;
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.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AdorneHandlerFactory} is responsible for creating thing handlers.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.adorne", service = ThingHandlerFactory.class)
public class AdorneHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(AdorneHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(THING_TYPE_HUB, THING_TYPE_SWITCH, THING_TYPE_DIMMER).collect(Collectors.toSet()));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
/**
* Creates handlers for switches, dimmers and hubs.
*/
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
logger.debug("Creating an AdorneSwitchHandler for thing '{}'", thing.getUID());
return new AdorneSwitchHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_DIMMER)) {
logger.debug("Creating an AdorneDimmerHandler for thing '{}'", thing.getUID());
return new AdorneDimmerHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_HUB)) {
logger.debug("Creating an AdorneHubHandler for bridge '{}'", thing.getUID());
return new AdorneHubHandler((Bridge) thing);
}
return null;
}
}

View File

@@ -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.binding.adorne.internal.handler;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
import org.openhab.binding.adorne.internal.hub.AdorneHubChangeNotify;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AdorneHubHandler} manages the state and status of the Adorne Hub's devices.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneHubHandler extends BaseBridgeHandler implements AdorneHubChangeNotify {
private final Logger logger = LoggerFactory.getLogger(AdorneHubHandler.class);
private @Nullable AdorneHubController adorneHubController = null;
public AdorneHubHandler(Bridge bridge) {
super(bridge);
}
/**
* The {@link AdorneHubHandler} does not support any commands itself. This method is a NOOP and only provided since
* its implementation is required.
*
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Unfortunately BaseBridgeHandler doesn't provide a default implementation of handleCommand. However, hub
// commands could be added as a future enhancement e.g. to support hub firmware upgrades.
}
/**
* Establishes the hub controller for communication with the hub.
*/
@Override
public void initialize() {
logger.debug("Initializing hub {}", getThing().getLabel());
updateStatus(ThingStatus.UNKNOWN);
AdorneHubConfiguration config = getConfigAs(AdorneHubConfiguration.class);
logger.debug("Configuration host:{} port:{}", config.host, config.port);
AdorneHubController adorneHubController = new AdorneHubController(config, scheduler, this);
this.adorneHubController = adorneHubController;
// Kick off the hub controller that handles all interactions with the hub for us
adorneHubController.start();
}
/**
* Disposes resources by stopping the hub controller.
*/
@Override
public void dispose() {
AdorneHubController adorneHubController = this.adorneHubController;
if (adorneHubController != null) {
adorneHubController.stop();
}
}
/**
* Returns the hub controller. Returns <code>null</code> if hub controller has not been created yet.
*
* @return hub controller
*/
public @Nullable AdorneHubController getAdorneHubController() {
return adorneHubController;
}
/**
* The {@link AdorneHubHandler} is notified that the state of one of its physical devices has changed. The
* {@link AdorneHubHandler} then asks the appropriate thing handler to update the thing to match the new state.
*
*/
@Override
public void stateChangeNotify(int zoneId, boolean onOff, int brightness) {
logger.debug("State changed (zoneId:{} onOff:{} brightness:{})", zoneId, onOff, brightness);
getThing().getThings().forEach(thing -> {
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
if (thingHandler != null && thingHandler.getZoneId() == zoneId) {
thingHandler.updateState(CHANNEL_POWER, OnOffType.from(onOff));
if (thing.getThingTypeUID().equals(THING_TYPE_DIMMER)) {
thingHandler.updateState(CHANNEL_BRIGHTNESS, new PercentType(brightness));
}
}
});
}
/**
* The {@link AdorneHubHandler} is notified that its connectivity has changed.
*
*/
@Override
public void connectionChangeNotify(boolean connected) {
logger.debug("Status changed (connected:{})", connected);
if (connected) {
// Refresh all of our things in case thing states changed while we were disconnected
getThing().getThings().forEach(thing -> {
AdorneSwitchHandler thingHandler = (AdorneSwitchHandler) thing.getHandler();
if (thingHandler != null) {
thingHandler.refresh();
}
});
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
}
}

View File

@@ -0,0 +1,173 @@
/**
* 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.binding.adorne.internal.handler;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.CHANNEL_POWER;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.adorne.internal.configuration.AdorneSwitchConfiguration;
import org.openhab.binding.adorne.internal.hub.AdorneHubController;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AdorneSwitchHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public class AdorneSwitchHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(AdorneSwitchHandler.class);
/**
* The zone ID that represents this {@link AdorneSwitchHandler}'s thing
*/
protected int zoneId;
public AdorneSwitchHandler(Thing thing) {
super(thing);
}
/**
* Handles refresh and on/off commands for channel
* {@link org.openhab.binding.adorne.internal.AdorneBindingConstants#CHANNEL_POWER}
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("handleCommand (channelUID:{} command:{}", channelUID, command);
try {
if (channelUID.getId().equals(CHANNEL_POWER)) {
if (command instanceof OnOffType) {
AdorneHubController adorneHubController = getAdorneHubController();
adorneHubController.setOnOff(zoneId, command.equals(OnOffType.ON));
} else if (command instanceof RefreshType) {
refreshOnOff();
}
}
} catch (IllegalStateException e) {
// Hub controller could't handle our commands. Unfortunately the framework has no mechanism to report
// runtime errors. If we throw the exception up the framework logs it as an error - we don't want that - we
// want the framework to handle it gracefully. No point to update the thing status, since the
// AdorneHubController already does that. So we are forced to swallow the exception here.
logger.debug("Failed to execute command {} for channel {} for thing {} ({})", command, channelUID,
getThing().getLabel(), e.getMessage());
}
}
/**
* Sets the handled thing to online.
*/
@Override
public void initialize() {
logger.debug("Initializing switch {}", getThing().getLabel());
AdorneSwitchConfiguration config = getConfigAs(AdorneSwitchConfiguration.class);
Integer configZoneId = config.zoneId;
if (configZoneId != null) {
zoneId = configZoneId;
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
return;
}
updateStatus(ThingStatus.ONLINE);
}
/**
* Updates thing status in response to bridge status changes.
*/
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.trace("bridgeStatusChanged bridgeStatusInfo:{}", bridgeStatusInfo.getStatus());
if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else {
updateStatus(bridgeStatusInfo.getStatus());
}
}
/**
* Returns the hub controller.
*
* @throws IllegalStateException if hub controller is not available yet.
*/
protected AdorneHubController getAdorneHubController() {
Bridge bridge;
AdorneHubHandler hubHandler;
AdorneHubController adorneHubController = null;
bridge = getBridge();
if (bridge != null) {
hubHandler = (AdorneHubHandler) bridge.getHandler();
if (hubHandler != null) {
adorneHubController = hubHandler.getAdorneHubController();
}
}
if (adorneHubController == null) {
throw new IllegalStateException("Hub Controller not available yet.");
}
return adorneHubController;
}
/**
* Returns the zone ID that represents this {@link AdorneSwitchHandler}'s thing
*
* @return zone ID
*/
public int getZoneId() {
return zoneId;
}
/**
* Refreshes the on/off state of our thing to the actual state of the device.
*
*/
public void refreshOnOff() {
// Asynchronously get our onOff state from the hub controller and update our state accordingly
AdorneHubController adorneHubController = getAdorneHubController();
adorneHubController.getState(zoneId).thenAccept(state -> {
OnOffType onOffState = OnOffType.from(state.onOff);
updateState(CHANNEL_POWER, onOffState);
logger.debug("Refreshed switch {} with switch state {}", getThing().getLabel(), onOffState);
});
}
/**
* Refreshes all supported channels.
*
*/
public void refresh() {
refreshOnOff();
}
/**
* Provides a public version of updateState.
*
*/
@Override
public void updateState(String channelID, State state) {
super.updateState(channelID, state);// Leverage our base class' protected method
}
}

View File

@@ -0,0 +1,40 @@
/**
* 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.binding.adorne.internal.hub;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link AdorneHubChangeNotify} interface is used by the {@link AdorneHubController} to notify listeners about
* Adorne device status and hub connection changes.
*
* @author Mark Theiding - Initial contribution
*/
@NonNullByDefault
public interface AdorneHubChangeNotify {
/**
* Notify listener about state change of on/off and brightness state
*
* @param zoneID zone ID for which change occurred
* @param onOff new on/off state
* @param brightness new brightness
*/
public void stateChangeNotify(int zoneId, boolean onOff, int brightness);
/**
* Notify listener about hub connection change
*
* @param connected new connection state
*/
public void connectionChangeNotify(boolean connected);
}

View File

@@ -0,0 +1,94 @@
/**
* 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.binding.adorne.internal.hub;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonStreamParser;
/**
* The {@link AdorneHubConnection} manages basic connectivity with the Adorne hub.
*
* @author Mark Theiding - Initial Contribution
*/
@NonNullByDefault
public class AdorneHubConnection {
private final Logger logger = LoggerFactory.getLogger(AdorneHubConnection.class);
private final Socket hubSocket;
private final PrintStream hubOut;
private final InputStreamReader hubInReader;
private final JsonStreamParser hubIn;
public AdorneHubConnection(String hubHost, int hubPort, int timeout) throws IOException {
hubSocket = new Socket(hubHost, hubPort);
hubSocket.setSoTimeout(timeout);
hubOut = new PrintStream(hubSocket.getOutputStream());
hubInReader = new InputStreamReader(hubSocket.getInputStream());
hubIn = new JsonStreamParser(hubInReader);
}
public void close() {
try {
hubInReader.close(); // Closes underlying input stream as well
} catch (IOException e) {
logger.warn("Closing hub input reader failed ({})", e.getMessage());
}
hubOut.close(); // Closes underlying output stream as well
try {
hubSocket.close();
} catch (IOException e) {
logger.warn("Closing hub controller socket failed ({})", e.getMessage());
}
}
public void cancel() {
try {
hubSocket.shutdownInput();
} catch (IOException e) {
logger.debug("Couldn't shutdown hub socket");
}
}
public void putMsg(String cmd) {
hubOut.print(cmd);
}
public @Nullable JsonObject getMsg() throws JsonParseException {
JsonElement msg = null;
JsonObject msgJsonObject = null;
msg = hubIn.next();
if (msg == null || (msg instanceof JsonPrimitive && msg.getAsCharacter() == 0)) {
return null; // Eat empty messages
}
logger.debug("Received message {}", msg);
if (msg instanceof JsonObject) {
msgJsonObject = (JsonObject) msg;
}
return msgJsonObject;
}
}

View File

@@ -0,0 +1,511 @@
/**
* 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.binding.adorne.internal.hub;
import static org.openhab.binding.adorne.internal.AdorneBindingConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntUnaryOperator;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.adorne.internal.AdorneDeviceState;
import org.openhab.binding.adorne.internal.configuration.AdorneHubConfiguration;
import org.openhab.core.thing.ThingTypeUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
/**
* The {@link AdorneHubController} manages the interaction with the Adorne hub. The controller maintains a connection
* with the Adorne Hub and listens to device changes and issues device commands. Interaction with the hub is performed
* asynchronously through REST messages.
*
* @author Mark Theiding - Initial Contribution
*/
@NonNullByDefault
public class AdorneHubController {
private final Logger logger = LoggerFactory.getLogger(AdorneHubController.class);
private static final int HUB_CONNECT_TIMEOUT = 10000;
private static final int HUB_RECONNECT_SLEEP_MINIMUM = 1;
private static final int HUB_RECONNECT_SLEEP_MAXIMUM = 15 * 60;
// Hub rest commands
private static final String HUB_REST_SET_ONOFF = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"Power\":%b}}\0";
private static final String HUB_REST_SET_BRIGHTNESS = "{\"ID\":%d,\"Service\":\"SetZoneProperties\",\"ZID\":%d,\"PropertyList\":{\"PowerLevel\":%d}}\0";
private static final String HUB_REST_REQUEST_STATE = "{\"ID\":%d,\"Service\":\"ReportZoneProperties\",\"ZID\":%d}\0";
private static final String HUB_REST_REQUEST_ZONES = "{\"ID\":%d,\"Service\":\"ListZones\"}\0";
private static final String HUB_REST_REQUEST_MACADDRESS = "{\"ID\":%d,\"Service\":\"SystemInfo\"}\0";
private static final String HUB_TOKEN_SERVICE = "Service";
private static final String HUB_TOKEN_ZID = "ZID";
private static final String HUB_TOKEN_PROPERTY_LIST = "PropertyList";
private static final String HUB_TOKEN_DEVICE_TYPE = "DeviceType";
private static final String HUB_TOKEN_SWITCH = "Switch";
private static final String HUB_TOKEN_DIMMER = "Dimmer";
private static final String HUB_TOKEN_NAME = "Name";
private static final String HUB_TOKEN_POWER = "Power";
private static final String HUB_TOKEN_POWER_LEVEL = "PowerLevel";
private static final String HUB_TOKEN_MAC_ADDRESS = "MACAddress";
private static final String HUB_TOKEN_ZONE_LIST = "ZoneList";
private static final String HUB_SERVICE_REPORT_ZONE_PROPERTIES = "ReportZoneProperties";
private static final String HUB_SERVICE_ZONE_PROPERTIES_CHANGED = "ZonePropertiesChanged";
private static final String HUB_SERVICE_LIST_ZONE = "ListZones";
private static final String HUB_SERVICE_SYSTEM_INFO = "SystemInfo";
private @Nullable Future<?> hubController;
private final String hubHost;
private int hubPort;
private @Nullable AdorneHubConnection hubConnection;
private final CompletableFuture<@Nullable Void> hubControllerConnected;
private int hubReconnectSleep; // Sleep time before we attempt re-connect
private final ScheduledExecutorService scheduler;
private volatile boolean stopWhenCommandsServed; // Stop the controller once all pending commands have been served
// When we submit commmands to the hub we don't correlate commands and responses. We simply use the first available
// response that answers our question. For that we store all pending commands.
// Note that for optimal resiliency we send a new request for each command even if a request is already pending
private final Map<Integer, CompletableFuture<AdorneDeviceState>> stateCommands;
private @Nullable CompletableFuture<List<Integer>> zoneCommand;
private @Nullable CompletableFuture<String> macAddressCommand;
private final AtomicInteger commandId; // We assign increasing command ids to all REST commands to the hub for
// easier troubleshooting
private final AdorneHubChangeNotify changeListener;
private final Object stopLock;
private final Object hubConnectionLock;
private final Object macAddressCommandLock;
private final Object zoneCommandLock;
public AdorneHubController(AdorneHubConfiguration config, ScheduledExecutorService scheduler,
AdorneHubChangeNotify changeListener) {
hubHost = config.host;
hubPort = config.port;
this.scheduler = scheduler;
this.changeListener = changeListener;
hubController = null;
hubConnection = null;
hubControllerConnected = new CompletableFuture<>();
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM;
stopWhenCommandsServed = false;
stopLock = new Object();
hubConnectionLock = new Object();
macAddressCommandLock = new Object();
zoneCommandLock = new Object();
stateCommands = new HashMap<>();
zoneCommand = null;
macAddressCommand = null;
commandId = new AtomicInteger(0);
}
/**
* Start the hub controller. Call only once.
*
* @return Future to inform the caller that the hub controller is ready for receiving commands
*/
public CompletableFuture<@Nullable Void> start() {
logger.info("Starting hub controller");
hubController = scheduler.submit(this::msgLoop);
return hubControllerConnected;
}
/**
* Stops the hub controller. Can't restart afterwards. If called before start nothing happens.
*/
public void stop() {
logger.info("Stopping hub controller");
synchronized (stopLock) {
// Canceling the controller tells the message loop to stop and also cancels recreation of the message loop
// if that is pending after a disconnect.
Future<?> hubController = this.hubController;
if (hubController != null) {
hubController.cancel(true);
}
}
// Stop the input stream in case controller is waiting on input
// Note this is best effort. If we are unlucky the hub can still enter waiting on input just after our stop
// here. Because waiting on input is long-running we can't just synchronize it with the stop check as case 2
// above. But that is ok as waiting on input has a timeout and will honor stop after that.
synchronized (hubConnectionLock) {
AdorneHubConnection hubConnection = this.hubConnection;
if (hubConnection != null) {
hubConnection.cancel();
}
}
cancelCommands();
}
/**
* Stops the hub controller once all in-flight commands have been executed.
*/
public void stopWhenCommandsServed() {
stopWhenCommandsServed = true;
}
/**
* Turns device on or off.
*
* @param zoneId the device's zone ID
* @param on true to turn on the device
*/
public void setOnOff(int zoneId, boolean on) {
sendRestCmd(String.format(HUB_REST_SET_ONOFF, getNextCommandId(), zoneId, on));
}
/**
* Sets the brightness for a device. Applies only to dimmer devices.
*
* @param zoneId the device's zone ID
* @param level A value from 1-100. Note that in particular value 0 is not supported, which means this method can't
* be used to turn off a dimmer.
*/
public void setBrightness(int zoneId, int level) {
if (level < 1 || level > 100) {
throw new IllegalArgumentException();
}
sendRestCmd(String.format(HUB_REST_SET_BRIGHTNESS, getNextCommandId(), zoneId, level));
}
/**
* Gets asynchronously the state for a device.
*
* @param zoneId the device's zone ID
* @return a future for the {@link AdorneDeviceState}
*/
public CompletableFuture<AdorneDeviceState> getState(int zoneId) {
// Note that we send the REST command for resiliency even if there is a pending command
sendRestCmd(String.format(HUB_REST_REQUEST_STATE, getNextCommandId(), zoneId));
CompletableFuture<AdorneDeviceState> stateCommand;
synchronized (stateCommands) {
stateCommand = stateCommands.get(zoneId);
if (stateCommand == null) {
stateCommand = new CompletableFuture<>();
stateCommands.put(zoneId, stateCommand);
}
}
return stateCommand;
}
/**
* Gets asynchronously all zone IDs that are in use on the hub.
*
* @return a future for the list of zone IDs
*/
public CompletableFuture<List<Integer>> getZones() {
// Note that we send the REST command for resiliency even if there is a pending command
sendRestCmd(String.format(HUB_REST_REQUEST_ZONES, getNextCommandId()));
CompletableFuture<List<Integer>> zoneCommand;
synchronized (zoneCommandLock) {
zoneCommand = this.zoneCommand;
if (zoneCommand == null) {
this.zoneCommand = zoneCommand = new CompletableFuture<>();
}
}
return zoneCommand;
}
/**
* Gets asynchronously the MAC address of the hub.
*
* @return a future for the MAC address
*/
public CompletableFuture<String> getMACAddress() {
// Note that we send the REST command for resiliency even if there is a pending command
sendRestCmd(String.format(HUB_REST_REQUEST_MACADDRESS, getNextCommandId()));
CompletableFuture<String> macAddressCommand;
synchronized (macAddressCommandLock) {
macAddressCommand = this.macAddressCommand;
if (macAddressCommand == null) {
this.macAddressCommand = macAddressCommand = new CompletableFuture<>();
}
}
return macAddressCommand;
}
private void sendRestCmd(String cmd) {
logger.debug("Sending command {}", cmd);
synchronized (hubConnectionLock) {
AdorneHubConnection hubConnection = this.hubConnection;
if (hubConnection != null) {
hubConnection.putMsg(cmd);
} else {
throw new IllegalStateException("Can't send command. Adorne Hub connection is not available.");
}
}
}
/**
* Runs the controller message loop that is interacting with the Adorne Hub by sending commands and listening for
* updates
*/
private void msgLoop() {
try {
JsonObject hubMsg;
JsonPrimitive jsonService;
String service;
// Main message loop listening for updates from the hub
logger.debug("Starting message loop");
while (!shouldStop()) {
if (!connect()) {
int sleep = hubReconnectSleep;
logger.debug("Waiting {} seconds before re-attempting to connect.", sleep);
if (hubReconnectSleep < HUB_RECONNECT_SLEEP_MAXIMUM) {
hubReconnectSleep = hubReconnectSleep * 2; // Increase sleep time exponentially
}
restartMsgLoop(sleep);
return;
} else {
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset
}
hubMsg = null;
try {
AdorneHubConnection hubConnection = this.hubConnection;
if (hubConnection != null) {
hubMsg = hubConnection.getMsg();
}
} catch (JsonParseException e) {
logger.debug("Failed to read valid message {}", e.getMessage());
disconnect(); // Disconnect so we can recover
}
if (hubMsg == null) {
continue;
}
// Process message based on service type
if ((jsonService = hubMsg.getAsJsonPrimitive(HUB_TOKEN_SERVICE)) != null) {
service = jsonService.getAsString();
} else {
continue; // Ignore messages that don't have a service specified
}
if (service.equals(HUB_SERVICE_REPORT_ZONE_PROPERTIES)) {
processMsgReportZoneProperties(hubMsg);
} else if (service.equals(HUB_SERVICE_ZONE_PROPERTIES_CHANGED)) {
processMsgZonePropertiesChanged(hubMsg);
} else if (service.equals(HUB_SERVICE_LIST_ZONE)) {
processMsgListZone(hubMsg);
} else if (service.equals(HUB_SERVICE_SYSTEM_INFO)) {
processMsgSystemInfo(hubMsg);
}
}
} catch (RuntimeException e) {
logger.warn("Hub controller failed", e);
}
// Shut down
disconnect();
cancelCommands();
hubControllerConnected.cancel(false);
logger.info("Exiting hub controller");
}
private boolean shouldStop() {
boolean stateCommandsIsEmpty;
synchronized (stateCommands) {
stateCommandsIsEmpty = stateCommands.isEmpty();
}
boolean commandsServed = stopWhenCommandsServed && stateCommandsIsEmpty && (zoneCommand == null)
&& (macAddressCommand == null);
return isCancelled() || commandsServed;
}
private boolean isCancelled() {
Future<?> hubController = this.hubController;
return hubController == null || hubController.isCancelled();
}
private boolean connect() {
try {
if (hubConnection == null) {
hubConnection = new AdorneHubConnection(hubHost, hubPort, HUB_CONNECT_TIMEOUT);
logger.debug("Hub connection established");
// Working around an Adorne Hub bug: the first command sent from a new connection intermittently
// gets lost in the hub. We are requesting the MAC address here simply to get this fragile first
// command out of the way. Requesting the MAC address and ignoring the result doesn't do any harm.
getMACAddress();
hubControllerConnected.complete(null);
changeListener.connectionChangeNotify(true);
}
return true;
} catch (IOException e) {
logger.debug("Couldn't establish hub connection ({}).", e.getMessage());
return false;
}
}
private void disconnect() {
hubReconnectSleep = HUB_RECONNECT_SLEEP_MINIMUM; // Reset our reconnect sleep time
synchronized (hubConnectionLock) {
AdorneHubConnection hubConnection = this.hubConnection;
if (hubConnection != null) {
hubConnection.close();
this.hubConnection = null;
}
}
changeListener.connectionChangeNotify(false);
}
private void cancelCommands() {
// If there are still pending commands we need to cancel them
synchronized (stateCommands) {
stateCommands.forEach((zoneId, stateCommand) -> stateCommand.cancel(false));
stateCommands.clear();
}
synchronized (zoneCommandLock) {
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
if (zoneCommand != null) {
zoneCommand.cancel(false);
this.zoneCommand = null;
}
}
synchronized (macAddressCommandLock) {
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
if (macAddressCommand != null) {
macAddressCommand.cancel(false);
this.macAddressCommand = null;
}
}
logger.debug("Cancelled commands");
}
private void restartMsgLoop(int sleep) {
synchronized (stopLock) {
if (!isCancelled()) {
this.hubController = scheduler.schedule(this::msgLoop, sleep, TimeUnit.SECONDS);
}
}
}
/**
* The hub sent zone properties in response to a command.
*/
private void processMsgReportZoneProperties(JsonObject hubMsg) {
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
logger.debug("Reporting zone properties for zone ID {} ", zoneId);
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
String deviceTypeStr = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_DEVICE_TYPE).getAsString();
ThingTypeUID deviceType;
if (deviceTypeStr.equals(HUB_TOKEN_SWITCH)) {
deviceType = THING_TYPE_SWITCH;
} else if (deviceTypeStr.equals(HUB_TOKEN_DIMMER)) {
deviceType = THING_TYPE_DIMMER;
} else {
logger.debug("Unsupported device type {}", deviceTypeStr);
return;
}
AdorneDeviceState state = new AdorneDeviceState(zoneId,
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_NAME).getAsString(), deviceType,
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean(),
jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt());
synchronized (stateCommands) {
CompletableFuture<AdorneDeviceState> stateCommand = stateCommands.get(zoneId);
if (stateCommand != null) {
stateCommand.complete(state);
stateCommands.remove(zoneId);
}
}
}
/**
* The hub informs us about a zone's change in properties.
*/
private void processMsgZonePropertiesChanged(JsonObject hubMsg) {
int zoneId = hubMsg.getAsJsonPrimitive(HUB_TOKEN_ZID).getAsInt();
logger.debug("Zone properties changed for zone ID {} ", zoneId);
JsonObject jsonPropertyList = hubMsg.getAsJsonObject(HUB_TOKEN_PROPERTY_LIST);
boolean onOff = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER).getAsBoolean();
int brightness = jsonPropertyList.getAsJsonPrimitive(HUB_TOKEN_POWER_LEVEL).getAsInt();
changeListener.stateChangeNotify(zoneId, onOff, brightness);
}
/**
* The hub sent a list of zones in response to a command.
*/
private void processMsgListZone(JsonObject hubMsg) {
List<Integer> zones = new ArrayList<>();
JsonArray jsonZoneList;
jsonZoneList = hubMsg.getAsJsonArray(HUB_TOKEN_ZONE_LIST);
jsonZoneList.forEach(jsonZoneId -> {
JsonPrimitive jsonZoneIdValue = ((JsonObject) jsonZoneId).getAsJsonPrimitive(HUB_TOKEN_ZID);
zones.add(jsonZoneIdValue.getAsInt());
});
synchronized (zoneCommandLock) {
CompletableFuture<List<Integer>> zoneCommand = this.zoneCommand;
if (zoneCommand != null) {
zoneCommand.complete(zones);
this.zoneCommand = null;
}
}
}
/**
* The hub sent system info in response to a command.
*/
private void processMsgSystemInfo(JsonObject hubMsg) {
synchronized (macAddressCommandLock) {
CompletableFuture<String> macAddressCommand = this.macAddressCommand;
if (macAddressCommand != null) {
macAddressCommand.complete(hubMsg.getAsJsonPrimitive(HUB_TOKEN_MAC_ADDRESS).getAsString());
this.macAddressCommand = null;
}
}
}
private int getNextCommandId() {
IntUnaryOperator op = commandId -> {
int newCommandId = commandId;
if (commandId == Integer.MAX_VALUE) {
newCommandId = 0;
}
return ++newCommandId;
};
return commandId.updateAndGet(op);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="adorne" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Adorne Binding</name>
<description>The Adorne Binding controls Legrand's Adorne Wi-Fi ready switches and outlets.</description>
<author>Mark Theiding</author>
</binding:binding>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="adorne"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="http://openhab.org/schemas/thing-description/v1.0.0 http://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="hub">
<label>Adorne Hub</label>
<description>The Adorne Hub serves as the bridge to control Adorne switches, dimmer switches and outlets.</description>
<config-description>
<parameter name="host" type="text">
<default>LCM1.local</default>
<label>Host</label>
<description>
Host name or IP address.
</description>
<context>network_address</context>
</parameter>
<parameter name="port" type="integer">
<default>2112</default>
<label>Port</label>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="adorne"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="hub"/>
</supported-bridge-type-refs>
<label>Adorne Switch</label>
<description>Controls an Adorne switch or outlet.</description>
<channels>
<channel id="power" typeId="system.power"/>
</channels>
<config-description>
<parameter name="zoneId" type="integer" required="true">
<label>Zone ID</label>
</parameter>
</config-description>
</thing-type>
<thing-type id="dimmer">
<supported-bridge-type-refs>
<bridge-type-ref id="hub"/>
</supported-bridge-type-refs>
<label>Adorne Dimmer Switch</label>
<description>Controls an Adorne dimmer switch.</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="brightness" typeId="system.brightness"/>
</channels>
<config-description>
<parameter name="zoneId" type="integer" required="true">
<label>Zone ID</label>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,5 @@
Switch LightBathroom {channel="adorne:switch:home:bathroom:power"}
Switch LightBedroomSwitch1 {channel="adorne:dimmer:home:bedroom1:power"}
Dimmer LightBedroomDimmer1 {channel="adorne:dimmer:home:bedroom1:brightness"}
Switch LightBedroomSwitch2 {channel="adorne:dimmer:home:bedroom2:power"}
Dimmer LightBedroomDimmer2 {channel="adorne:dimmer:home:bedroom2:brightness"}

View File

@@ -0,0 +1,14 @@
sitemap demo label="Adorne Binding Demo"
{
Frame label="Adorne Switch" {
Switch item=LightBathroom label="Bathroom" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
}
Frame label="Adorne Dimmer using Slider" {
Switch item=LightBedroomSwitch1 label="Bedroom 1" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
Slider item=LightBedroomDimmer1 label="Bedroom 1" icon="light-on" minValue=1 maxValue=100 step=1 // Requires OpenHAB 2.5
}
Frame label="Adorne Dimmer using Setpoint" {
Switch item=LightBedroomSwitch2 label="Bedroom 2" mappings=["ON"="On", "OFF"="Off"] icon="light-on"
Setpoint item=LightBedroomDimmer2 label="Bedroom 2" icon="light-on" minValue=1 maxValue=100 step=5
}
}

View File

@@ -0,0 +1,5 @@
Bridge adorne:hub:home "Adorne Hub" {
switch bathroom "Bathroom" [zoneId=0]
dimmer bedroom1 "Bedroom1" [zoneId=1]
dimmer bedroom2 "Bedroom2" [zoneId=2]
}