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,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>