/** * 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.dmx.internal.config; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openhab.binding.dmx.DmxBindingProvider; import org.openhab.binding.dmx.DmxService; import org.openhab.binding.dmx.DmxStatusUpdateListener; import org.openhab.binding.dmx.internal.cmd.DmxCommand; import org.openhab.binding.dmx.internal.cmd.DmxFadeCommand; import org.openhab.binding.dmx.internal.cmd.DmxSuspendingFadeCommand; import org.openhab.core.binding.BindingConfig; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.model.item.binding.BindingConfigParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * DMX specific binding configuration for items. Used to link items to their DMX * config. * * @author Davy Vanherbergen * @since 1.2.0 */ public abstract class DmxItem implements BindingConfig, DmxStatusUpdateListener { protected static final Logger logger = LoggerFactory.getLogger(DmxItem.class); private static final Pattern DMX_CHANNEL_PATTERN = Pattern.compile("[\\d,].*(:\\d{3,6}){0,1}"); private static final Pattern DMX_CMD_PATTERN = Pattern.compile("[A-Z0-9]+\\[[A-Z0-9,:\\-/\\|]+\\]"); private static final Pattern DMX_CONFIG_PATTERN = Pattern.compile("([ ]*[A-Z0-9]+\\[[A-Z0-9,:\\-/\\|]+\\][ ,]*)+"); /** Minimum status update delay in ms */ public static int MIN_UPDATE_DELAY = 100; /** DMX channel numbers (512 max) */ protected int[] channels; /** Minimum number of ms between status updates */ protected int updateDelay; /** Binding provider */ private DmxBindingProvider bindingProvider; /** Item name */ protected String name; /** Time when last status update was sent out */ protected long lastUpdated; /** Map holding custom DMX commands which override the standard commands **/ protected Map<String, DmxCommand> customCommands = new HashMap<String, DmxCommand>(); /** Last known state */ protected State currentState; /** * Create new item based on the provided configuration string. * * @param itemName * name for the item * @param configString * item configuration string * @param dmxBindingProvider * BindingProvider which created this item * @throws BindingConfigParseException * when the configstring could not be parsed. */ public DmxItem(String itemName, String configString, DmxBindingProvider dmxBindingProvider) throws BindingConfigParseException { name = itemName; bindingProvider = dmxBindingProvider; Matcher configMatcher = DMX_CONFIG_PATTERN.matcher(configString.replaceAll("(\\s)+", "")); if (!configMatcher.matches()) { throw new BindingConfigParseException( "DMX Configuration must match pattern: " + configMatcher.pattern().toString()); } try { Matcher cmdMatcher = DMX_CMD_PATTERN.matcher(configString.replaceAll("(\\s)+", "")); while (cmdMatcher.find()) { String cmdString = cmdMatcher.group(); String cmd = cmdString.substring(0, cmdString.indexOf('[')); String cmdValue = cmdString.substring(cmdString.indexOf('[') + 1, cmdString.lastIndexOf(']')); if (cmd.equals("CHANNEL")) { parseChannelConfig(cmdValue); } else { String dmxCommandType = cmdValue.split("\\|")[0]; if (dmxCommandType.equals(DmxCommand.types.FADE.toString())) { DmxCommand dmxCommand = new DmxFadeCommand(this, cmdValue.substring(cmdValue.indexOf("|") + 1)); customCommands.put(cmd, dmxCommand); } else if (dmxCommandType.equals(DmxCommand.types.SFADE.toString())) { DmxCommand dmxCommand = new DmxSuspendingFadeCommand(this, cmdValue.substring(cmdValue.indexOf("|") + 1)); customCommands.put(cmd, dmxCommand); } else { throw new BindingConfigParseException("Unsupported DMX command: " + dmxCommandType); } } } if (channels == null) { throw new BindingConfigParseException("No valid channel config found in " + configString); } } catch (Exception e) { logger.error("Invalid DMX configuration for item {} : {}", itemName, e.getMessage()); throw new BindingConfigParseException(e.getMessage()); } } /** * Extract channel id information from channel configuration string. * * @param channelString * string to parse * @throws BindingConfigParseException * if parsing failed. */ private void parseChannelConfig(String channelString) throws BindingConfigParseException { Matcher channelConfigMatcher = DMX_CHANNEL_PATTERN.matcher(channelString); if (!channelConfigMatcher.matches()) { throw new BindingConfigParseException( "DMX channel configuration : " + channelString + " doesn't match " + DMX_CHANNEL_PATTERN); } String[] values = channelString.split(":"); // parse channel number & footprint if (values[0].indexOf('/') == -1) { // no channel width specified String[] tmp = values[0].split(","); if (tmp.length == 1) { int footprint = getFootPrint(); channels = new int[footprint]; int start = parseChannelNumber(tmp[0]); for (int i = 0; i < footprint; i++) { channels[i] = start + i; } } else { channels = new int[tmp.length]; for (int i = 0; i < tmp.length; i++) { channels[i] = parseChannelNumber(tmp[i]); if (channels[i] < 1 || channels[i] > 512) { } } } } else { // channel width specified String[] tmp = values[0].split("/"); int startChannel = parseChannelNumber(tmp[0]); int channelWidth = Integer.parseInt(tmp[1]); channels = new int[channelWidth]; for (int i = 0; i < channelWidth; i++) { channels[i] = startChannel + i; } } // parse update delay if (values.length == 2) { updateDelay = Integer.parseInt(values[1]); if (updateDelay < MIN_UPDATE_DELAY) { updateDelay = 0; } } logger.debug("Linked item {} to channels {}", name, channels); } private int parseChannelNumber(String input) throws BindingConfigParseException { try { int channel = Integer.parseInt(input); if (channel < 1 || channel > 512) { throw new BindingConfigParseException( "DMX channel configuration : " + input + " is not a valid dmx channel (1-512)"); } return channel; } catch (NumberFormatException e) { throw new BindingConfigParseException( "DMX channel configuration : " + input + " is not a valid dmx channel (1-512)"); } } /** * Try to execute the provided openHAB command. * * @param service * DMXservice. * @param command * openHAB command. */ public abstract void processCommand(DmxService service, Command command); /** * Check if the current item wants to be notified of state changes. * * @return true if status updates are needed. */ public abstract boolean isStatusListener(); /** * Check if an openHAB command has been overridden by a DMX command. * * @param cmd * to check * @return true if there is a DMX command available instead. */ protected final boolean isRedefinedByCustomCommand(Command cmd) { return customCommands.containsKey(cmd.toString()); } /** * Publish the new state to the event bus, if it was changed since the last * known value. * * @param state * new state. */ protected void publishState(State state) { lastUpdated = System.currentTimeMillis(); if (bindingProvider == null) { return; } if (currentState != null && currentState.equals(state)) { return; } bindingProvider.postUpdate(name, state); currentState = state; } /** * {@inheritDoc} */ @Override public int getChannel() { return channels[0]; } /** * @return all DMX channels bound to this item */ public int[] getChannels() { return channels; } /** * {@inheritDoc} */ @Override public int getFootPrint() { // default footprint return 1; } /** * {@inheritDoc} */ @Override public int getUpdateDelay() { return updateDelay; } /** * {@inheritDoc} */ @Override public abstract void processStatusUpdate(int[] channelValues); /** * {@inheritDoc} */ @Override public long getLastUpdateTime() { return lastUpdated; } /** * Get the channel for the specified index. * * @param index * @return channel number */ public int getChannel(int index) { return channels[index]; } }