/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.io.harmonyhub;
import static java.lang.String.format;
import static java.util.Collections.list;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.whistlingfish.harmony.HarmonyClient;
import net.whistlingfish.harmony.HarmonyHubListener;
import net.whistlingfish.harmony.protocol.LoginToken;
/**
* Handles connection and event handling for all Harmony Hub devices.
*
* @author Matt Tucker
* @author Dan Cunningham
* @since 1.7.0
*/
public class HarmonyHubGateway implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(HarmonyHubGateway.class);
/**
* Our internal mapping of hubs to qualifiers
*/
private Map<String, HarmonyHubInstance> hubs = new HashMap<String, HarmonyHubInstance>();
private Map<String, ScheduledFuture> reconnectJobs = new HashMap<String, ScheduledFuture>();
ScheduledExecutorService reconnectService = Executors.newSingleThreadScheduledExecutor();
/**
* our internal mapping of hub listeners
*/
private final List<HarmonyHubGatewayListener> hubListener = Collections
.synchronizedList(new ArrayList<HarmonyHubGatewayListener>());
/**
* Matching pattern for config params (qualifier.host|username|password)
*/
private static final Pattern CONFIG_PATTERN = Pattern.compile("((.*)\\.)?(host|username|password|discoveryName)");
/**
* Discover timeout
*/
private static final int DISCO_TIME = 30;
/**
* are we configured correctly?
*/
private volatile boolean properlyConfigured = false;
/**
* if no qualifier is given, we use this as a key
*/
private static String NOQUALIFIER = "";
/**
* Bundle starting
*/
public void activate() {
logger.info("HarmonyHub gateway activated");
}
/**
* Bundle stopping
*/
public void deactivate() {
logger.info("HarmonyHub gateway deactivated");
removeAllClients();
}
/**
* Are we configured correctly
*
* @return our configuration state
*/
public boolean isProperlyConfigured() {
return properlyConfigured;
}
/**
* internal method to set our config state, we notify listeners of our
* state change
*
* @param isConfigured
*/
private synchronized void setProperlyConfigured(boolean isConfigured) {
properlyConfigured = isConfigured;
for (HarmonyHubGatewayListener l : hubListener) {
l.configured(isConfigured);
}
}
/**
* Add listener who want to know our configured state
*
* @param listener
*/
public synchronized void addHarmonyHubGatewayListener(HarmonyHubGatewayListener listener) {
if (!hubListener.contains(listener)) {
hubListener.add(listener);
listener.configured(properlyConfigured);
}
}
/**
* Remove listener who no longer want to know our configured state
*
* @param listener
*/
public synchronized void removeHarmonyHubGatewayListener(HarmonyHubGatewayListener listener) {
hubListener.remove(listener);
}
/**
* Remove all clients and clear our map
*/
private synchronized void removeAllClients() {
for (String qualifier : hubs.keySet()) {
HarmonyClient c = hubs.get(qualifier).getClient();
c.disconnect();
}
hubs.clear();
}
@Override
public synchronized void updated(Dictionary<String, ?> config) throws ConfigurationException {
if (config != null) {
final Map<String, HostConfig> hostConfigs = new HashMap<String, HostConfig>();
for (String key : list(config.keys())) {
String value = (String) config.get(key);
Matcher m = CONFIG_PATTERN.matcher(key);
if (!m.matches()) {
continue;
}
String qualifier = checkQualifier(m.group(2));
String configKey = m.group(3);
HostConfig hostConfig = hostConfigs.get(qualifier);
if (hostConfig == null) {
hostConfig = new HostConfig();
hostConfigs.put(qualifier, hostConfig);
}
if (configKey.equals("host")) {
hostConfig.setHost(value);
} else if (configKey.equals("username")) {
hostConfig.setUsername(value);
} else if (configKey.equals("discoveryName")) {
hostConfig.setDiscoveryName(value);
} else if (configKey.equals("password")) {
hostConfig.setPassword(value);
}
}
setProperlyConfigured(false);
// clean up existing clients
removeAllClients();
// don't block OH startup as this may take a bit
new Thread(new Runnable() {
@Override
public void run() {
for (Entry<String, HostConfig> entry : hostConfigs.entrySet()) {
final String qualifier = entry.getKey();
final HostConfig hostConfig = entry.getValue();
connect(qualifier, hostConfig);
}
}
}).start();
}
}
private void connect(final String qualifier, final HostConfig hostConfig) {
if (hostConfig.useDiscovery()) {
setProperlyConfigured(true);
final HarmonyHubDiscovery disco = new HarmonyHubDiscovery(DISCO_TIME, hostConfig.getHost());
disco.addListener(new HarmonyHubDiscoveryListener() {
@Override
public void hubDiscoveryFinished() {
logger.warn("Could not find a HarmonyHub with the discovery name {}",
hostConfig.getDiscoveryName());
scheduleConnect(qualifier, hostConfig);
}
@Override
public void hubDiscovered(HarmonyHubDiscoveryResult result) {
logger.debug("Found HarmonyHub with discoveryName {} looking for {}", result.getFriendlyName(),
hostConfig.getDiscoveryName());
if (result.getFriendlyName().toLowerCase().equals(hostConfig.getDiscoveryName().toLowerCase())) {
disco.removeListener(this);
connectClient(qualifier, result.getHost(), result.getAccountId(), result.getSessionID());
}
}
});
disco.startDiscovery();
} else if (hostConfig.useCredentials()) {
setProperlyConfigured(true);
connectClient(qualifier, hostConfig);
} else {
logger.error("Config must have either a discoveryName or host/username/password");
}
}
private void scheduleConnect(final String qualifier, final HostConfig hostConfig) {
synchronized (reconnectJobs) {
if (reconnectJobs.containsKey(qualifier)) {
ScheduledFuture<?> job = reconnectJobs.remove(qualifier);
job.cancel(true);
}
reconnectJobs.put(qualifier, reconnectService.schedule(new Runnable() {
@Override
public void run() {
connect(qualifier, hostConfig);
}
}, 30, TimeUnit.SECONDS));
}
}
/**
* Connects a client and adds to our map
*
* @param qualifier
* @param hostConfig
*/
private synchronized void connectClient(String qualifier, HostConfig hostConfig) {
try {
if (hubs.containsKey(qualifier)) {
HarmonyHubInstance instance = hubs.get(qualifier);
instance.client.disconnect();
hubs.remove(qualifier);
}
HarmonyClient harmonyClient = HarmonyClient.getInstance();
logger.debug("Connecting {} to {} with user {}", qualifier, hostConfig.getHost(), hostConfig.getUsername());
harmonyClient.connect(hostConfig.getHost(), hostConfig.getUsername(), hostConfig.getPassword());
hubs.put(qualifier, new HarmonyHubInstance(harmonyClient));
logger.debug("Devices for qualifier {}\n{}", qualifier, harmonyClient.getDeviceLabels().toString());
logger.debug("Activity for qualifier {}\n{}", qualifier, harmonyClient.getConfig().getActivities());
logger.debug("Config for qualifier {}\n{}", qualifier, harmonyClient.getConfig().toJson());
} catch (Exception e) {
logger.error(format(//
"Failed creating harmony hub connection to %s", hostConfig.getHost()), e);
}
}
/**
* Connects a client and adds to our map
*
* @param qualifier
* @param hostConfig
*/
private synchronized void connectClient(String qualifier, String host, String accountId, String sessionId) {
try {
if (hubs.containsKey(qualifier)) {
HarmonyHubInstance instance = hubs.get(qualifier);
instance.client.disconnect();
hubs.remove(qualifier);
}
HarmonyClient harmonyClient = HarmonyClient.getInstance();
logger.debug("Connecting {} to {} using discovery tokens", qualifier, host);
harmonyClient.connect(host, new LoginToken(accountId, sessionId));
hubs.put(qualifier, new HarmonyHubInstance(harmonyClient));
logger.debug("Devices for qualifier {}\n{}", qualifier, harmonyClient.getDeviceLabels().toString());
logger.debug("Activity for qualifier {}\n{}", qualifier, harmonyClient.getConfig().getActivities());
logger.debug("Config for qualifier {}\n{}", qualifier, harmonyClient.getConfig().toJson());
} catch (Exception e) {
logger.error(format(//
"Failed creating harmony hub connection to %s", host), e);
}
}
protected interface ClientRunnable {
void run(HarmonyClient client);
}
/**
* Look up a client by its qualifier and have it run our runnable
*
* @param qualifier
* @param runnable
*/
private void withClient(String qualifier, final ClientRunnable runnable) {
final HarmonyHubInstance hub = hubs.get(qualifier);
logger.debug("running for qualifier {} and client {}", qualifier, hub);
if (hub == null) {
throw new IllegalArgumentException(format("No client '%s' defined", qualifier));
}
hub.execute(runnable);
}
/**
* Simulates pressing a button on a harmony remote
*
* @param deviceId
* @param button
*/
public void pressButton(final int deviceId, final String button) {
pressButton(null, deviceId, button);
}
/**
* Simulates pressing a button on a harmony remote
*
* @param qualifier
* @param deviceId
* @param button
*/
public void pressButton(String qualifier, final int deviceId, final String button) {
logger.debug("pressButton for qualifer {} deviceId {} and button {}", qualifier, deviceId, button);
if (!properlyConfigured) {
throw new IllegalStateException(
"Harmony Hub Gateway is not properly configured, or the connection is not yet started");
}
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
client.pressButton(deviceId, button);
}
});
}
/**
* Simulates pressing a button on a harmony remote
*
* @param device
* @param button
*/
public void pressButton(final String device, final String button) {
pressButton(null, device, button);
}
/**
* Simulates pressing a button on a harmony remote
*
* @param qualifier
* @param device
* @param button
*/
public void pressButton(String qualifier, final String device, final String button) {
logger.debug("pressButton for qualifer {} device {} and button {}", qualifier, device, button);
if (!properlyConfigured) {
throw new IllegalStateException(
"Harmony Hub Gateway is not properly configured, or the connection is not yet started");
}
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
try {
client.pressButton(Integer.parseInt(device), button);
} catch (NumberFormatException e) {
client.pressButton(device, button);
}
}
});
}
/**
* Starts a Harmony Hub activity
*
* @param activityId
*/
public void startActivity(final int activityId) {
startActivity(null, activityId);
}
/**
* Starts a Harmony Hub activity
*
* @param qualifier
* @param activityId
*/
public void startActivity(String qualifier, final int activityId) {
logger.debug("startActivity for qualifer {} and activityId {}", qualifier, activityId);
if (!properlyConfigured) {
throw new IllegalStateException(
"Harmony Hub Gateway is not properly configured, or the connection is not yet started");
}
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
client.startActivity(activityId);
}
});
}
/**
* Starts a Harmony Hub activity
*
* @param activity
*/
public void startActivity(final String activity) {
startActivity(null, activity);
}
/**
* Starts a Harmony Hub activity
*
* @param qualifier
* @param activity
*/
public void startActivity(String qualifier, final String activity) {
logger.debug("startActivity for qualifer {} and activity {}", qualifier, activity);
if (!properlyConfigured) {
throw new IllegalStateException(
"Harmony Hub Gateway is not properly configured, or the connection is not yet started");
}
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
try {
client.startActivity(Integer.parseInt(activity));
} catch (NumberFormatException e) {
client.startActivityByName(activity);
}
}
});
}
/**
* Adds a {@link HarmonyHubListener} to a {@link HarmonyClient}
*
* @param listener
*/
public void addListener(final HarmonyHubListener listener) {
addListener(null, listener);
}
/**
* Adds a {@link HarmonyHubListener} to a {@link HarmonyClient}
*
* @param qualifier
* @param listener
*/
public void addListener(String qualifier, final HarmonyHubListener listener) {
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
client.addListener(listener);
}
});
}
/**
* Removes a {@link HarmonyHubListener} from a {@link HarmonyClient}
*
* @param listener
*/
public void removeListener(final HarmonyHubListener listener) {
removeListener(null, listener);
}
/**
* Removes a {@link HarmonyHubListener} from a {@link HarmonyClient}
*
* @param qualifier
* @param listener
*/
public void removeListener(String qualifier, final HarmonyHubListener listener) {
withClient(checkQualifier(qualifier), new ClientRunnable() {
@Override
public void run(HarmonyClient client) {
client.removeListener(listener);
}
});
}
/**
* If no qualifier is given we will use the default {@link NOQUALIFIER}
*
* @param qualifier
* @return original qualifier or {@link NOQUALIFIER}
*/
private String checkQualifier(String qualifier) {
return qualifier == null ? NOQUALIFIER : qualifier;
}
/**
* HarmonyHubInstance holds a {@link HarmonyClient} and a {@link ExecutorService} together
* so that commands to one hub do not block commands to another or stop the main OH
* processing thread.
*
* @author Dan Cunningham
*
*/
class HarmonyHubInstance {
HarmonyClient client;
/**
* execute commmands in their own threads as they could block for some time
*/
ExecutorService executorService = Executors.newSingleThreadExecutor();
/**
* Creates a new HarmonyHubInstance from a given client
*
* @param client
*/
public HarmonyHubInstance(HarmonyClient client) {
super();
this.client = client;
}
/**
* Returns the HarmonyClient assocaited with this instance
*
* @return
*/
public HarmonyClient getClient() {
return client;
}
/**
* Executes the {@link ClientRunnable} in this hubs {@link ExecutorService} and with
* the associated {@link HarmonyClient}
*
* @param clientRunable
*/
public void execute(final ClientRunnable clientRunable) {
executorService.execute(new Runnable() {
@Override
public void run() {
clientRunable.run(client);
}
});
}
}
}