/** * 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.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.Calendar; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.IllegalClassException; import org.apache.commons.lang.ObjectUtils; import org.joda.time.DateTime; import org.openhab.binding.plugwise.PlugwiseBindingProvider; import org.openhab.binding.plugwise.PlugwiseCommandType; import org.openhab.binding.plugwise.internal.CirclePlus.SetClockJob; import org.openhab.binding.plugwise.internal.PlugwiseGenericBindingProvider.PlugwiseBindingConfigElement; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; 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.CronScheduleBuilder; import org.quartz.CronTrigger; 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 { public static final String STICK_JOB_DATA_KEY = "Stick"; public static final String MAC_JOB_DATA_KEY = "MAC"; private static final Logger logger = LoggerFactory.getLogger(PlugwiseBinding.class); private static final Pattern EXTRACT_PLUGWISE_CONFIG_PATTERN = Pattern .compile("^(.*?)\\.(mac|type|port|interval)$"); /** the refresh interval which is used to check for changes in the binding configurations */ private static long refreshInterval = 5000; private Stick stick; protected void addBindingProvider(PlugwiseBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(PlugwiseBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { if (config == null) { return; } validateKeyPatternsInConfig(config); stick = setupStick(config); if (stick != null) { setupNonStickDevices(config); stick.startBackgroundThreads(); setProperlyConfigured(true); } else { logger.warn("Plugwise needs at least one Stick in order to operate"); } } private Stick setupStick(Dictionary<String, ?> config) { String port = ObjectUtils.toString(config.get("stick.port"), null); if (port == null) { return null; } Stick stick = new Stick(port, this); logger.debug("Plugwise added Stick connected to serial port {}", port); String interval = ObjectUtils.toString(config.get("stick.interval"), null); if (interval != null) { stick.setInterval(Integer.valueOf(interval)); logger.debug("Setting the interval to send ZigBee PDUs to {} ms", interval); } String retries = ObjectUtils.toString(config.get("stick.retries"), null); if (retries != null) { stick.setRetries(Integer.valueOf(retries)); logger.debug("Setting the maximum number of attempts to send a message to ", retries); } return stick; } private void setupNonStickDevices(Dictionary<String, ?> config) { Set<String> deviceNames = getDeviceNamesFromConfig(config); for (String deviceName : deviceNames) { if ("stick".equals(deviceName)) { continue; } if (stick.getDeviceByName(deviceName) != null) { continue; } String MAC = ObjectUtils.toString(config.get(deviceName + ".mac"), null); if (MAC == null || MAC.equals("")) { logger.warn("Plugwise can not add device with name {} without a MAC address", deviceName); } else if (stick.getDeviceByMAC(MAC) != null) { logger.warn( "Plugwise can not add device with name: {} and MAC address: {}, " + "the same MAC address is already used by device with name: {}", deviceName, MAC, stick.getDeviceByMAC(MAC).name); } else { String deviceType = ObjectUtils.toString(config.get(deviceName + ".type"), null); PlugwiseDevice device = createPlugwiseDevice(deviceType, MAC, deviceName); if (device != null) { stick.addDevice(device); } } } } private PlugwiseDevice createPlugwiseDevice(String deviceType, String MAC, String deviceName) { PlugwiseDevice device = null; if ("circleplus".equals(deviceType) || "circleplus".equals(deviceName)) { // for backwards compatibility a device with the name 'circleplus' always creates a CirclePlus device = new CirclePlus(MAC, stick, deviceName); logger.debug("Plugwise created Circle+ with name: {} and MAC address: {}", deviceName, MAC); } else if ("circle".equals(deviceType) || deviceType == null) { // for backwards compatibility a device without a deviceType always creates a Circle device = new Circle(MAC, stick, deviceName); logger.debug("Plugwise created Circle with name: {} and MAC address: {}", deviceName, MAC); } else if ("scan".equals(deviceType)) { device = new Scan(MAC, stick, deviceName); logger.debug("Plugwise created Scan with name: {} and MAC address: {}", deviceName, MAC); } else if ("sense".equals(deviceType)) { device = new Sense(MAC, stick, deviceName); logger.debug("Plugwise created Sense with name: {} and MAC address: {}", deviceName, MAC); } else if ("stealth".equals(deviceType)) { device = new Stealth(MAC, stick, deviceName); logger.debug("Plugwise created Stealth with name: {} and MAC address: {}", deviceName, MAC); } else if ("switch".equals(deviceType)) { device = new Switch(MAC, stick, deviceName); logger.debug("Plugwise created Switch with name: {} and MAC address: {}", deviceName, MAC); } else { logger.warn( "Plugwise can not create device with name: '{}' because it has an unknown device type: '{}'. " + "Known device types are: circle|circleplus|scan|sense|stealth|switch", deviceName, deviceType); } return device; } private Set<String> getDeviceNamesFromConfig(Dictionary<String, ?> config) { Set<String> names = new HashSet<String>(); Enumeration<String> keys = config.keys(); while (keys.hasMoreElements()) { String key = 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()) { continue; } matcher.reset(); matcher.find(); String name = matcher.group(1); names.add(name); } return names; } private void validateKeyPatternsInConfig(Dictionary<String, ?> config) { Enumeration<String> keys = config.keys(); while (keys.hasMoreElements()) { String key = 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.warn("Given plugwise-config-key '" + key + "' does not follow the expected pattern '<PlugwiseId>.<mac|type|port|interval>'"); continue; } } } @Override public void activate() { // Nothing to do here. We start the binding when the first item bindigconfig is processed } @Override public void deactivate() { if (stick != null) { // unschedule all the quartz jobs try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); 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()); } } } catch (SchedulerException e) { logger.error("An exception occurred while getting a reference to the Quartz Scheduler ({})", e.getMessage()); } stick.close(); } } @Override protected void internalReceiveCommand(String itemName, Command command) { PlugwiseBindingProvider provider = findFirstMatchingBindingProvider(itemName); if (command != null) { String commandAsString = command.toString(); 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.getDevice(plugwiseID); if (plug != null) { switch (plugwiseCommandType) { case CURRENTSTATE: if (plug instanceof Circle) { result = ((Circle) plug).setPowerState(commandAsString); } 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).getName(), ctype); qualifiedItems.addAll(qualifiedItemsFriendly); State type = null; try { type = createStateForType(ctype, value); } catch (BindingConfigParseException e) { logger.error("Error parsing a value {} to a state variable of type {}", value.toString(), ctype.getTypeClass().toString()); } for (String item : qualifiedItems) { if (type instanceof State) { eventPublisher.postUpdate(item, type); } else { throw new IllegalClassException( "Cannot process update of type " + (type == null ? "null" : type.toString())); } } } } } @SuppressWarnings("unchecked") private State createStateForType(PlugwiseCommandType ctype, Object value) throws BindingConfigParseException { Class<? extends Type> typeClass = ctype.getTypeClass(); // the logic below covers all possible command types and value types if (typeClass == DecimalType.class && value instanceof Float) { return new DecimalType((Float) value); } else if (typeClass == DecimalType.class && value instanceof Double) { return new DecimalType((Double) value); } else if (typeClass == OnOffType.class && value instanceof Boolean) { return ((Boolean) value).booleanValue() ? OnOffType.ON : OnOffType.OFF; } else if (typeClass == DateTimeType.class && value instanceof Calendar) { return new DateTimeType((Calendar) value); } else if (typeClass == DateTimeType.class && value instanceof DateTime) { return new DateTimeType(((DateTime) value).toCalendar(Locale.getDefault())); } else if (typeClass == StringType.class && value instanceof String) { return new StringType((String) value); } logger.debug("less efficient (generic) logic is applied for converting a Plugwise value " + "(command type class: {}, value class {})", typeClass.getName(), value.getClass().getName()); List<Class<? extends State>> stateTypeList = new ArrayList<Class<? extends State>>(); stateTypeList.add((Class<? extends State>) typeClass); return TypeParser.parseState(stateTypeList, value.toString()); } /** * 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()) { try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); scheduleJobs(sched); } catch (SchedulerException e) { logger.error("An exception occurred while getting a reference to the Quartz Scheduler ({})", e.getMessage()); } } } private void scheduleJobs(Scheduler scheduler) { for (PlugwiseBindingProvider provider : providers) { for (PlugwiseBindingConfigElement element : provider.getIntervalList()) { PlugwiseCommandType type = element.getCommandType(); if (type.getJobClass() == null) { continue; } // check if the device already exists (via cfg definition of Role Call) if (stick.getDevice(element.getId()) == null) { logger.debug("The Plugwise device with id {} is not yet defined", element.getId()); // check if the config string really contains a MAC address Pattern MAC_PATTERN = Pattern.compile("(\\w{16})"); Matcher matcher = MAC_PATTERN.matcher(element.getId()); if (matcher.matches()) { List<CirclePlus> cps = stick.getDevicesByClass(CirclePlus.class); if (!cps.isEmpty()) { CirclePlus cp = cps.get(0); if (!cp.getMAC().equals(element.getId())) { // a circleplus has been added/detected and it is not what is in the binding config PlugwiseDevice device = new Circle(element.getId(), stick, element.getId()); stick.addDevice(device); logger.debug("Plugwise added Circle with MAC address: {}", element.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", element.getId()); } } if (stick.getDevice(element.getId()) != null) { String jobName = element.getId() + "-" + type.getJobClass().toString(); if (!isExistingJob(scheduler, jobName)) { // set up the Quartz jobs JobDataMap map = new JobDataMap(); map.put(STICK_JOB_DATA_KEY, stick); map.put(MAC_JOB_DATA_KEY, stick.getDevice(element.getId()).MAC); JobDetail job = newJob(type.getJobClass()) .withIdentity(jobName, "Plugwise-" + provider.toString()).usingJobData(map).build(); Trigger trigger = newTrigger() .withIdentity(element.getId() + "-" + type.getJobClass().toString(), "Plugwise-" + provider.toString()) .startNow() .withSchedule( simpleSchedule().repeatForever().withIntervalInSeconds(element.getInterval())) .build(); try { scheduler.scheduleJob(job, trigger); } catch (SchedulerException e) { logger.error("An exception occurred while scheduling a Plugwise Quartz Job", e); } } } else { logger.error("Error scheduling a Quartz Job for a non-defined Plugwise device (" + element.getId() + ")"); } } } List<CirclePlus> cps = stick.getDevicesByClass(CirclePlus.class); if (!cps.isEmpty()) { CirclePlus cp = cps.get(0); String jobName = cp.MAC + "-SetCirclePlusClock"; if (!isExistingJob(scheduler, jobName)) { JobDataMap map = new JobDataMap(); map.put(CirclePlus.CIRCLE_PLUS_JOB_DATA_KEY, cp); JobDetail job = newJob(SetClockJob.class).withIdentity(jobName, "Plugwise").usingJobData(map).build(); CronTrigger trigger = newTrigger().withIdentity(jobName, "Plugwise").startNow() .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")).build(); try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); sched.scheduleJob(job, trigger); } catch (SchedulerException e) { logger.error("Error scheduling Circle+ setClock Quartz Job", e); } } } } private boolean isExistingJob(Scheduler scheduler, String jobName) { try { for (String group : scheduler.getJobGroupNames()) { for (JobKey jobKey : scheduler.getJobKeys(jobGroupEquals(group))) { if (jobKey.getName().equals(jobName)) { return true; } } } } catch (SchedulerException e1) { logger.error("An exception occurred while querying the Quartz Scheduler ({})", e1.getMessage()); } return false; } @Override protected long getRefreshInterval() { return refreshInterval; } @Override protected String getName() { return "Plugwise Refresh Service"; } }