/** * 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.mios.internal.config; import java.util.Calendar; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openhab.binding.mios.internal.MiosActivator; import org.openhab.core.binding.BindingConfig; import org.openhab.core.items.Item; import org.openhab.core.library.items.ColorItem; import org.openhab.core.library.items.ContactItem; import org.openhab.core.library.items.DateTimeItem; import org.openhab.core.library.items.DimmerItem; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.RollershutterItem; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; 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.StringType; import org.openhab.core.transform.TransformationException; import org.openhab.core.transform.TransformationHelper; import org.openhab.core.transform.TransformationService; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.openhab.core.types.UnDefType; import org.openhab.model.item.binding.BindingConfigParseException; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * MiOS specific abstract class for the openHAB {@link BindingConfig}. * <p> * * Sub-classes of this class represent the various "types" of data that can be bound within a MiOS System. * <p> * These include the following objects: <br> * <ul> * <li>System - {@link SystemBindingConfig} * <li>Scene - {@link SceneBindingConfig} * <li>Device - {@link DeviceBindingConfig} * </ul> * <p> * * The general form of a MiOS Binding is: * <p> * <ul> * <li> * <tt>mios="unit:<i>unitName</i>,<i>miosThing</i>{,command:<i>commandTransform</i>}{,in:<i>inTransform</i>}{,out:<i>outTransform</i>}"</tt> * </ul> * <p> * * Each sub-class has a specific format for the <i>miosThing</i>, and examples are outlined in those sub-classes, in * addition to the <code>README.md</code> file shipped with the binding. * * * @author Mark Clark * @since 1.6.0 */ public abstract class MiosBindingConfig implements BindingConfig { private static Pattern TRANSFORM_PATTERN = Pattern.compile("(?<transform>[a-zA-Z]+)[(]{1}?(?<param>.*)[)]{1}"); protected static final Logger logger = LoggerFactory.getLogger(MiosBindingConfig.class); private int miosId; private String context; private String itemName; private String unitName; private String miosStuff; private String cachedProperty; private String cachedBinding; private Class<? extends Item> itemType; private String commandTransform; private String inTransform; private String outTransform; private String inTransformName = null; private String inTransformParam = null; private String outTransformName = null; private String outTransformParam = null; private TransformationService inTransformationService; private TransformationService outTransformationService; protected MiosBindingConfig(String context, String itemName, String unitName, int miosId, String miosStuff, Class<? extends Item> itemType, String commandTransform, String inTransform, String outTransform) throws BindingConfigParseException { // Crude parameter validations assert(miosId >= 0); this.context = context; this.itemName = itemName; this.unitName = unitName; this.miosId = miosId; this.miosStuff = miosStuff; this.itemType = itemType; this.commandTransform = commandTransform; this.inTransform = inTransform; this.outTransform = outTransform; Matcher m; if (inTransform != null) { m = TRANSFORM_PATTERN.matcher(inTransform); if (m.matches()) { this.inTransformName = m.group("transform"); this.inTransformParam = m.group("param"); logger.trace("start: IN '{}', '{}'", this.inTransformName, this.inTransformParam); } else { logger.warn("Unable to parse (in) Transformation string '{}', ignored.", inTransform); } } if (outTransform != null) { m = TRANSFORM_PATTERN.matcher(outTransform); if (m.matches()) { this.outTransformName = m.group("transform"); this.outTransformParam = m.group("param"); logger.trace("start: OUT '{}', '{}'", this.outTransformName, this.outTransformParam); } else { logger.warn("Unable to parse (out) Transformation string '{}', ignored.", outTransform); } } } /** * The name of the openHAB context. * * @return the name of the openHAB context this Binding Configuration is associated with. */ public String getContext() { return this.context; } /** * The name of the openHAB Item. * * @return the name of the Item this Binding Configuration is associated with. */ public String getItemName() { return this.itemName; } /** * The name of the MiOS Unit. * <p> * * @return the name of the MiOS Unit this Binding Configuration is associated with. */ public String getUnitName() { return unitName; }; /** * The type of MiOS object. * <p> * * The MiOS Binding supports binding to Device, Scene, and System objects within a MiOS System. This method returns * which type of MiOS object the Binding Configuration represents, using the same format/value used in the Binding * Configuration entry. * <p> * The value returned is dependent upon the specific sub-class of MiOSBindingConfig materialized from the Item * configuration information. * * @return the type of MiOS object. */ public abstract String getMiosType(); /** * The id of the MiOS object. * <p> * * Each object within a MiOS Unit has a type-specific Id. If the type of object doesn't have an Id, then the value * will be 0. * * @return the Id of the MiOS object, or 0 if the type doesn't allocate Id's. */ public int getMiosId() { return miosId; }; /** * A type-specific Name/Reference representing a named subcomponent of the MiOS object. * <p> * * Each object within a MiOS Unit has a type-specific Name/Reference string. This value works in conjunction with * the MiOS Id, to further define the data to be returned. One "id" in a MiOS system may have multiple attributes * associated with it, and this string is used to disambiguate those attributes. * * @return the Name/Reference of the MiOS object represented by this Binding Configuration. */ public String getMiosStuff() { return miosStuff; } /** * Convert to a Property string. * <p> * * This form includes the MiOS Device locator information (Id, Type, and Stuff) only. * <p> * It contains only enough information to identify the target component within the MiOS Unit. * * @return the Binding Configuration as a string, suitable for use within openHAB Configuration files. */ public String toProperty() { return cachedProperty; } /** * Convert to a full Binding string. * <p> * * This form includes the MiOS Device locator information, from <code>toProperty()</code>, in addition to any * transformations ( <code>in:</code>, <code>out:</code>, <code>command:</code>) specified in the original Binding. */ public String toBinding() { return cachedBinding; } protected Class<? extends Item> getItemType() { return itemType; } private String getInTransform() { return inTransform; } private String getInTransformName() { return inTransformName; } private String getInTransformParam() { return inTransformParam; } private String getOutTransform() { return outTransform; } private String getOutTransformName() { return outTransformName; } private String getOutTransformParam() { return outTransformParam; } private TransformationService getInTransformationService() { String name = getInTransformName(); if (name == null || name.equals("")) { return null; } if (inTransformationService != null) { return inTransformationService; } BundleContext context = MiosActivator.getContext(); inTransformationService = TransformationHelper.getTransformationService(context, name); if (inTransformationService == null) { logger.warn("Transformation Service (in) '{}' not found for declaration '{}'.", name, getInTransform()); } return inTransformationService; } /** * Returns a {@link State} which is inherited from the {@link Item}s accepted DataTypes. The call is delegated to * the {@link TypeParser}. If <code>item</code> is <code>null</code> the {@link StringType} is used. * <p> * * Code borrowed from {@link HttpBinding}. * * @param itemType * @param value * * @return a {@link State} which type is inherited by the {@link TypeParser} or a {@link StringType} if * <code>item</code> is <code>null</code> */ private State createState(String value) { Class<? extends Item> itemType = getItemType(); State result; try { if (itemType.isAssignableFrom(NumberItem.class)) { // // For things like Weather Items when they're bound to // NumberItems. // // eg. Heat Index if ("".equals(value)) { result = UnDefType.NULL; } else { result = DecimalType.valueOf(value); } } else if (itemType.isAssignableFrom(ContactItem.class)) { result = OpenClosedType.valueOf(value); } else if (itemType.isAssignableFrom(SwitchItem.class)) { result = OnOffType.valueOf(value); } else if (itemType.isAssignableFrom(DimmerItem.class)) { result = PercentType.valueOf(value); } else if (itemType.isAssignableFrom(RollershutterItem.class)) { result = PercentType.valueOf(value); } else if (itemType.isAssignableFrom(DateTimeItem.class)) { try { // // If we're presented with the empty string, then consider // it to be the Undefined NULL value. // // This has been observed during Full-updates from a MiOS // unit that has Leviton Scene Controllers present // (LastUpdated). // if ("".equals(value)) { result = UnDefType.NULL; } else { // // See if it "looks" like an Epoch-style date. MiOS // Units return these as String/Integer versions of the // date and they need to be converted. // // This logic really belongs inside the OH 1.x core // class DateTimeType, but that's closed to changes... // doing it here also avoids the thread-safety issues // present in the DateTimeType class. // long l = Long.parseLong(value) * 1000; Calendar c = Calendar.getInstance(); c.setTimeInMillis(l); result = new DateTimeType(c); } } catch (NumberFormatException nfe) { result = DateTimeType.valueOf(value); } } else if (itemType.isAssignableFrom(ColorItem.class)) { result = HSBType.valueOf(value); } else { result = StringType.valueOf(value); } logger.trace("createState: Converted '{}' to '{}', bound to '{}'", new Object[] { value, result.getClass().getName(), itemType }); return result; } catch (Exception e) { logger.debug("Couldn't create state of type '{}' for value '{}'", itemType, value); return StringType.valueOf(value); } } /** * Transform data from a MiOS Unit into a form suitable for use in an openHAB Item. * <p> * * Data received from a MiOS unit is in a number of different formats (String, Boolean, DataTime, etc). These values * may need to be transformed from their original format prior to being pushed into the corresponding openHAB Item. * <p> * This method is used internally within the MiOS Binding to perform that transformation. * <p> * This method is responsible for transforming the inbound value from the MiOS Unit, into the form required by the * openHAB Item. * <p> * metadata supplied by the user, via the <code>in:</code> parameter, in the Binding Configuration is used to define * the transformation that must be performed. * <p> * If the <code>in:</code> parameter is missing, then no transformation will occur, and the source-value will be * returned (as a <code>StringType</code>). * <p> * If the <code>in:</code> parameter is present, then its value is used to determine which openHAB * TransformationService should be used to transform the value. * * @return the transformed value, or the input (<code>value</code>) if no transformation has been specified in the * Binding Configuration. * * @throws TransformationException * if the underlying Transformation fails in any manner. */ public State transformIn(State value) throws TransformationException { TransformationService ts = getInTransformationService(); if (ts != null) { return createState(ts.transform(getInTransformParam(), value.toString())); } else { if (value instanceof StringType) { value = createState(value.toString()); logger.trace("transformIn: Converted value '{}' from StringType to more scoped type '{}'", value, value.getClass()); } return value; } } private TransformationService getOutTransformationService() { String name = getOutTransformName(); if (name == null || name.equals("")) { return null; } if (outTransformationService != null) { return outTransformationService; } BundleContext context = MiosActivator.getContext(); outTransformationService = TransformationHelper.getTransformationService(context, name); if (outTransformationService == null) { logger.warn("Transformation Service (out) '{}' not found for declaration '{}'.", name, getOutTransform()); } return outTransformationService; } /** * Transform data in an openHAB Item into a form suitable for use in calls made to a MiOS Unit. * <p> * * In order to calls a MiOS Unit, we may need to transform an Item's current value from its openHAB State to a form * suitable for transmission to remote MiOS Unit. * <p> * This method is responsible for transforming an Item's State value, using metadata supplied by the user, via the * <code>out:</code> parameter, in the Binding Configuration. * <p> * If the <code>out:</code> parameter is missing, then no transformation will occur, and the source-value will be * returned. * <p> * If the <code>out:</code> parameter is present, then its value is used to determine which openHAB * TransformationService should be used to transform the value. * * @return the transformed value, or the input (<code>value</code>) if no transformation has been specified in the * Binding Configuration. * * @throws TransformationException * if the underlying Transformation fails in any manner. */ public State transformOut(State value) throws TransformationException { TransformationService ts = getOutTransformationService(); if (ts != null) { return createState(ts.transform(getOutTransformParam(), value.toString())); } else { return value; } } /** * Transform an openHAB {@link Command} into a call suitable for invoking a service on a MiOS Unit. * <p> * * In order to call a MiOS Unit, we need the openHAB {@link Command} in the format they understand. This method is * responsible for transforming the Command, using the metadata supplied by the user in the <code>command:</code> * parameter of the Binding Configuration. Internally, this method uses that metadata to perform a transformation, * using the openHAB Transformation service, to perform the mapping. * <p> * If the <code>command:</code> parameter is missing, then no commands can be delivered to the MiOS Unit, and * <code>null</code> is returned. * <p> * If the <code>command:</code> parameter is present, then the associated openHAB Transformation Service is invoked * to perform the required mapping, and the transformed value is returned. * * @return the transformed value, or null if no transformation has been specified in the Binding Configuration. * * @throws TransformationException * if the underlying Transformation fails in any manner. */ public abstract String transformCommand(Command command) throws TransformationException; /** * Implementation method to be overridden by sub-classes when they have a different "format" of output for * <code>toProperty()</code>. * <p> */ protected void initialize() { StringBuilder result = new StringBuilder(100); String tmp; int i = getMiosId(); String id = ((i == 0) ? "" : String.valueOf(i)); String stuff = getMiosStuff(); String unit = getUnitName(); // Normalize the Default MiOS Unit name, so it doesn't appear in the // property String. unit = (unit == null) ? "" : "unit:" + unit + ','; if (stuff != null) { result.append(unit).append(getMiosType()).append(':').append(id).append('/').append(stuff); } else { result.append(unit).append(getMiosType()).append(':').append(id); } cachedProperty = result.toString(); tmp = getCommandTransform(); if (tmp != null) { result.append(",command:").append(tmp); } tmp = getInTransform(); if (tmp != null) { result.append(",in:").append(tmp); } tmp = getOutTransform(); if (tmp != null) { result.append(",out:").append(tmp); } cachedBinding = result.toString(); }; /** * Return the Command Transformation binding parameter. * <p> * * If the Binding configuration has the <code>command:</code> parameter specified, then return that value. Otherwise * return <code>null</code>. * * @return the value of the <code>command:</code> Binding configuration parameter, or <code>null</code> if not * specified. */ protected String getCommandTransform() { return commandTransform; }; /** * Return the full Binding String. */ @Override public String toString() { return toBinding(); } /** * Validate the Item's data type against the <code>BindingConfig</code>. * <p> * * If this validation fails, then a <code>BindingConfigParseException</code> is thrown. By default, this method will * succeed if the Item is a <code>StringItem</code>, <code>NumberItem</code>, <code>SwitchItem</code> , * <code>DimmerItem</code>, <code>ContactItem</code>, <code>DataTimeItem</code>. * <p> * Subclasses can augment this behavior or replace it, so that Bindings can reconfigure their support for Item * Types. * * @param item * the Item that requiring validation. * @exception BindingConfigParseException * when the validation of the <code>BindingConfig</code> fails for the Item. */ public void validateItemType(Item item) throws BindingConfigParseException { Class<? extends Item> t = getItemType(); if (!((t == StringItem.class) || (t == SwitchItem.class) || (t == DimmerItem.class) || (t == NumberItem.class) || (t == ContactItem.class) || (t == DateTimeItem.class) || (t == RollershutterItem.class))) { throw new BindingConfigParseException(String.format( "Item %s is of type %s, but only String, Switch, Dimmer, Number, Contact, DataTime, and Rollershutter Items are allowed.", item.getName(), item.getClass().getSimpleName())); } } }