added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user