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,43 @@
<?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="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="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" 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.harmonyhub</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,20 @@
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
== Third-party Content
harmony-client
* License: EPL 2.0 License
* Project: https://github.com/digitaldan/harmony-client
* Source: https://github.com/digitaldan/harmony-client

View File

@@ -0,0 +1,150 @@
# Logitech Harmony Hub Binding
The Harmony Hub binding is used to enable communication between openHAB and multiple Logitech Harmony Hub devices.
The API exposed by the Harmony Hub is relatively limited, but it does allow for reading the current activity as well as setting the activity and sending device commands.
## Overview
The Harmony binding represents a "Hub" as a bridge thing type and "Devices" as things connected to the bridge.
### Hub
A hub (bridge thing) represents a physical Harmony Hub.
The hub possesses a single channel with the id "activity" which is a StringType set to the name of the current activity.
This channel is dynamically generated with the possible activity strings listed as channel state options.
### Devices
Devices are dynamically created.
There is a single device thing for every physical device configured on the harmony hub.
Each device has a single channel with the id "button" which sends a string with the name of the button to press on the device.
This channel is dynamically generated with the possible button press strings listed as channel state options.
## Discovery
The Harmony binding will automatically find all Harmony Hubs on the local network and add them to the inbox.
Once a Hub has been added, any connected devices will also added to the Inbox.
## Binding Configuration
The binding requires no special configuration
## Thing Configuration
This is optional, it is recommended to let the binding discover and add hubs and devices.
To manually configure a Harmony Hub thing you may specify its host name ("host") as well as an optional search timeout value in seconds ("discoveryTimeout") and optional heart beat interval (heartBeatInterval) in seconds.
In the thing file, this looks e.g. like
```java
Bridge harmonyhub:hub:GreatRoom [ host="192.168.1.100"]
```
To manually configure a Harmony device thing you may specify its numeric id ("id") or its name ("name"), but not both.
Note that this is prefixed by the hub the device is controlled from.
In the thing file, this looks e.g. like
```java
Bridge harmonyhub:hub:great [ name="Great Room"] {
device denon [ name="Denon AV Receiver"]
}
```
or
```java
Bridge harmonyhub:hub:great [ name="Great Room"] {
device denon [ id=176254]
}
```
## Channels
Hubs can report and change the current activity:
items:
```java
String HarmonyGreatRoomActivity "Current Activity [%s]" (gMain) { channel="harmonyhub:hub:GreatRoom:currentActivity" }
```
Hubs can also send a button press to a device associated with the current activity.
A String item can be used to send any button name/label or a Player item can be used to send Play/Pause/FastForward/Rewind/SkipForward/SkipBackward.
This mimics the physical remote where buttons are mapped differently depending on which activity is running.
For example the play button may be sent to a DVD player when running a "Watch DVD" activity, or it may be sent to an Apple TV when running a "Watch Movie" activity.
```java
String HarmonyHubGreatButton { channel="harmonyhub:hub:GreatRoom:buttonPress" }
Player HarmonyHubGreatPlayer { channel="harmonyhub:hub:GreatRoom:player" }
```
Devices can be sent button commands directly, regardless if they are part of the current running activity or not.
```java
String HarmonyGreatRoomDenon "Denon Button Press" (gMain) { channel="harmonyhub:device:GreatRoom:29529817:buttonPress" }
```
Hubs can also trigger events when a new activity is starting (activityStarting channel) and after it is started (activityStarted channel).
The name of the event is equal to the activity name, with all non-alphanumeric characters replaced with underscore.
rules:
```javascript
rule "Starting TV"
when
Channel "harmonyhub:hub:GreatRoom:activityStarting" triggered Watch_TV
then
logInfo("Harmony", "TV is starting...")
end
rule "TV started"
when
Channel "harmonyhub:hub:GreatRoom:activityStarted" triggered Watch_TV
then
logInfo("Harmony", "TV is started")
end
rule "Going off"
when
Channel "harmonyhub:hub:GreatRoom:activityStarting" triggered PowerOff
then
logInfo("Harmony", "Hub is going off...")
end
rule "Hub off"
when
Channel "harmonyhub:hub:GreatRoom:activityStarted" triggered PowerOff
then
logInfo("Harmony", "Hub is off - no activity")
end
```
## Example Sitemap
Using the above things channels and items
Sitemap:
```perl
sitemap demo label="Main Menu" {
Frame {
Switch item=HarmonyGreatRoomActivity mappings=[PowerOff="PowerOff", TIVO="TIVO", Music="Music","APPLE TV"="APPLE TV", NETFLIX="NETFLIX"]
Switch item=HarmonyHubGreatButton label="Direction Pad" mappings=[DirectionUp='Up', DirectionDown='Down', DirectionLeft='<', DirectionRight='>', Select='OK']
Switch item=HarmonyGreatRoomDenon mappings=["Volume Up"="Volume Up","Volume Down"="Volume Down"]
}
}
```
## ButtonPress values
Example subset of values for the current activity "buttonPress" channels
```
Mute,VolumeDown,VolumeUp,DirectionDown,DirectionLeft,DirectionRight,DirectionUp,Select,Stop,Play,Rewind,Pause,FastForward,SkipBackward,SkipForward,Menu,Back,Home,SelectGame,PageDown,PageUp,Aspect,Display,Search,Cross,Circle,Square,Triangle,PS,Info,NumberEnter,Hyphen,Number0,Number1,Number2,Number3,Number4,Number5,Number6,Number7,Number8,Number9,PrevChannel,ChannelDown,ChannelUp,Record,FrameAdvance,C,B,D,A,Live,ThumbsDown,ThumbsUp,TiVo,WiiA,WiiB,Guide,Clear,Green,Red,Blue,Yellow,Dot,Return,Favorite,Exit,Sleep
```
A complete list of names for device buttons values can be determined via the REST API for channel-types, <http://YourServer:8080/rest/channel-types>.
Search the JSON for "harmonyhub:device".

View File

@@ -0,0 +1,30 @@
<?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.binding.harmonyhub</artifactId>
<name>openHAB Add-ons :: Bundles :: HarmonyHub Binding</name>
<properties>
<bnd.importpackage>!com.martiansoftware.jsap.*,!org.kohsuke.*</bnd.importpackage>
</properties>
<dependencies>
<dependency>
<groupId>com.github.digitaldan</groupId>
<artifactId>harmony-client</artifactId>
<version>1.1.5</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -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.binding.harmonyhub.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link HarmonyHubBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add device properties
*/
@NonNullByDefault
public class HarmonyHubBindingConstants {
public static final String BINDING_ID = "harmonyhub";
// List of all Thing Type UIDs
public static final ThingTypeUID HARMONY_HUB_THING_TYPE = new ThingTypeUID(BINDING_ID, "hub");
public static final ThingTypeUID HARMONY_DEVICE_THING_TYPE = new ThingTypeUID(BINDING_ID, "device");
// List of all Channel IDs
public static final String CHANNEL_CURRENT_ACTIVITY = "currentActivity";
public static final String CHANNEL_ACTIVITY_STARTING_TRIGGER = "activityStarting";
public static final String CHANNEL_ACTIVITY_STARTED_TRIGGER = "activityStarted";
public static final String CHANNEL_BUTTON_PRESS = "buttonPress";
public static final String CHANNEL_PLAYER = "player";
public static final String DEVICE_PROPERTY_ID = "id";
public static final String DEVICE_PROPERTY_NAME = "name";
public static final String HUB_PROPERTY_ID = "id";
public static final String HUB_PROPERTY_HOST = "host";
public static final String HUB_PROPERTY_NAME = "name";
}

View File

@@ -0,0 +1,177 @@
/**
* 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.harmonyhub.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.harmonyhub.internal.discovery.HarmonyDeviceDiscoveryService;
import org.openhab.binding.harmonyhub.internal.handler.HarmonyDeviceHandler;
import org.openhab.binding.harmonyhub.internal.handler.HarmonyHubHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeProvider;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
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.Reference;
/**
* The {@link HarmonyHubHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
@Component(service = { ThingHandlerFactory.class, ChannelTypeProvider.class,
ChannelGroupTypeProvider.class }, configurationPid = "binding.harmonyhub")
public class HarmonyHubHandlerFactory extends BaseThingHandlerFactory
implements ChannelTypeProvider, ChannelGroupTypeProvider {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.concat(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS.stream(),
HarmonyDeviceHandler.SUPPORTED_THING_TYPES_UIDS.stream())
.collect(Collectors.toSet());
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final HttpClient httpClient;
private final List<ChannelType> channelTypes = new CopyOnWriteArrayList<>();
private final List<ChannelGroupType> channelGroupTypes = new CopyOnWriteArrayList<>();
@Activate
public HarmonyHubHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(HarmonyHubBindingConstants.HARMONY_HUB_THING_TYPE)) {
HarmonyHubHandler harmonyHubHandler = new HarmonyHubHandler((Bridge) thing, this);
registerHarmonyDeviceDiscoveryService(harmonyHubHandler);
return harmonyHubHandler;
}
if (thingTypeUID.equals(HarmonyHubBindingConstants.HARMONY_DEVICE_THING_TYPE)) {
return new HarmonyDeviceHandler(thing, this);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof HarmonyHubHandler) {
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.remove(thingHandler.getThing().getUID());
if (serviceReg != null) {
serviceReg.unregister();
}
}
}
/**
* Adds HarmonyHubHandler to the discovery service to find Harmony Devices
*
* @param harmonyHubHandler
*/
private synchronized void registerHarmonyDeviceDiscoveryService(HarmonyHubHandler harmonyHubHandler) {
HarmonyDeviceDiscoveryService discoveryService = new HarmonyDeviceDiscoveryService(harmonyHubHandler);
this.discoveryServiceRegs.put(harmonyHubHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return channelTypes;
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
for (ChannelType channelType : channelTypes) {
if (channelType.getUID().equals(channelTypeUID)) {
return channelType;
}
}
return null;
}
@Override
public @Nullable ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID,
@Nullable Locale locale) {
for (ChannelGroupType channelGroupType : channelGroupTypes) {
if (channelGroupType.getUID().equals(channelGroupTypeUID)) {
return channelGroupType;
}
}
return null;
}
@Override
public Collection<ChannelGroupType> getChannelGroupTypes(@Nullable Locale locale) {
return channelGroupTypes;
}
public HttpClient getHttpClient() {
return this.httpClient;
}
public void addChannelType(ChannelType type) {
channelTypes.add(type);
}
public void removeChannelType(ChannelType type) {
channelTypes.remove(type);
}
public void removeChannelTypesForThing(ThingUID uid) {
List<ChannelType> removes = new ArrayList<>();
for (ChannelType c : channelTypes) {
if (c.getUID().getAsString().startsWith(uid.getAsString())) {
removes.add(c);
}
}
channelTypes.removeAll(removes);
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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.harmonyhub.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link HarmonyDeviceConfig} class represents the configuration for a device connected to a Harmony Hub
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class HarmonyDeviceConfig {
public int id;
public @Nullable String name;
}

View File

@@ -0,0 +1,28 @@
/**
* 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.harmonyhub.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link HarmonyHubConfig} class represents the configuration of a Harmony Hub
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class HarmonyHubConfig {
public @Nullable String host;
public int heartBeatInterval;
}

View File

@@ -0,0 +1,113 @@
/**
* 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.harmonyhub.internal.discovery;
import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.harmonyhub.internal.handler.HarmonyHubHandler;
import org.openhab.binding.harmonyhub.internal.handler.HubStatusListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.digitaldan.harmony.config.Device;
import com.digitaldan.harmony.config.HarmonyConfig;
/**
* The {@link HarmonyDeviceDiscoveryService} class discovers Harmony Devices connected to a Harmony Hub
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class HarmonyDeviceDiscoveryService extends AbstractDiscoveryService implements HubStatusListener {
private static final int TIMEOUT = 5;
private final Logger logger = LoggerFactory.getLogger(HarmonyDeviceDiscoveryService.class);
private final HarmonyHubHandler bridge;
public HarmonyDeviceDiscoveryService(HarmonyHubHandler bridge) {
super(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
logger.debug("HarmonyDeviceDiscoveryService {}", bridge);
this.bridge = bridge;
this.bridge.addHubStatusListener(this);
}
@Override
protected void startScan() {
discoverDevices();
}
@Override
protected void startBackgroundDiscovery() {
discoverDevices();
}
@Override
public void hubStatusChanged(ThingStatus status) {
if (status.equals(ThingStatus.ONLINE)) {
discoverDevices();
}
}
@Override
protected void deactivate() {
super.deactivate();
bridge.removeHubStatusListener(this);
}
/**
* Discovers devices connected to a hub
*/
private void discoverDevices() {
if (bridge.getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Harmony Hub not online, scanning postponed");
return;
}
logger.debug("getting devices on {}", bridge.getThing().getUID().getId());
bridge.getConfigFuture().thenAccept(this::addDiscoveryResults).exceptionally(e -> {
logger.debug("Could not get harmony config for discovery, skipping");
return null;
});
}
private void addDiscoveryResults(@Nullable HarmonyConfig config) {
if (config == null) {
logger.debug("addDiscoveryResults: skipping null config");
return;
}
for (Device device : config.getDevices()) {
String label = device.getLabel();
int id = device.getId();
ThingUID bridgeUID = bridge.getThing().getUID();
ThingUID thingUID = new ThingUID(HARMONY_DEVICE_THING_TYPE, bridgeUID, String.valueOf(id));
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(thingUID)
.withLabel(label)
.withBridge(bridgeUID)
.withProperty(DEVICE_PROPERTY_ID, id)
.withProperty(DEVICE_PROPERTY_NAME, label)
.build());
// @formatter:on
}
}
}

View File

@@ -0,0 +1,283 @@
/**
* 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.harmonyhub.internal.discovery;
import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.harmonyhub.internal.handler.HarmonyHubHandler;
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.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HarmonyHubDiscoveryService} class discovers Harmony hubs and adds the results to the inbox.
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.harmonyhub")
public class HarmonyHubDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(HarmonyHubDiscoveryService.class);
// notice the port appended to the end of the string
private static final String DISCOVERY_STRING = "_logitech-reverse-bonjour._tcp.local.\n%d";
private static final int DISCOVERY_PORT = 5224;
private static final int TIMEOUT = 15;
private static final long REFRESH = 600;
private boolean running;
private @Nullable HarmonyServer server;
private @Nullable ScheduledFuture<?> broadcastFuture;
private @Nullable ScheduledFuture<?> discoveryFuture;
private @Nullable ScheduledFuture<?> timeoutFuture;
public HarmonyHubDiscoveryService() {
super(HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return HarmonyHubHandler.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public void startScan() {
logger.debug("StartScan called");
startDiscovery();
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Start Harmony Hub background discovery");
ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
if (localDiscoveryFuture == null || localDiscoveryFuture.isCancelled()) {
logger.debug("Start Scan");
discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH, TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stop HarmonyHub background discovery");
ScheduledFuture<?> localDiscoveryFuture = discoveryFuture;
if (localDiscoveryFuture != null && !localDiscoveryFuture.isCancelled()) {
localDiscoveryFuture.cancel(true);
discoveryFuture = null;
}
stopDiscovery();
}
/**
* Starts discovery for Harmony Hubs
*/
private synchronized void startDiscovery() {
if (running) {
return;
}
try {
final HarmonyServer localServer = new HarmonyServer();
localServer.start();
server = localServer;
broadcastFuture = scheduler.scheduleWithFixedDelay(() -> {
sendDiscoveryMessage(String.format(DISCOVERY_STRING, localServer.getPort()));
}, 0, 2, TimeUnit.SECONDS);
timeoutFuture = scheduler.schedule(this::stopDiscovery, TIMEOUT, TimeUnit.SECONDS);
running = true;
} catch (IOException e) {
logger.error("Could not start Harmony discovery server ", e);
}
}
/**
* Stops discovery of Harmony Hubs
*/
private synchronized void stopDiscovery() {
ScheduledFuture<?> localBroadcastFuture = broadcastFuture;
if (localBroadcastFuture != null) {
localBroadcastFuture.cancel(true);
}
ScheduledFuture<?> localTimeoutFuture = timeoutFuture;
if (localTimeoutFuture != null) {
localTimeoutFuture.cancel(true);
}
HarmonyServer localServer = server;
if (localServer != null) {
localServer.stop();
}
running = false;
}
/**
* Send broadcast message over all active interfaces
*
* @param discoverString
* String to be used for the discovery
*/
private void sendDiscoveryMessage(String discoverString) {
try (DatagramSocket bcSend = new DatagramSocket()) {
bcSend.setBroadcast(true);
byte[] sendData = discoverString.getBytes();
// Broadcast the message over all the network interfaces
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue;
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress[] broadcast = new InetAddress[] { InetAddress.getByName("224.0.0.1"),
InetAddress.getByName("255.255.255.255"), interfaceAddress.getBroadcast() };
for (InetAddress bc : broadcast) {
// Send the broadcast package!
if (bc != null) {
try {
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc,
DISCOVERY_PORT);
bcSend.send(sendPacket);
} catch (IOException e) {
logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
} catch (Exception e) {
logger.debug("{}", e.getMessage(), e);
}
logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
networkInterface.getDisplayName());
}
}
}
}
} catch (IOException e) {
logger.debug("IO error during HarmonyHub discovery: {}", e.getMessage());
}
}
/**
* Server which accepts TCP connections from Harmony Hubs during the discovery process
*
* @author Dan Cunningham - Initial contribution
*
*/
private class HarmonyServer {
private final ServerSocket serverSocket;
private final List<String> responses = new ArrayList<>();
private boolean running;
public HarmonyServer() throws IOException {
serverSocket = new ServerSocket(0);
logger.debug("Creating Harmony server on port {}", getPort());
}
public int getPort() {
return serverSocket.getLocalPort();
}
public void start() {
running = true;
Thread localThread = new Thread(this::run, "HarmonyDiscoveryServer(tcp/" + getPort() + ")");
localThread.start();
}
public void stop() {
running = false;
try {
serverSocket.close();
} catch (IOException e) {
logger.error("Could not stop harmony discovery socket", e);
}
}
private void run() {
while (running) {
try (Socket socket = serverSocket.accept();
Reader isr = new InputStreamReader(socket.getInputStream());
BufferedReader in = new BufferedReader(isr)) {
String input;
while ((input = in.readLine()) != null) {
if (!running) {
break;
}
logger.trace("READ {}", input);
// response format is key1:value1;key2:value2;key3:value3;
Map<String, String> properties = Stream.of(input.split(";")).map(line -> line.split(":", 2))
.collect(Collectors.toMap(entry -> entry[0], entry -> entry[1]));
String friendlyName = properties.get("friendlyName");
String hostName = properties.get("host_name");
String ip = properties.get("ip");
if (StringUtils.isNotBlank(friendlyName) && StringUtils.isNotBlank(hostName)
&& StringUtils.isNotBlank(ip) && !responses.contains(hostName)) {
responses.add(hostName);
hubDiscovered(ip, friendlyName, hostName);
}
}
} catch (IOException | IndexOutOfBoundsException e) {
if (running) {
logger.debug("Error connecting with found hub", e);
}
}
}
}
}
private void hubDiscovered(String ip, String friendlyName, String hostName) {
String thingId = hostName.replaceAll("[^A-Za-z0-9\\-_]", "");
logger.trace("Adding HarmonyHub {} ({}) at host {}", friendlyName, thingId, ip);
ThingUID uid = new ThingUID(HARMONY_HUB_THING_TYPE, thingId);
// @formatter:off
thingDiscovered(DiscoveryResultBuilder.create(uid)
.withLabel("HarmonyHub " + friendlyName)
.withProperty(HUB_PROPERTY_HOST, ip)
.withProperty(HUB_PROPERTY_NAME, friendlyName)
.build());
// @formatter:on
}
}

View File

@@ -0,0 +1,232 @@
/**
* 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.harmonyhub.internal.handler;
import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.harmonyhub.internal.HarmonyHubHandlerFactory;
import org.openhab.binding.harmonyhub.internal.config.HarmonyDeviceConfig;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
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.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.digitaldan.harmony.config.ControlGroup;
import com.digitaldan.harmony.config.Device;
import com.digitaldan.harmony.config.Function;
import com.digitaldan.harmony.config.HarmonyConfig;
/**
* The {@link HarmonyDeviceHandler} is responsible for handling commands for Harmony Devices, which are
* sent to one of the channels. It also is responsible for dynamically creating the button press channel
* based on the device's available button press functions.
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class HarmonyDeviceHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(HarmonyDeviceHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(HARMONY_DEVICE_THING_TYPE);
private HarmonyHubHandlerFactory factory;
private @NonNullByDefault({}) HarmonyDeviceConfig config;
public HarmonyDeviceHandler(Thing thing, HarmonyHubHandlerFactory factory) {
super(thing);
this.factory = factory;
}
protected @Nullable HarmonyHubHandler getHarmonyHubHandler() {
Bridge bridge = getBridge();
return bridge != null ? (HarmonyHubHandler) bridge.getHandler() : null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Handling command '{}' for {}", command, channelUID);
if (command instanceof RefreshType) {
// nothing to refresh
return;
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Hub is offline, ignoring command {} for channel {}", command, channelUID);
return;
}
if (!(command instanceof StringType)) {
logger.warn("Command '{}' is not a String type for channel {}", command, channelUID);
return;
}
HarmonyHubHandler hubHandler = getHarmonyHubHandler();
if (hubHandler == null) {
logger.warn("Command '{}' cannot be handled because {} has no bridge", command, getThing().getUID());
return;
}
int id = config.id;
String name = config.name;
String message = "Pressing button '{}' on {}";
if (id > 0) {
logger.debug(message, command, id);
hubHandler.pressButton(id, command.toString());
} else if (name != null) {
logger.debug(message, command, name);
hubHandler.pressButton(name, command.toString());
} else {
logger.warn("Command '{}' cannot be handled because {} has no valid id or name configured", command,
getThing().getUID());
}
// may need to ask the list if this can be set here?
updateState(channelUID, UnDefType.UNDEF);
}
@Override
public void initialize() {
config = getConfigAs(HarmonyDeviceConfig.class);
boolean validConfiguration = config.name != null || config.id >= 0;
if (validConfiguration) {
updateStatus(ThingStatus.UNKNOWN);
updateBridgeStatus();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"A harmony device thing must be configured with a device name OR a postive device id");
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
updateBridgeStatus();
}
@Override
public void dispose() {
factory.removeChannelTypesForThing(getThing().getUID());
}
/**
* Updates our state based on the bridge/hub
*/
private void updateBridgeStatus() {
Bridge bridge = getBridge();
ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
HarmonyHubHandler hubHandler = getHarmonyHubHandler();
boolean bridgeOnline = bridgeStatus == ThingStatus.ONLINE;
boolean thingOnline = getThing().getStatus() == ThingStatus.ONLINE;
if (bridgeOnline && hubHandler != null && !thingOnline) {
updateStatus(ThingStatus.ONLINE);
hubHandler.getConfigFuture().thenAcceptAsync(this::updateButtonPressChannel, scheduler).exceptionally(e -> {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Getting config failed: " + e.getMessage());
return null;
});
} else if (!bridgeOnline || hubHandler == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
/**
* Updates the buttonPress channel with the available buttons as option states.
*/
private void updateButtonPressChannel(@Nullable HarmonyConfig harmonyConfig) {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(
getThing().getUID().getAsString() + ":" + CHANNEL_BUTTON_PRESS);
if (harmonyConfig == null) {
logger.debug("Cannot update {} when HarmonyConfig is null", channelTypeUID);
return;
}
logger.debug("Updating {}", channelTypeUID);
List<StateOption> states = getButtonStateOptions(harmonyConfig);
ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "Send Button Press", "String")
.withDescription("Send a button press to device " + getThing().getLabel())
.withStateDescription(new StateDescription(null, null, null, null, false, states)).build();
factory.addChannelType(channelType);
Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), CHANNEL_BUTTON_PRESS), "String")
.withType(channelTypeUID).build();
// replace existing buttonPress with updated one
List<Channel> newChannels = new ArrayList<>();
for (Channel c : getThing().getChannels()) {
if (!c.getUID().equals(channel.getUID())) {
newChannels.add(c);
}
}
newChannels.add(channel);
ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(newChannels);
updateThing(thingBuilder.build());
}
private List<StateOption> getButtonStateOptions(HarmonyConfig harmonyConfig) {
int id = config.id;
String name = config.name;
List<StateOption> states = new LinkedList<>();
// Iterate through button function commands and add them to our state list
for (Device device : harmonyConfig.getDevices()) {
boolean sameId = name == null && device.getId() == id;
boolean sameName = name != null && name.equals(device.getLabel());
if (sameId || sameName) {
for (ControlGroup controlGroup : device.getControlGroup()) {
for (Function function : controlGroup.getFunction()) {
states.add(new StateOption(function.getName(), function.getLabel()));
}
}
break;
}
}
return states;
}
}

View File

@@ -0,0 +1,448 @@
/**
* 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.harmonyhub.internal.handler;
import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.harmonyhub.internal.HarmonyHubHandlerFactory;
import org.openhab.binding.harmonyhub.internal.config.HarmonyHubConfig;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.RewindFastforwardType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.digitaldan.harmony.HarmonyClient;
import com.digitaldan.harmony.HarmonyClientListener;
import com.digitaldan.harmony.config.Activity;
import com.digitaldan.harmony.config.Activity.Status;
import com.digitaldan.harmony.config.HarmonyConfig;
/**
* The {@link HarmonyHubHandler} is responsible for handling commands for Harmony Hubs, which are
* sent to one of the channels.
*
* @author Dan Cunningham - Initial contribution
* @author Pawel Pieczul - added support for hub status changes
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public class HarmonyHubHandler extends BaseBridgeHandler implements HarmonyClientListener {
private final Logger logger = LoggerFactory.getLogger(HarmonyHubHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(HARMONY_HUB_THING_TYPE);
private static final Comparator<Activity> ACTIVITY_COMPERATOR = Comparator.comparing(Activity::getActivityOrder,
Comparator.nullsFirst(Integer::compareTo));
private static final int RETRY_TIME = 60;
private static final int HEARTBEAT_INTERVAL = 30;
// Websocket will timeout after 60 seconds, pick a sensible max under this,
private static final int HEARTBEAT_INTERVAL_MAX = 50;
private List<HubStatusListener> listeners = new CopyOnWriteArrayList<>();
private final HarmonyHubHandlerFactory factory;
private @NonNullByDefault({}) HarmonyHubConfig config;
private final HarmonyClient client;
private @Nullable ScheduledFuture<?> retryJob;
private @Nullable ScheduledFuture<?> heartBeatJob;
private int heartBeatInterval;
public HarmonyHubHandler(Bridge bridge, HarmonyHubHandlerFactory factory) {
super(bridge);
this.factory = factory;
client = new HarmonyClient(factory.getHttpClient());
client.addListener(this);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Handling command '{}' for {}", command, channelUID);
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Hub is offline, ignoring command {} for channel {}", command, channelUID);
return;
}
if (command instanceof RefreshType) {
client.getCurrentActivity().thenAccept(activity -> {
updateState(activity);
});
return;
}
Channel channel = getThing().getChannel(channelUID.getId());
if (channel == null) {
logger.warn("No such channel for UID {}", channelUID);
return;
}
switch (channel.getUID().getId()) {
case CHANNEL_CURRENT_ACTIVITY:
if (command instanceof DecimalType) {
try {
client.startActivity(((DecimalType) command).intValue());
} catch (Exception e) {
logger.warn("Could not start activity", e);
}
} else {
try {
try {
int actId = Integer.parseInt(command.toString());
client.startActivity(actId);
} catch (NumberFormatException ignored) {
client.startActivityByName(command.toString());
}
} catch (IllegalArgumentException e) {
logger.warn("Activity '{}' is not known by the hub, ignoring it.", command);
} catch (Exception e) {
logger.warn("Could not start activity", e);
}
}
break;
case CHANNEL_BUTTON_PRESS:
client.pressButtonCurrentActivity(command.toString());
break;
case CHANNEL_PLAYER:
String cmd = null;
if (command instanceof PlayPauseType) {
if (command == PlayPauseType.PLAY) {
cmd = "Play";
} else if (command == PlayPauseType.PAUSE) {
cmd = "Pause";
}
} else if (command instanceof NextPreviousType) {
if (command == NextPreviousType.NEXT) {
cmd = "SkipForward";
} else if (command == NextPreviousType.PREVIOUS) {
cmd = "SkipBackward";
}
} else if (command instanceof RewindFastforwardType) {
if (command == RewindFastforwardType.FASTFORWARD) {
cmd = "FastForward";
} else if (command == RewindFastforwardType.REWIND) {
cmd = "Rewind";
}
}
if (cmd != null) {
client.pressButtonCurrentActivity(cmd);
} else {
logger.warn("Unknown player type {}", command);
}
break;
default:
logger.warn("Unknown channel id {}", channel.getUID().getId());
}
}
@Override
public void initialize() {
config = getConfigAs(HarmonyHubConfig.class);
cancelRetry();
updateStatus(ThingStatus.UNKNOWN);
retryJob = scheduler.schedule(this::connect, 0, TimeUnit.SECONDS);
}
@Override
public void dispose() {
listeners.clear();
cancelRetry();
disconnectFromHub();
factory.removeChannelTypesForThing(getThing().getUID());
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail detail, @Nullable String comment) {
super.updateStatus(status, detail, comment);
logger.debug("Updating listeners with status {}", status);
for (HubStatusListener listener : listeners) {
listener.hubStatusChanged(status);
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
client.getCurrentActivity().thenAccept((activity) -> {
updateState(channelUID, new StringType(activity.getLabel()));
});
}
@Override
public void hubDisconnected(@Nullable String reason) {
if (getThing().getStatus() == ThingStatus.ONLINE) {
setOfflineAndReconnect(String.format("Could not connect: %s", reason));
}
}
@Override
public void hubConnected() {
heartBeatJob = scheduler.scheduleWithFixedDelay(() -> {
try {
client.sendPing();
} catch (Exception e) {
logger.debug("heartbeat failed", e);
setOfflineAndReconnect("Hearbeat failed");
}
}, heartBeatInterval, heartBeatInterval, TimeUnit.SECONDS);
updateStatus(ThingStatus.ONLINE);
getConfigFuture().thenAcceptAsync(harmonyConfig -> updateCurrentActivityChannel(harmonyConfig), scheduler)
.exceptionally(e -> {
setOfflineAndReconnect("Getting config failed: " + e.getMessage());
return null;
});
client.getCurrentActivity().thenAccept(activity -> {
updateState(activity);
});
}
@Override
public void activityStatusChanged(@Nullable Activity activity, @Nullable Status status) {
updateActivityStatus(activity, status);
}
@Override
public void activityStarted(@Nullable Activity activity) {
updateState(activity);
}
/**
* Starts the connection process
*/
private synchronized void connect() {
disconnectFromHub();
heartBeatInterval = Math.min(config.heartBeatInterval > 0 ? config.heartBeatInterval : HEARTBEAT_INTERVAL,
HEARTBEAT_INTERVAL_MAX);
String host = config.host;
// earlier versions required a name and used network discovery to find the hub and retrieve the host,
// this section is to not break that and also update older configurations to use the host configuration
// option instead of name
if (StringUtils.isBlank(host)) {
host = getThing().getProperties().get(HUB_PROPERTY_HOST);
if (StringUtils.isNotBlank(host)) {
Configuration genericConfig = getConfig();
genericConfig.put(HUB_PROPERTY_HOST, host);
updateConfiguration(genericConfig);
} else {
logger.debug("host not configured");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "host not configured");
return;
}
}
try {
logger.debug("Connecting: host {}", host);
client.connect(host);
} catch (Exception e) {
logger.debug("Could not connect to HarmonyHub at {}", host, e);
setOfflineAndReconnect("Could not connect: " + e.getMessage());
}
}
private void disconnectFromHub() {
ScheduledFuture<?> localHeartBeatJob = heartBeatJob;
if (localHeartBeatJob != null && !localHeartBeatJob.isDone()) {
localHeartBeatJob.cancel(false);
}
client.disconnect();
}
private void setOfflineAndReconnect(String error) {
disconnectFromHub();
retryJob = scheduler.schedule(this::connect, RETRY_TIME, TimeUnit.SECONDS);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
}
private void cancelRetry() {
ScheduledFuture<?> localRetryJob = retryJob;
if (localRetryJob != null && !localRetryJob.isDone()) {
localRetryJob.cancel(false);
}
}
private void updateState(@Nullable Activity activity) {
if (activity != null) {
logger.debug("Updating current activity to {}", activity.getLabel());
updateState(new ChannelUID(getThing().getUID(), CHANNEL_CURRENT_ACTIVITY),
new StringType(activity.getLabel()));
}
}
private void updateActivityStatus(@Nullable Activity activity, @Nullable Status status) {
if (activity == null) {
logger.debug("Cannot update activity status of {} with activity that is null", getThing().getUID());
return;
} else if (status == null) {
logger.debug("Cannot update activity status of {} with status that is null", getThing().getUID());
return;
}
logger.debug("Received {} activity status for {}", status, activity.getLabel());
switch (status) {
case ACTIVITY_IS_STARTING:
triggerChannel(CHANNEL_ACTIVITY_STARTING_TRIGGER, getEventName(activity));
break;
case ACTIVITY_IS_STARTED:
case HUB_IS_OFF:
// hub is off is received with power-off activity
triggerChannel(CHANNEL_ACTIVITY_STARTED_TRIGGER, getEventName(activity));
break;
case HUB_IS_TURNING_OFF:
// hub is turning off is received for current activity, we will translate it into activity starting
// trigger of power-off activity (with ID=-1)
getConfigFuture().thenAccept(config -> {
if (config != null) {
Activity powerOff = config.getActivityById(-1);
if (powerOff != null) {
triggerChannel(CHANNEL_ACTIVITY_STARTING_TRIGGER, getEventName(powerOff));
}
}
}).exceptionally(e -> {
setOfflineAndReconnect("Getting config failed: " + e.getMessage());
return null;
});
break;
default:
break;
}
}
private String getEventName(Activity activity) {
return activity.getLabel().replaceAll("[^A-Za-z0-9]", "_");
}
/**
* Updates the current activity channel with the available activities as option states.
*/
private void updateCurrentActivityChannel(@Nullable HarmonyConfig config) {
ChannelTypeUID channelTypeUID = new ChannelTypeUID(getThing().getUID() + ":" + CHANNEL_CURRENT_ACTIVITY);
if (config == null) {
logger.debug("Cannot update {} when HarmonyConfig is null", channelTypeUID);
return;
}
logger.debug("Updating {}", channelTypeUID);
List<Activity> activities = config.getActivities();
// sort our activities in order
Collections.sort(activities, ACTIVITY_COMPERATOR);
// add our activities as channel state options
List<StateOption> states = new LinkedList<>();
for (Activity activity : activities) {
states.add(new StateOption(activity.getLabel(), activity.getLabel()));
}
ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "Current Activity", "String")
.withDescription("Current activity for " + getThing().getLabel())
.withStateDescription(new StateDescription(null, null, null, "%s", false, states)).build();
factory.addChannelType(channelType);
Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), CHANNEL_CURRENT_ACTIVITY), "String")
.withType(channelTypeUID).build();
// replace existing currentActivity with updated one
List<Channel> newChannels = new ArrayList<>();
for (Channel c : getThing().getChannels()) {
if (!c.getUID().equals(channel.getUID())) {
newChannels.add(c);
}
}
newChannels.add(channel);
BridgeBuilder thingBuilder = editThing();
thingBuilder.withChannels(newChannels);
updateThing(thingBuilder.build());
}
/**
* Sends a button press to a device
*
* @param device
* @param button
*/
public void pressButton(int device, String button) {
client.pressButton(device, button);
}
/**
* Sends a button press to a device
*
* @param device
* @param button
*/
public void pressButton(String device, String button) {
client.pressButton(device, button);
}
public CompletableFuture<@Nullable HarmonyConfig> getConfigFuture() {
return client.getConfig();
}
/**
* Adds a HubConnectedListener
*
* @param listener
*/
public void addHubStatusListener(HubStatusListener listener) {
listeners.add(listener);
listener.hubStatusChanged(getThing().getStatus());
}
/**
* Removes a HubConnectedListener
*
* @param listener
*/
public void removeHubStatusListener(HubStatusListener listener) {
listeners.remove(listener);
}
}

View File

@@ -0,0 +1,28 @@
/**
* 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.harmonyhub.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingStatus;
/**
* the {@link HarmonyDeviceHandler} interface is for classes wishing to register
* to be called back when a HarmonyHub status changes
*
* @author Dan Cunningham - Initial contribution
* @author Wouter Born - Add null annotations
*/
@NonNullByDefault
public interface HubStatusListener {
public void hubStatusChanged(ThingStatus status);
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="harmonyhub" 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>HarmonyHub Binding</name>
<description>The HarmonyHub Binding integrates Logitech Harmony hubs and remotes.</description>
<author>Dan Cunningham</author>
</binding:binding>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="harmonyhub"
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">
<bridge-type id="hub">
<label>Harmony Hub</label>
<description>A Logitech Harmony Hub</description>
<channels>
<channel id="currentActivity" typeId="currentActivity"/>
<channel id="activityStarting" typeId="eventTrigger">
<label>Activity Starting Trigger</label>
<description>Triggered when an activity is starting</description>
</channel>
<channel id="activityStarted" typeId="eventTrigger">
<label>Activity Started Trigger</label>
<description>Triggered when an activity is started</description>
</channel>
<channel id="buttonPress" typeId="buttonPress">
<label>Button Press</label>
<description>The label/name of the button to press on a Harmony Hub which will be sent to the device associated with
the current activity and label</description>
</channel>
<channel id="player" typeId="player"/>
</channels>
<properties>
<property name="name"></property>
</properties>
<config-description>
<parameter name="host" type="text" required="false">
<label>Host</label>
<description>Host or IP address of hub.</description>
<context>network-address</context>
</parameter>
<parameter name="heartBeatInterval" type="integer" required="false" min="1" max="50">
<label>Heart Beat Interval</label>
<default>30</default>
<description>Heartbeat keep alive time in seconds.
</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="hub"/>
</supported-bridge-type-refs>
<label>Harmony Device</label>
<description>Logitech Harmony Hub Device</description>
<channels>
<channel id="buttonPress" typeId="buttonPress"/>
</channels>
<config-description>
<parameter name="id" type="integer" required="false">
<label>ID</label>
<description>Numeric ID of the Harmony Device (ID or name is required)</description>
</parameter>
<parameter name="name" type="text" required="false">
<label>Name</label>
<description>Name of the Harmony Device (name or ID is required)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="currentActivity">
<item-type>String</item-type>
<label>Current Activity</label>
<description>The label/name of the current activity of a Harmony Hub</description>
</channel-type>
<channel-type id="eventTrigger">
<kind>trigger</kind>
<label>Harmony Hub Event Trigger</label>
<description>Triggered when Harmony Hub sent an event with activity status</description>
</channel-type>
<channel-type id="buttonPress">
<item-type>String</item-type>
<label>Button Press</label>
<description>The label/name of the button to press on a Harmony Hub device (write only)</description>
</channel-type>
<channel-type id="player">
<item-type>Player</item-type>
<label>Player Control</label>
<description>Send player commands (Rewind,FastForward,Play,Pause,SkipForward,SkipBackwards) to the device associated
with the current running activity.</description>
</channel-type>
</thing:thing-descriptions>