/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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.plugwise.internal;
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.Dictionary;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.IllegalClassException;
import org.openhab.binding.plugwise.PlugwiseBindingProvider;
import org.openhab.binding.plugwise.PlugwiseCommandType;
import org.openhab.binding.plugwise.internal.PlugwiseGenericBindingProvider.PlugwiseBindingConfigElement;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.TypeParser;
import org.openhab.model.item.binding.BindingConfigParseException;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
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
*
* @author Karel Goderis
* @since 1.1.0
*/
public class PlugwiseBinding extends AbstractActiveBinding<PlugwiseBindingProvider> implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(PlugwiseBinding.class);
private static final Pattern EXTRACT_PLUGWISE_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(mac|port|interval)$");
/** the refresh interval which is used to check for changes in the binding configurations */
private static long refreshInterval = 5000;
private Stick stick;
@SuppressWarnings("rawtypes")
@Override
public void updated(Dictionary config) throws ConfigurationException {
if (config != null) {
// First of all make sure the Stick gets set up
Enumeration keys = config.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
// the config-key enumeration contains additional keys that we
// don't want to process here ...
if ("service.pid".equals(key)) {
continue;
}
Matcher matcher = EXTRACT_PLUGWISE_CONFIG_PATTERN.matcher(key);
if (!matcher.matches()) {
logger.error("given plugwise-config-key '"
+ key
+ "' does not follow the expected pattern '<PlugwiseId>.<mac|port|interval>'");
continue;
}
matcher.reset();
matcher.find();
String plugwiseID = matcher.group(1);
if(plugwiseID.equals("stick")) {
if (stick == null) {
String configKey = matcher.group(2);
String value = (String) config.get(key);
if ("port".equals(configKey)) {
stick = new Stick(value,this);
logger.info("Plugwise added Stick connected to serial port {}",value);
} else if ("interval".equals(configKey)) {
// do nothing for now. we will set in the second run
} else if ("retries".equals(configKey)) {
// do nothing for now. we will set in the second run
}
else {
throw new ConfigurationException(configKey,
"the given configKey '" + configKey + "' is unknown");
}
}
}
}
if(stick != null) {
// re-run through the configuration and setup the remaining devices
keys = config.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
// the config-key enumeration contains additional keys that we
// don't want to process here ...
if ("service.pid".equals(key)) {
continue;
}
Matcher matcher = EXTRACT_PLUGWISE_CONFIG_PATTERN.matcher(key);
if (!matcher.matches()) {
logger.error("given plugwise-config-key '"
+ key
+ "' does not follow the expected pattern '<PlugwiseId>.<mac|port>'");
continue;
}
matcher.reset();
matcher.find();
String plugwiseID = matcher.group(1);
if(plugwiseID.equals("stick")) {
String configKey = matcher.group(2);
String value = (String) config.get(key);
if ("interval".equals(configKey)) {
stick.setInterval(Integer.valueOf(value));
logger.info("Setting the interval to send ZigBee PDUs to {} ms",value);
} else if ("retries".equals(configKey)) {
stick.setRetries(Integer.valueOf(value));
logger.info("Setting the maximum number of attempts to send a message to ",value);
}else if ("port".equals(configKey)) {
//ignore
}
else {
throw new ConfigurationException(configKey,
"the given configKey '" + configKey + "' is unknown");
}
}
PlugwiseDevice device = stick.getDeviceByName(plugwiseID);
if (device == null && !plugwiseID.equals("stick")) {
String configKey = matcher.group(2);
String value = (String) config.get(key);
String MAC = null;
if ("mac".equals(configKey)) {
MAC = value;
}
else {
throw new ConfigurationException(configKey,
"the given configKey '" + configKey + "' is unknown");
}
if(!MAC.equals("")) {
if(plugwiseID.equals("circleplus")) {
if(stick.getDeviceByMAC(MAC)==null) {
device = new CirclePlus(MAC,stick);
logger.info("Plugwise added Circle+ with MAC address: {}",MAC);
}
} else {
if(stick.getDeviceByMAC(MAC)==null) {
device = new Circle(MAC,stick,plugwiseID);
logger.info("Plugwise added Circle with MAC address: {}",MAC);
}
}
stick.plugwiseDeviceCache.add(device);
}
}
}
setProperlyConfigured(true);
} else {
logger.error("Plugwise needs at least one Stick in order to operate");
}
}
}
public void activate() {
// Nothing to do here. We start the binding when the first item bindigconfig is processed
}
public void deactivate() {
if(stick!=null) {
//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 Quarz Scheduler");
}
for (PlugwiseBindingProvider provider : providers) {
try {
for(JobKey jobKey : sched.getJobKeys(jobGroupEquals("Plugwise-"+provider.toString()))) {
sched.deleteJob(jobKey);
}
} catch (SchedulerException e) {
logger.error("An exception occurred while deleting the Plugwise Quartz jobs ({})",e.getMessage());
}
}
stick.close();
}
}
@Override
protected void internalReceiveCommand(String itemName,
Command command) {
PlugwiseBindingProvider provider = findFirstMatchingBindingProvider(itemName);
String commandAsString = command.toString();
if(command != null){
List<Command> commands = new ArrayList<Command>();
// check if the command is valid for this item by checking if a pw ID exists
String checkID = provider.getPlugwiseID(itemName,command);
if(checkID != null) {
commands.add(command);
} else {
// ooops - command is not defined, but maybe we have something of the same Type (e.g Decimal, String types)
//commands = provider.getCommandsByType(itemName, command.getClass());
commands = provider.getAllCommands(itemName);
}
for(Command someCommand : commands) {
String plugwiseID = provider.getPlugwiseID(itemName,someCommand);
PlugwiseCommandType plugwiseCommandType = provider.getPlugwiseCommandType(itemName,someCommand);
if(plugwiseID != null) {
if(plugwiseCommandType != null){
@SuppressWarnings("unused")
boolean result = executeCommand(plugwiseID,plugwiseCommandType,commandAsString);
// Each command is responsible to make sure that a result value for the action is polled from the device
// which then will be used to do a postUpdate
// if new commands would be added later on that do not have this possibility, then a kind of
// auto-update has to be performed here below
} else {
logger.error(
"wrong command type for binding [Item={}, command={}]",
itemName, commandAsString);
}
}
else {
logger.error("{} is an unrecognised command for Item {}",commandAsString,itemName);
}
}
}
}
private boolean executeCommand(String plugwiseID,
PlugwiseCommandType plugwiseCommandType, String commandAsString) {
boolean result = false;
if(plugwiseID != null) {
PlugwiseDevice plug = stick.getDeviceByMAC(plugwiseID);
if(plug != null) {
switch (plugwiseCommandType) {
case CURRENTSTATE:
if(plug instanceof Circle || plug instanceof CirclePlus) {
result = ((Circle)plug).setPowerState(commandAsString);
((Circle)plug).updateInformation();
}
default:
break;
};
} else {
logger.error(
"Plugwise device is not defined for device with ID {}",plugwiseID);
}
}
return result;
}
/**
* Method to post updates to the OH runtime.
*
*
* @param MAC of the Plugwise device concerned
* @param ctype is the Plugwise Command type
* @param value is the value (to be converted) to post
*/
public void postUpdate(String MAC, PlugwiseCommandType ctype, Object value) {
if(MAC != null && ctype != null && value != null) {
for(PlugwiseBindingProvider provider : providers) {
Set<String> qualifiedItems = provider.getItemNames(MAC, ctype);
// Make sure we also capture those devices that were pre-defined with a friendly name in a .cfg or alike
Set<String> qualifiedItemsFriendly = provider.getItemNames(stick.getDevice(MAC).getFriendlyName(), ctype);
qualifiedItems.addAll(qualifiedItemsFriendly);
Type type = null;
try {
type = createStateForType(ctype,value.toString());
} catch (BindingConfigParseException e) {
logger.error("Error parsing a value {} to a state variable of type {}",value.toString(),ctype.getTypeClass().toString());
}
for(String anItem : qualifiedItems) {
if (type instanceof State) {
eventPublisher.postUpdate(anItem, (State) type);
} else {
throw new IllegalClassException("Cannot process update of type " + type.toString());
}
}
}
}
}
@SuppressWarnings("unchecked")
private Type createStateForType(PlugwiseCommandType ctype, String value) throws BindingConfigParseException {
Class<? extends Type> typeClass = ctype.getTypeClass();
List<Class<? extends State>> stateTypeList = new ArrayList<Class<? extends State>>();
stateTypeList.add((Class<? extends State>) typeClass);
State state = TypeParser.parseState(stateTypeList, value);
return state;
}
/**
* Find the first matching {@link PlugwiseBindingProvider}
* according to <code>itemName</code>
*
* @param itemName
*
* @return the matching binding provider or <code>null</code> if no binding
* provider could be found
*/
protected PlugwiseBindingProvider findFirstMatchingBindingProvider(String itemName) {
PlugwiseBindingProvider firstMatchingProvider = null;
for (PlugwiseBindingProvider provider : providers) {
List<String> plugwiseIDs = provider.getPlugwiseID(itemName);
if (plugwiseIDs != null && plugwiseIDs.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 (PlugwiseBindingProvider provider : providers) {
List<PlugwiseBindingConfigElement> compiledList = ((PlugwiseBindingProvider)provider).getIntervalList();
Iterator<PlugwiseBindingConfigElement> pbcIterator = compiledList.iterator();
while(pbcIterator.hasNext()) {
PlugwiseBindingConfigElement anElement = pbcIterator.next();
PlugwiseCommandType type = anElement.getCommandType();
// check if the device already exists (via cfg definition of Role Call)
if(stick.getDevice(anElement.getId())==null) {
logger.debug("The Plugwise device with id {} is not yet defined",anElement.getId());
// check if the config string really contains a MAC address
Pattern MAC_PATTERN = Pattern.compile("(\\w{16})");
Matcher matcher = MAC_PATTERN.matcher(anElement.getId());
if(matcher.matches()){
CirclePlus cp = (CirclePlus) stick.getDeviceByName("circleplus");
if(cp!=null) {
if(!cp.getMAC().equals(anElement.getId())) {
//a circleplus has been added/detected and it is not what is in the binding config
PlugwiseDevice device = new Circle(anElement.getId(),stick,anElement.getId());
stick.plugwiseDeviceCache.add(device);
logger.info("Plugwise added Circle with MAC address: {}",anElement.getId());
}
} else {
logger.warn("Plugwise can not guess the device that should be added. Consider defining it in the openHAB configuration file");
}
} else {
logger.warn("Plugwise can not add a valid device without a proper MAC address. {} can not be used",anElement.getId());
}
}
if(stick.getDevice(anElement.getId())!=null) {
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(anElement.getId()+"-"+type.getJobClass().toString())) {
jobExists = true;
break;
}
}
}
} catch (SchedulerException e1) {
logger.error("An exception occurred while quering the Quartz Scheduler ({})",e1.getMessage());
}
if(!jobExists) {
// set up the Quartz jobs
JobDataMap map = new JobDataMap();
map.put("Stick", stick);
map.put("MAC",stick.getDevice(anElement.getId()).MAC);
JobDetail job = newJob(type.getJobClass())
.withIdentity(anElement.getId()+"-"+type.getJobClass().toString(), "Plugwise-"+provider.toString())
.usingJobData(map)
.build();
Trigger trigger = newTrigger()
.withIdentity(anElement.getId()+"-"+type.getJobClass().toString(), "Plugwise-"+provider.toString())
.startNow()
.withSchedule(simpleSchedule()
.repeatForever()
.withIntervalInSeconds(anElement.getInterval()))
.build();
try {
sched.scheduleJob(job, trigger);
} catch (SchedulerException e) {
logger.error("An exception occurred while scheduling a Quartz Job");
}
}
} else {
logger.error("Error scheduling a Quartz Job for a non-defined Plugwise device");
}
}
}
}
}
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
@Override
protected String getName() {
return "Plugwise Refresh Service";
}
}