/**
* 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.binding.hdanywhere.internal;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.hdanywhere.HDanywhereBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
import org.openhab.io.net.http.HttpUtil;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Main binding class for the HDanywhere HDMI Matrix. (www.hdanywhere.com)
* This binding does not use the UDP control or RS232 control channels provided by HDanywhere but
* instead uses the output from the built-in webserver to steer the matrix. The main advantage is
* that actual input/output port mappings can be polled, which is not provided by the "standard"
* integration interfaces.
*
* The binding is developed for matrixes with firmware version V1.2(20131222)
*
* @author Karel Goderis
* @since 1.4.0
*/
public class HDanywhereBinding extends AbstractActiveBinding<HDanywhereBindingProvider>implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(HDanywhereBinding.class);
/** the refresh interval which is used to check for changes in the binding configurations */
private static long refreshInterval = 5000;
/** the timeout to use for connecting to a given host (defaults to 5000 milliseconds) */
private static int timeout = 5000;
private static final Pattern EXTRACT_HDANYWHERE_CONFIG_PATTERN = Pattern
.compile("(.*)\\.(.*)\\.(.*)\\.(.*)\\.(ports)$");
/** structure to track configured matrices */
private HashMap<String, Integer> portMappingCache = new HashMap<String, Integer>();
/** structure to store actual input/output states */
private HashMap<String, Integer> matrixCache = new HashMap<String, Integer>();
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
if (config != null) {
Enumeration keys = config.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
Matcher matcher = EXTRACT_HDANYWHERE_CONFIG_PATTERN.matcher(key);
if (!matcher.matches()) {
logger.debug("given hdanywhere-config-key '" + key
+ "' does not follow the expected pattern '<host_IP_Address>.ports'");
continue;
}
matcher.reset();
matcher.find();
String hostIP = matcher.group(1) + "." + matcher.group(2) + "." + matcher.group(3) + "."
+ matcher.group(4);
String configKey = matcher.group(5);
String value = (String) config.get(key);
if ("ports".equals(configKey)) {
matrixCache.put(hostIP, Integer.valueOf(value));
} else {
throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown");
}
}
}
setProperlyConfigured(true);
}
@Override
public void activate() {
// Nothing to do here. We start the binding when the first item bindigconfig is processed
}
@Override
public void deactivate() {
// unschedule all the quartz jobs
Scheduler sched = null;
try {
sched = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
logger.error("An exception occurred while getting a reference to the Quartz Scheduler");
}
for (HDanywhereBindingProvider provider : providers) {
try {
for (JobKey jobKey : sched.getJobKeys(jobGroupEquals("HDanywhere-" + provider.toString()))) {
sched.deleteJob(jobKey);
}
} catch (SchedulerException e) {
logger.error("An exception occurred while deleting the HDanywhere Quartz jobs ({})", e.getMessage());
}
}
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
HDanywhereBindingProvider provider = findFirstMatchingBindingProvider(itemName);
if (provider == null) {
logger.trace("doesn't find matching binding provider [itemName={}, command={}]", itemName, command);
return;
}
List<String> hosts = provider.getHosts(itemName);
int sourcePort = Integer.valueOf(command.toString());
for (String aHost : hosts) {
Integer numberOfPorts = matrixCache.get(aHost);
if (numberOfPorts == null) {
// we default to the smallest matrix currently sold by HDanywhere
numberOfPorts = 4;
}
if (sourcePort > numberOfPorts) {
// nice try - we can switch to a port that does not physically exist
logger.warn("{} goes beyond the physical number of {} ports available on the matrix {}",
new Object[] { sourcePort, numberOfPorts, aHost });
} else {
List<Integer> ports = provider.getPorts(aHost, itemName);
String httpMethod = "GET";
String url = "http://" + aHost + "/switch.cgi?command=3&data0=";
for (Integer aPort : ports) {
url = url + aPort.toString() + "&data1=";
url = url + command.toString() + "&checksum=";
int checksum = 3 + aPort + sourcePort;
url = url + String.valueOf(checksum);
if (isNotBlank(httpMethod) && isNotBlank(url)) {
String response = HttpUtil.executeUrl(httpMethod, url, null, null, null, timeout);
Pattern p = Pattern.compile("The output " + aPort + " select input (.*).");
Matcher m = p.matcher(response);
while (m.find()) {
List<Class<? extends State>> stateTypeList = new ArrayList<Class<? extends State>>();
stateTypeList.add(DecimalType.class);
State state = TypeParser.parseState(stateTypeList, m.group(1));
if (!portMappingCache.containsKey(aHost + ":" + aPort)) {
portMappingCache.put(aHost + ":" + aPort, Integer.valueOf(m.group(1)));
eventPublisher.postUpdate(itemName, state);
} else {
int cachedValue = portMappingCache.get(aHost + ":" + aPort);
if (cachedValue != Integer.valueOf(m.group(1))) {
portMappingCache.put(aHost + ":" + aPort, Integer.valueOf(m.group(1)));
eventPublisher.postUpdate(itemName, state);
}
}
}
}
}
}
}
}
/**
* Find the first matching {@link HDanywhereBindingProvider}
* according to <code>itemName</code>
*
* @param itemName
*
* @return the matching binding provider or <code>null</code> if no binding
* provider could be found
*/
protected HDanywhereBindingProvider findFirstMatchingBindingProvider(String itemName) {
HDanywhereBindingProvider firstMatchingProvider = null;
for (HDanywhereBindingProvider provider : providers) {
List<String> hosts = provider.getHosts(itemName);
if (hosts != null && hosts.size() > 0) {
firstMatchingProvider = provider;
break;
}
}
return firstMatchingProvider;
}
@Override
protected void execute() {
if (isProperlyConfigured()) {
Scheduler sched = null;
try {
sched = StdSchedulerFactory.getDefaultScheduler();
} catch (SchedulerException e) {
logger.error("An exception occurred while getting a reference to the Quartz Scheduler");
}
for (HDanywhereBindingProvider provider : providers) {
HashMap<String, Integer> compiledList = provider.getIntervalList();
if (compiledList != null) {
Iterator<String> pbcIterator = compiledList.keySet().iterator();
while (pbcIterator.hasNext()) {
String aHost = pbcIterator.next();
boolean jobExists = false;
// enumerate each job group
try {
for (String group : sched.getJobGroupNames()) {
// enumerate each job in group
for (JobKey jobKey : sched.getJobKeys(jobGroupEquals(group))) {
if (jobKey.getName().equals(aHost)) {
jobExists = true;
break;
}
}
}
} catch (SchedulerException e1) {
logger.error("An exception occurred while querying the Quartz Scheduler ({})",
e1.getMessage());
}
if (!jobExists) {
// set up the Quartz jobs
JobDataMap map = new JobDataMap();
map.put("host", aHost);
map.put("binding", this);
JobDetail job = newJob(HDanywhereBinding.PollJob.class)
.withIdentity(aHost, "HDanywhere-" + provider.toString()).usingJobData(map).build();
Trigger trigger = newTrigger().withIdentity(aHost, "HDanywhere-" + provider.toString())
.startNow().withSchedule(simpleSchedule().repeatForever()
.withIntervalInSeconds(compiledList.get(aHost)))
.build();
try {
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("An exception occurred while scheduling a Quartz Job");
}
}
}
}
}
}
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "HDanywhere Refresh Service";
}
/**
* Quartz Job that does poll the HDanwywhere matrix and parses the html string returned
* by the built-in webserver
*
*/
public static class PollJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// get the reference to the Stick
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String host = (String) dataMap.get("host");
HDanywhereBinding theBinding = (HDanywhereBinding) dataMap.get("binding");
String httpMethod = "GET";
String url = "http://" + host + "/status_show.shtml";
if (isNotBlank(httpMethod) && isNotBlank(url)) {
String response = HttpUtil.executeUrl(httpMethod, url, null, null, null, timeout);
Integer numberOfPorts = theBinding.matrixCache.get(host);
if (numberOfPorts == null) {
// we default to the smallest matrix currently sold by HDanywhere
numberOfPorts = 4;
}
if (response != null) {
for (int i = 1; i <= numberOfPorts; i++) {
Pattern p = Pattern.compile("var out" + i + "var = (.*);");
Matcher m = p.matcher(response);
while (m.find()) {
List<Class<? extends State>> stateTypeList = new ArrayList<Class<? extends State>>();
stateTypeList.add(DecimalType.class);
State state = TypeParser.parseState(stateTypeList, m.group(1));
for (HDanywhereBindingProvider provider : theBinding.providers) {
Collection<String> theItems = provider.getItemNames();
for (String anItem : theItems) {
List<Integer> itemPorts = provider.getPorts(host, anItem);
for (Integer aPort : itemPorts) {
if (aPort == i) {
if (!theBinding.portMappingCache.containsKey(host + ":" + aPort)) {
theBinding.portMappingCache.put(host + ":" + aPort,
Integer.valueOf(m.group(1)));
theBinding.eventPublisher.postUpdate(anItem, state);
} else {
int cachedValue = theBinding.portMappingCache.get(host + ":" + aPort);
if (cachedValue != Integer.valueOf(m.group(1))) {
theBinding.portMappingCache.put(host + ":" + aPort,
Integer.valueOf(m.group(1)));
theBinding.eventPublisher.postUpdate(anItem, state);
}
}
}
}
}
}
}
}
}
}
}
}
}