/** * Copyright (c) 2010-2016, 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.diyonxbee.internal; import java.awt.Color; import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.util.Dictionary; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.commons.lang.StringUtils; import org.openhab.binding.diyonxbee.DiyOnXBeeBindingProvider; import org.openhab.core.binding.AbstractBinding; import org.openhab.core.events.EventPublisher; import org.openhab.core.items.Item; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.osgi.framework.BundleContext; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.rapplogic.xbee.api.ApiId; import com.rapplogic.xbee.api.PacketListener; import com.rapplogic.xbee.api.XBee; import com.rapplogic.xbee.api.XBeeAddress64; import com.rapplogic.xbee.api.XBeeException; import com.rapplogic.xbee.api.XBeeResponse; import com.rapplogic.xbee.api.XBeeTimeoutException; import com.rapplogic.xbee.api.zigbee.ZNetRxResponse; import com.rapplogic.xbee.api.zigbee.ZNetTxRequest; import com.rapplogic.xbee.util.ByteUtils; /** * * @author juergen.richtsfeld@gmail.com * @since 1.9 */ public class DiyOnXBeeBinding extends AbstractBinding<DiyOnXBeeBindingProvider> implements PacketListener, ManagedService { static final String ITEM_SEPARATOR = "\r\n"; private static final Logger logger = LoggerFactory.getLogger(DiyOnXBeeBinding.class); /** * The BundleContext. This is only valid when the bundle is ACTIVE. It is * set in the activate() method and must not be accessed anymore once the * deactivate() method was called or before activate() was called. */ // private BundleContext bundleContext; /** * the serialPort used to communicate with the XBee */ private String serialPort = ""; /** * The baud rate to use when communicating with the XBee module. Defaults to * 9600 */ private int baudRate = 9600; private XBee xbee; /** * don't use directly, use {@link #xbeeUsageLock} or {@link #xbeeSetupLock} */ private final ReadWriteLock xbeeLock = new ReentrantReadWriteLock(); /** * use this lock for 'normal' usage of the xbee object */ private final Lock xbeeUsageLock = xbeeLock.readLock(); /** * use this lock when changing the {@link #xbee member} */ private final Lock xbeeSetupLock = xbeeLock.writeLock(); private ItemRegistry itemRegistry; private EventPublisher eventPublisher; public DiyOnXBeeBinding() { } public void activate() { logger.debug("Activate"); } public void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void unsetEventPublisher(EventPublisher evt) { this.eventPublisher = null; } public void setItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = itemRegistry; } public void removeItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } /** * Called by the SCR to activate the component with its configuration read * from CAS * * @param bundleContext * BundleContext of the Bundle that defines this component * @param configuration * Configuration properties for this component obtained from the * ConfigAdmin service */ public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) { // this.bundleContext = bundleContext; } /** * Called by the SCR when the configuration of a binding has been changed * through the ConfigAdmin service. * * @param configuration * Updated configuration properties */ public void modified(final Map<String, Object> configuration) { // update the internal configuration accordingly } /** * Called by the SCR to deactivate the component when either the * configuration is removed or mandatory references are no longer satisfied * or the component has simply been stopped. * * @param reason * Reason code for the deactivation:<br> * <ul> * <li>0 – Unspecified * <li>1 – The component was disabled * <li>2 – A reference became unsatisfied * <li>3 – A configuration was changed * <li>4 – A configuration was deleted * <li>5 – The component was disposed * <li>6 – The bundle was stopped * </ul> */ public void deactivate(final int reason) { xbeeSetupLock.lock(); try { if (xbee != null) { xbee.removePacketListener(this); xbee.close(); xbee = null; } } finally { xbeeSetupLock.unlock(); } } /** * @{inheritDoc */ @Override protected void internalReceiveCommand(String itemName, Command command) { // the code being executed when a command was sent on the openHAB // event bus goes here. This method is only called if one of the // BindingProviders provide a binding for the given 'itemName'. logger.debug("internalReceiveCommand({},{}) is called!", itemName, command); for (DiyOnXBeeBindingProvider provider : providers) { if (provider.providesBindingFor(itemName)) { internalReceiveCommand(provider, itemName, command); } } } /** * * @return if the command was sent successfully */ private boolean internalReceiveCommand(DiyOnXBeeBindingProvider provider, String itemName, Command command) { final String remote = provider.getRemote(itemName); final int[] remoteAddress = FormatUtil.fromReadableAddress(remote); Item item; try { item = itemRegistry.getItem(itemName); } catch (ItemNotFoundException e1) { logger.error("unable to get item {}", itemName, e1); return false; } final String commandValue = createCommand(item, command); if (commandValue == null) { logger.warn("unable to create command {} for item {}", commandValue, itemName); return false; } else { logger.debug("created command {} for item {}", commandValue, itemName); } final String commandString = new StringBuilder().append(provider.getId(itemName)).append('=') .append(commandValue).append('\n').toString(); final ZNetTxRequest request = new ZNetTxRequest(new XBeeAddress64(remoteAddress), createPayload(commandString)); xbeeUsageLock.lock(); try { if (xbee == null) { logger.error("cannot send command to {} as the XBee module isn't initialized", itemName); return false; } else { final XBeeResponse response = xbee.sendSynchronous(request); // TODO: // evaluate // response? return true; } } catch (XBeeTimeoutException e) { logger.error("failed sending {} to {}", command, itemName, e); } catch (XBeeException e) { logger.error("failed sending {} to {}", command, itemName, e); } finally { xbeeUsageLock.unlock(); } return false; } private int[] createPayload(String requestString) { return ByteUtils.stringToIntArray(requestString); } private String createCommand(final Item item, Command command) { if (command == OnOffType.ON) { return "ON"; } else if (command == OnOffType.OFF) return "OFF"; else if (command instanceof IncreaseDecreaseType) { final State state = item.getState(); if (state instanceof HSBType) { final HSBType hsbType = (HSBType) state; return changeColorBrightness(hsbType, (IncreaseDecreaseType) command); } else if (state instanceof PercentType) { final PercentType percent = (PercentType) state; final PercentType newBrightness = changeBrightness((IncreaseDecreaseType) command, percent); return makeHUE(newBrightness.floatValue()); } } else if (command instanceof HSBType) { final HSBType hsb = (HSBType) command; return makeRGB(hsb.toColor()); } else if (command instanceof PercentType) { final PercentType percent = (PercentType) command; return makeHUE(percent.floatValue()); } return null; } private String changeColorBrightness(final HSBType hsbType, IncreaseDecreaseType increaseDecrease) { final PercentType brightness = hsbType.getBrightness(); final PercentType newBrightness = changeBrightness(increaseDecrease, brightness); final HSBType newHSB = new HSBType(hsbType.getHue(), hsbType.getSaturation(), newBrightness); return makeRGB(newHSB.toColor()); } private PercentType changeBrightness(IncreaseDecreaseType increaseDecrease, final PercentType brightness) { final PercentType newBrightness; if (increaseDecrease == IncreaseDecreaseType.DECREASE) { BigDecimal changed = brightness.toBigDecimal().subtract(BigDecimal.ONE); if (changed.compareTo(BigDecimal.ZERO) < 0) changed = BigDecimal.ZERO; newBrightness = new PercentType(changed); } else { BigDecimal changed = brightness.toBigDecimal().add(BigDecimal.ONE); if (changed.compareTo(PercentType.HUNDRED.toBigDecimal()) > 0) { changed = PercentType.HUNDRED.toBigDecimal(); } newBrightness = new PercentType(changed); } return newBrightness; } private String makeRGB(Color color) { final StringBuilder sb = new StringBuilder(12); sb.append("RGB"); appendColor(sb, color.getRed()); appendColor(sb, color.getGreen()); appendColor(sb, color.getBlue()); return sb.toString(); } private String makeHUE(final float value) { final StringBuilder sb = new StringBuilder(6); sb.append("HUE"); final float brightnessValue = value / 100f * 255f; appendColor(sb, (int) brightnessValue); return sb.toString(); } private State parseRGBState(final String value) { if (!value.startsWith("RGB") || value.length() != 12) return null; try { final int red = Integer.valueOf(value.substring(3, 6)); final int green = Integer.valueOf(value.substring(6, 9)); final int blue = Integer.valueOf(value.substring(9, 12)); return new HSBType(new Color(red, green, blue)); } catch (NumberFormatException e) { logger.warn("cannot parse color from {}", value, e); } return null; } private State parseHUE(final String value) { if (!value.startsWith("HUE") || value.length() != 6) return null; try { final double hue = Integer.valueOf(value.substring(3, 6)); return new PercentType(new BigDecimal(hue / 255d * 100d)); } catch (NumberFormatException e) { logger.warn("cannot parse color from {}", value, e); } return null; } private void appendColor(StringBuilder sb, int channel) { if (channel < 100) { sb.append('0'); } if (channel < 10) { sb.append('0'); } sb.append(channel); } /** * @{inheritDoc */ @Override protected void internalReceiveUpdate(String itemName, State newState) { // the code being executed when a state was sent on the openHAB // event bus goes here. This method is only called if one of the // BindingProviders provide a binding for the given 'itemName'. logger.debug("internalReceiveUpdate({},{}) is called!", itemName, newState); } private final Map<String, String> lastKeys = new HashMap<String, String>(); @Override public void processResponse(XBeeResponse response) { if (response.getApiId() == ApiId.ZNET_RX_RESPONSE) { final ZNetRxResponse rxResponse = (ZNetRxResponse) response; final String message = ByteUtils.toString(rxResponse.getData()); final String remoteAddress = FormatUtil.readableAddress(rxResponse.getRemoteAddress64().getAddress()); processResponse(message, remoteAddress); } } void processResponse(final String message, final String remoteAddress) { logger.debug("received message: '{}' from '{}'", message, remoteAddress); int startIdx = 0; do { final int idxEquals = message.indexOf('=', startIdx); final int idxEnd = message.indexOf(ITEM_SEPARATOR, startIdx); if (idxEquals > 0 && idxEnd > 0) { if (idxEnd > idxEquals) { final String key = message.substring(startIdx, idxEquals); final String value = message.substring(idxEquals + 1, idxEnd); startIdx = idxEnd + ITEM_SEPARATOR.length(); tryUpdate(key, value, remoteAddress); lastKeys.remove(remoteAddress); } else { final String lastKey = lastKeys.remove(remoteAddress); if (lastKey != null) { final String value = message.substring(startIdx, idxEnd); tryUpdate(lastKey, value, remoteAddress); } startIdx = idxEnd + ITEM_SEPARATOR.length(); } } else if (idxEquals > 0) { lastKeys.put(remoteAddress, message.substring(startIdx, idxEquals)); startIdx = -1; } else { startIdx = -1; } } while (startIdx > 0); } private void tryUpdate(final String key, final String value, final String remoteAddress) { logger.debug("trying to set {} of {} to {}", key, remoteAddress, value); boolean updated = false; for (final DiyOnXBeeBindingProvider provider : providers) { for (final String itemName : provider.getItemNames()) { final String id = provider.getId(itemName); final String remote = provider.getRemote(itemName); if (key.equals(id) && remote.equals(remoteAddress)) { final List<Class<? extends State>> availableTypes = provider.getAvailableItemTypes(itemName); final State state = parseState(value, availableTypes, provider, itemName); if (state != null) { updated = true; eventPublisher.postUpdate(itemName, state); } } } } if (!updated) { logger.warn("unmatched item: key='{}', value='{}' from '{}'", key, value, remoteAddress); } } private State parseState(final String value, final List<Class<? extends State>> availableTypes, DiyOnXBeeBindingProvider provider, String itemName) { State state = TypeParser.parseState(availableTypes, value); if (state == null) { state = parseCustomState(value, availableTypes); } else if (state.getClass() == DecimalType.class) { final Integer max = provider.getMaxValue(itemName); if (max != null) { final DecimalType type = (DecimalType) state; final double percentage = type.doubleValue() / max.intValue() * 100d; return new DecimalType(percentage); } } return state; } private State parseCustomState(String value, List<Class<? extends State>> availableTypes) { for (Class<? extends State> clazz : availableTypes) { if (clazz == HSBType.class) { final State rgb = parseRGBState(value); if (rgb != null) { return rgb; } } if (clazz == PercentType.class) { final State hue = parseHUE(value); if (hue != null) { return hue; } } } return null; } @Override public void updated(Dictionary<String, ?> properties) throws ConfigurationException { { final String serialPort = (String) properties.get("serialPort"); if (StringUtils.isNotBlank(serialPort)) { this.serialPort = serialPort; } } { final String baudRate = (String) properties.get("baudRate"); if (StringUtils.isNotBlank(baudRate)) { this.baudRate = Integer.parseInt(baudRate); } } String canonical = serialPort; try { // This code below enables to use a device name in // /dev/serial/by-id/... on linux File device = new File(serialPort); if (device.canRead()) { canonical = device.getCanonicalPath(); } } catch (IOException e1) { logger.info("unable to get canonical path for '{}'", serialPort); canonical = serialPort; } xbeeSetupLock.lock(); try { if (xbee != null) { xbee.removePacketListener(this); xbee.close(); xbee = null; } xbee = new XBee(); try { logger.info("opening XBee communication on '{}'", canonical); xbee.open(canonical, baudRate); } catch (XBeeException e) { logger.error("failed to open connection to XBee module", e); xbee = null; return; } } finally { xbeeSetupLock.unlock(); } xbee.addPacketListener(this); } }