/** * 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.insteonhub.internal; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang.StringUtils; import org.openhab.binding.insteonhub.InsteonHubBindingProvider; import org.openhab.binding.insteonhub.internal.InsteonHubBindingConfig.BindingType; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubAdjustmentType; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubLevelUpdateType; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubProxy; import org.openhab.binding.insteonhub.internal.hardware.InsteonHubProxyListener; import org.openhab.binding.insteonhub.internal.util.InsteonHubBindingConfigUtil; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.UpDownType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Insteon Hub binding. Handles all commands and polls configured devices to * process updates. * * @author Eric Thill * @since 1.4.0 */ public class InsteonHubBinding extends AbstractActiveBinding<InsteonHubBindingProvider>implements ManagedService { private static final Logger logger = LoggerFactory.getLogger(InsteonHubBinding.class); public static final String DEFAULT_HUB_ID = "_default"; private static final long DEFAULT_REFRESH_INTERVAL = 60000; private static final String BINDING_NAME = "InsteonHubBinding"; private final Map<String, AtomicLong> itemDimTimeouts = Collections .synchronizedMap(new HashMap<String, AtomicLong>()); private long refreshInterval = DEFAULT_REFRESH_INTERVAL; private volatile boolean activated; // Map of proxies. key=hubId, value=proxy // Used to keep track of proxies private final Map<String, InsteonHubProxy> proxies = new HashMap<String, InsteonHubProxy>(); @Override protected long getRefreshInterval() { return refreshInterval; } @Override protected String getName() { return "InsteonHub Refresh Service"; } @Override protected void execute() { logger.debug(BINDING_NAME + " execute"); Set<InsteonHubBindingDeviceInfo> deviceInfos = InsteonHubBindingConfigUtil.getConfiguredDevices(providers); // loop through all configured devices for (InsteonHubBindingDeviceInfo deviceInfo : deviceInfos) { // lookup proxy for device InsteonHubProxy proxy = proxies.get(deviceInfo.getHubId()); if (proxy != null) { // request device level from proxy // this will callback in AsyncEventPublisher if device exists proxy.requestDeviceLevel(deviceInfo.getDeviceId()); } } logger.debug(BINDING_NAME + " execute complete"); } private static int levelToPercent(int level) { return (int) (100 * level / 255.0); } @Override protected void internalReceiveCommand(String itemName, Command command) { // get configuration for this item InsteonHubBindingConfig config = InsteonHubBindingConfigUtil.getConfigForItem(providers, itemName); if (config == null) { logger.error(BINDING_NAME + " received command for unknown item '" + itemName + "'"); return; } // parse info from config BindingType type = config.getBindingType(); String hubId = config.getDeviceInfo().getHubId(); String deviceId = config.getDeviceInfo().getDeviceId(); // lookup proxy from this configuration InsteonHubProxy proxy = proxies.get(hubId); if (proxy == null) { logger.error(BINDING_NAME + " received command for unknown hub id '" + hubId + "'"); return; } if (logger.isDebugEnabled()) { logger.debug(BINDING_NAME + " processing command '" + command + "' of type '" + command.getClass().getSimpleName() + "' for item '" + itemName + "'"); } try { // process according to type if (type == BindingType.SWITCH) { // set value on or off if (command instanceof OnOffType) { proxy.setDevicePower(deviceId, command == OnOffType.ON); } } else if (type == BindingType.DIMMER) { // INSTEON Dimmer supports Dimmer and RollerShutter types if (command instanceof OnOffType) { // ON or OFF => Set level to 255 or 0 int level = command == OnOffType.ON ? 255 : 0; proxy.setDeviceLevel(deviceId, level); } else if (command instanceof IncreaseDecreaseType) { // Increase/Decrease => Incremental Brighten/Dim InsteonHubAdjustmentType adjustmentType; if (command == IncreaseDecreaseType.INCREASE) { adjustmentType = InsteonHubAdjustmentType.BRIGHTEN; } else { adjustmentType = InsteonHubAdjustmentType.DIM; } if (setDimTimeout(itemName)) { proxy.startDeviceAdjustment(deviceId, adjustmentType); } } else if (command instanceof UpDownType) { // Up/Down => Start Brighten/Dim InsteonHubAdjustmentType adjustmentType; if (command == UpDownType.UP) { adjustmentType = InsteonHubAdjustmentType.BRIGHTEN; } else { adjustmentType = InsteonHubAdjustmentType.DIM; } proxy.startDeviceAdjustment(deviceId, adjustmentType); } else if (command instanceof StopMoveType) { // Stop => Stop Brighten/Dim if (command == StopMoveType.STOP) { proxy.stopDeviceAdjustment(deviceId); } } else { // set level from 0 to 100 percent value byte percentByte = Byte.parseByte(command.toString()); float percent = percentByte * .01f; int level = (int) (255 * percent); proxy.setDeviceLevel(deviceId, level); } } } catch (Throwable t) { logger.error("Error processing command '" + command + "' for item '" + itemName + "'", t); } } // returns true if the timeout was not already set private boolean setDimTimeout(String itemName) { AtomicLong timeout = itemDimTimeouts.get(itemName); if (timeout == null) { timeout = new AtomicLong(System.currentTimeMillis() + 400); itemDimTimeouts.put(itemName, timeout); return true; } else { long existing = timeout.getAndSet(System.currentTimeMillis() + 400); return existing == 0; } } protected void addBindingProvider(InsteonHubBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(InsteonHubBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } @Override public synchronized void updated(Dictionary<String, ?> config) throws ConfigurationException { logger.debug(BINDING_NAME + " updated"); try { // Process device configuration if (config != null) { String refreshIntervalString = (String) config.get("refresh"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } // Stop all existing proxy async threads for (InsteonHubProxy proxy : proxies.values()) { proxy.stop(); } // Clear proxy map. It will be rebuilt. proxies.clear(); // Load new proxies Map<String, InsteonHubProxy> newProxies = InsteonHubProxyFactory.createInstances(config); proxies.putAll(newProxies); for (Map.Entry<String, InsteonHubProxy> entry : proxies.entrySet()) { String hubId = entry.getKey(); InsteonHubProxy proxy = entry.getValue(); proxy.addListener(new AsyncEventPublisher(hubId)); // If activated, start proxy now if (activated) { proxy.start(); } } // Set properly configured setProperlyConfigured(true); } } catch (Throwable t) { logger.error("Error configuring " + getName(), t); setProperlyConfigured(false); } } @Override public synchronized void activate() { logger.debug(BINDING_NAME + " activated"); activated = true; dimStopThread.start(); // start all proxy async threads for (InsteonHubProxy proxy : proxies.values()) { proxy.start(); } } @Override public synchronized void deactivate() { logger.debug(BINDING_NAME + " deactivated"); activated = false; // stop all proxy async threads for (InsteonHubProxy proxy : proxies.values()) { proxy.stop(); } } @Override protected void internalReceiveUpdate(String itemName, State newState) { if (logger.isTraceEnabled()) { logger.trace(BINDING_NAME + " received update for '" + itemName + "' of type '" + newState.getClass().getSimpleName() + "' with value '" + newState + "'"); } // ignore } /** * This class listens for updates from the InsteonHubProxy. */ private class AsyncEventPublisher implements InsteonHubProxyListener { private final String hubId; public AsyncEventPublisher(String hubId) { this.hubId = hubId; } @Override public void onLevelUpdate(String device, int level, InsteonHubLevelUpdateType updateType) { Collection<InsteonHubBindingConfig> configs = InsteonHubBindingConfigUtil.getConfigsForDevice(providers, hubId, device); for (InsteonHubBindingConfig config : configs) { BindingType type = config.getBindingType(); // FIXME Currently filtering STATUS_CHANGE out for non-dimmer types b/c it's not working properly. Need // to learn more. if (type == BindingType.SWITCH && updateType == InsteonHubLevelUpdateType.STATUS_CHANGE) { // switch => 0=OFF, else=ON State update = level == 0 ? OnOffType.OFF : OnOffType.ON; sendUpdate(config, update); } else if (type == BindingType.DIMMER && updateType == InsteonHubLevelUpdateType.STATUS_CHANGE) { // dimmer => 0-255 to percent State update = new PercentType(levelToPercent(level)); sendUpdate(config, update); } else if (type == BindingType.INPUT_ON_OFF && updateType == InsteonHubLevelUpdateType.STATUS_CHANGE) { // on/off input => translate Integer onValue = config.getOnValue(); Integer offValue = config.getOffValue(); State update = parseDigitalUpdate(level, onValue, offValue, OnOffType.ON, OnOffType.OFF); sendUpdate(config, update); } else if (type == BindingType.INPUT_OPEN_CLOSED && updateType == InsteonHubLevelUpdateType.STATUS_CHANGE) { // open/closed input => translate Integer openValue = config.getOpenValue(); Integer closedValue = config.getClosedValue(); State update = parseDigitalUpdate(level, openValue, closedValue, OpenClosedType.OPEN, OpenClosedType.CLOSED); sendUpdate(config, update); } else if (type == BindingType.INPUT_UBYTE) { // analog byte value => 0-255 sendUpdate(config, new DecimalType(level)); } else if (type == BindingType.INPUT_PERCENT) { // analog percentage => 0-255 to percent sendUpdate(config, new PercentType(levelToPercent(level))); } } } // Get the corresponding on/off value depending on the configured values private State parseDigitalUpdate(int value, Integer onValue, Integer offValue, State onState, State offState) { if (onValue != null && offValue != null) { // if on and off configured, // if either match => use state // otherwise => UNDEF if (value == onValue) { return onState; } else if (value == offValue) { return offState; } else { return UnDefType.UNDEF; } } else if (onValue != null) { // if only on configured, // if on matches => ON // otherwise => OFF if (value == onValue) { return onState; } else { return offState; } } else if (offValue != null) { // if only off configured, // if off matches => OFF // otherwise => ON if (value == offValue) { return offState; } else { return onState; } } else { // if neither configured, // if 0 => OFF // otherwise => ON if (value == 0) { return offState; } else { return onState; } } } private void sendUpdate(InsteonHubBindingConfig config, State update) { eventPublisher.postUpdate(config.getItemName(), update); } } private final Thread dimStopThread = new Thread() { @Override public void run() { while (activated) { long curTime = System.currentTimeMillis(); synchronized (itemDimTimeouts) { // check all timeouts for (Map.Entry<String, AtomicLong> entry : itemDimTimeouts.entrySet()) { // parse from entry String itemName = entry.getKey(); AtomicLong timeout = entry.getValue(); // check if timeout is set and has elapsed if (timeout.get() > 0 && curTime > timeout.get()) { // timeout elapsed => reset timeout and stop dim/brt timeout.set(0); InsteonHubBindingConfig config = InsteonHubBindingConfigUtil.getConfigForItem(providers, itemName); InsteonHubProxy proxy = proxies.get(config.getDeviceInfo().getHubId()); proxy.stopDeviceAdjustment(config.getDeviceInfo().getDeviceId()); } } } try { Thread.sleep(300); } catch (InterruptedException e) { // ignore } } } }; }