/**
* 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.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.mios.internal.MiosActivator;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.ColorItem;
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.model.item.binding.BindingConfigParseException;
import org.osgi.framework.BundleContext;
/**
* A BindingConfig object targeted at MiOS Device Variables and Attributes.
* <p>
*
* The device-specific form of a MiOS Binding is:<br>
* <ul>
* <li><nobr>
* <tt>mios="unit:<i>unitName</i>,device:<i>deviceId</i>/service/<i>serviceURN</i>/<i>serviceVariable</i>,<i>optionalTransformations</i></tt>
* </nobr>
* <li><nobr>
* <tt>mios="unit:<i>unitName</i>,device:<i>deviceId</i>/service/<i>serviceAlias</i>/<i>serviceVariable</i>,<i>optionalTransformations</i></tt>
* </nobr>
* <li><nobr> <tt>mios="unit:<i>unitName</i>,device:<i>deviceId</i>/<i>attrName</i>,<i>optionalTransformations</i></tt>
* </nobr>
* </ul>
* <p>
*
*
* Example Item declarations...<br>
* <ul>
* <li>
* <code><nobr>Number MiOSMemoryAvailable "Available [%.0f KB]" (BindingDemo) {mios="unit:house,device:382/service/urn:cd-jackson-com:serviceId:SystemMonitor/memoryAvailable"}</nobr></code>
* <li>
* <code><nobr>Number Weather_Temperature "Outside Temperature [%.1f F]" <temperature> (Weather_Chart) {mios="unit:house,device:318/service/urn:upnp-org:serviceId:TemperatureSensor1/CurrentTemperature"}</nobr></code>
* <li>
* <code><nobr>Switch FamilyTheatreLightsStatus "Family Theatre Lights" (GSwitch) {mios="unit:house,device:13/service/urn:upnp-org:serviceId:SwitchPower1/Status,command:ON|OFF,in:MAP(miosSwitchIn.map),out:MAP(miosSwitchOut.map)"}</nobr></code>
* <li>
* <code><nobr>Dimmer FamilyTheatreLightsLoadLevelStatus "Family Theatre Lights [%d] %" <slider> (GDimmer) {mios="unit:house,device:13/service/urn:upnp-org:serviceId:Dimming1/LoadLevelStatus,command:MAP(miosDimmerCommand.map)", autoupdate="false"}</nobr></code>
* </ul>
* <p>
*
* There are also a set of pre-defined UPnP Service aliases, so a short-hand notation can be used for common UPnP
* ServiceId's:<br>
* <ul>
* <li>
* <code><nobr>Number MiOSMemoryAvailable "Available [%.0f KB]" (BindingDemo) {mios="unit:house,device:382/service/SystemMonitor/memoryAvailable"}</nobr></code>
* <li>
* <code><nobr>Number Weather_Temperature "Outside Temperature [%.1f F]" <temperature> (Weather_Chart) {mios="unit:house,device:318/service/TemperatureSensor1/CurrentTemperature"}</nobr></code>
* <li>
* <code><nobr>Switch FamilyTheatreLightsStatus "Family Theatre Lights" (GSwitch) {mios="unit:house,device:13/service/SwitchPower1/Status,command:ON|OFF,in:MAP(miosSwitchIn.map),out:MAP(miosSwitchOut.map)"}</nobr></code>
* <li>
* <code><nobr>Dimmer FamilyTheatreLightsLoadLevelStatus "Family Theatre Lights [%d] %" <slider> (GDimmer) {mios="unit:house,device:13/service/Dimming1/LoadLevelStatus,command:MAP(miosDimmerCommand.map)", autoupdate="false"}</nobr></code>
* </ul>
* <p>
*
* The complete list of UPnP Service aliases is listed in the documentation file, or can be seen in the file
* <code>ServiceAliases.properties</code>.
* <p>
*
* In addition to binding against a Device's Variables, you can bind to the set of Device Attributes it exposes.
* Specifically its <code>id</code> and <code>status</code>:
* <p>
* <ul>
* <li>
* <code><nobr>Number FamilyTheatreLightsId "ID [%d]" {mios="unit:house,device:13/id"}</nobr></code>
* <li>
* <code><nobr>String FamilyTheatreLightsDeviceStatus "Device Status [%s]" {mios="unit:house,device:13/status,in:MAP(miosStatusIn.map)"}</nobr></code>
* </ul>
* <p>
*
* <b>Optional Transformations</b>
* <p>
* In many cases, the values exposed by a MiOS Unit aren't suitable for use within openHAB, and a value
* mapping/transformation may be needed.
* <p>
*
* Values may be transformed using an input transformation (<code>in:</code>) or a command transformation (
* <code>command:</code>). In future, values will also be transformed prior to being sent to a MiOS Unit via the output
* transformation (<code>out:</code>)
* <p>
*
* Transformation files are provided, for the common input and command transformations, in the
* <code>examples/transform</code> directory of the MiOS Binding.
* <p>
* The examples presented here assume that these MAP transformation files have been copied to the appropriate openHAB
* configuration location (typically <code>configuration/transform</code>).
* <p>
*
* There are example Input transformation maps:
* <ul>
* <li><code>miosSwitchIn.map</code> - MiOS Switch device ( <code>service/SwitchPower1/Status</code>) properties when
* mapped to a <code>Switch</code> Item.
* <li><code>miosStatusIn.map</code> - MiOS Status attributes ( <code>/status</code>) to make them readable states when
* mapped to a <code>String</code> Item.
* <li><code>miosContactIn.map</code> - MiOS Security Sensor device ( <code>/service/SecuritySensor1/Tripped</code>)
* when mapped to a <code>Contact</code> Item.
* <li><code>miosZWaveStatusIn.map</code> - MiOS System attribute ( <code>system:/ZWaveStatus</code>) when mapped to a
* <code>String</code> Item.
* <li><code>miosWeatherConditionGroupIn.map</code> - MiOS
* </ul>
*
* <p>
* Example Output transformation maps:
* <ul>
* <li><code>miosSwitchOut.map</code> - MiOS
* <li><code>miosContactOut.map</code> - MiOS
* </ul>
*
* <p>
* And example Command transformation maps:
* <ul>
* <li><code>miosArmedCommand.map</code> - MiOS Security Sensor device ( <code>/service/SecuritySensor1/Armed</code>)
* when mapped to a <code>Switch</code> Item.
* <li><code>miosDimmerCommand.map</code> - MiOS Dimmer device ( <code>/service/Dimming1/LoadLevelStatus</code>) when
* mapped to a <code>Dimmer</code> Item.
* <li><code>miosLockCommand.map</code> - MiOS Lock device ( <code>/service/DoorLock1/Status</code>) properties when
* mapped to a <code>Switch</code> Item.
* <li><code>miosTStatModeStatusCommand.map</code> - MiOS Thermostat device (
* <code>/service/HVAC_UserOperatingMode1/ModeStatus</code>) when mapped to a <code>String</code> Item.
* <li><code>miosTStatSetpointCoolCommand.map</code> - MiOS Thermostat device (
* <code>/service/TemperatureSetpoint1_Cool/CurrentSetpoint</code>) when mapped to a <code>Number</code> Item.
* <li><code>miosTStatSetpointHeatCommand.map</code> - MiOS Thermostat device (
* <code>/service/TemperatureSetpoint1_Heat/CurrentSetpoint</code>) when mapped to a <code>Number</code> Item.
* <li><code>miosTStatFanOperatingModeCommand.map</code> - MiOS Thermostat device (
* <code>/service/HVAC_FanOperatingMode1/Mode</code>) when mapped to a <code>String</code> Item.
* <li><code>miosUPnPRenderingControlVolumeCommand.map</code> - MiOS UPnP Volume (
* <code>service/RenderingControl/Volume</code>) property when mapped to a <code>Dimmer</code> Item.
* <li><code>miosUPnPRenderingControlMuteCommand.map</code> - MiOS UPnP Mute (
* <code>service/RenderingControl/Mute</code>) property when mapped to a <code>Switch</code> Item.
* <li><code>miosUPnPTransportStatePlayModeCommand.map</code> - MiOS UPnP PlayMode (
* <code>service/AVTransport/TransportState</code>) when mapped to a <code>String</code> Item.
* </ul>
* <p>
*
* More details on these mappings can be found in the <code>README.md<code> file associated
* with this Binding.
* <p>
*
* @author Mark Clark
* @since 1.6.0
*/
public class DeviceBindingConfig extends MiosBindingConfig {
private static final String DEFAULT_COMMAND_TRANSFORM = "_defaultCommand";
private static Map<String, String> COMMAND_DEFAULTS = new HashMap<String, String>();
static {
COMMAND_DEFAULTS.put("ON", "urn:upnp-org:serviceId:SwitchPower1/SetTarget(newTargetValue=1)");
COMMAND_DEFAULTS.put("OFF", "urn:upnp-org:serviceId:SwitchPower1/SetTarget(newTargetValue=0)");
COMMAND_DEFAULTS.put("TOGGLE", "urn:micasaverde-com:serviceId:HaDevice1/ToggleState()");
COMMAND_DEFAULTS.put("INCREASE", "urn:upnp-org:serviceId:Dimming1/StepUp()");
COMMAND_DEFAULTS.put("DECREASE", "urn:upnp-org:serviceId:Dimming1/StepDown()");
COMMAND_DEFAULTS.put("UP", "urn:upnp-org:serviceId:WindowCovering1/Up()");
COMMAND_DEFAULTS.put("DOWN", "urn:upnp-org:serviceId:WindowCovering1/Down()");
COMMAND_DEFAULTS.put("STOP", "urn:upnp-org:serviceId:WindowCovering1/Stop()");
}
private static Properties aliasMap = new Properties();
private static String SERVICE_ALIASES = "org/openhab/binding/mios/internal/config/ServiceAliases.properties";
private static HashMap<String, ParameterDefaults> paramDefaults = new HashMap<String, ParameterDefaults>();
private static String PARAM_DEFAULTS = "org/openhab/binding/mios/internal/config/DeviceDefaults.properties";
static {
ClassLoader cl = DeviceBindingConfig.class.getClassLoader();
InputStream input;
input = cl.getResourceAsStream(SERVICE_ALIASES);
try {
aliasMap.load(input);
logger.debug("Successfully loaded UPnP Service Aliases from '{}', entries '{}'", SERVICE_ALIASES,
aliasMap.size());
} catch (Exception e) {
// Pre-shipped with the Binding, so it should never error out.
logger.error("Failed to load Service Alias file '{}', Exception", SERVICE_ALIASES, e);
}
input = cl.getResourceAsStream(PARAM_DEFAULTS);
try {
Properties tmp = new Properties();
tmp.load(input);
for (Entry<Object, Object> e : tmp.entrySet()) {
paramDefaults.put((String) e.getKey(), ParameterDefaults.parse((String) e.getValue()));
}
logger.debug("Successfully loaded Device Parameter defaults from '{}', entries '{}'", PARAM_DEFAULTS,
paramDefaults.size());
} catch (Exception e) {
// Pre-shipped with the Binding, so it should never error out.
logger.error("Failed to load Device Parameter defaults file '{}', Exception", PARAM_DEFAULTS, e);
}
}
private static final Pattern SERVICE_IN_PATTERN = Pattern
.compile("service/(?<serviceName>[^/]+)(/(?<serviceVar>[^/]+))?");
private static final Pattern SERVICE_COMMAND_TRANSFORM_PATTERN = Pattern
.compile("(?<transform>(?<transformCommand>[a-zA-Z]+)\\((?<transformParam>.*)\\))");
private static final Pattern SERVICE_COMMAND_INMAP_PATTERN = Pattern
.compile("(?<mapName>.+?)(=(?<serviceName>.+)/(?<serviceAction>.+))?");
private String commandTransformName;
private String commandTransformParam;
private Map<String, String> commandMap;
private TransformationService commandTransformationService;
private DeviceBindingConfig(String context, String itemName, String unitName, int id, String stuff,
Class<? extends Item> itemType, String commandTransform, String inTransform, String outTransform)
throws BindingConfigParseException {
super(context, itemName, unitName, id, stuff, itemType, commandTransform, inTransform, outTransform);
}
/**
* Map Aliased Service names into their "formal" UPnP-style format.
*
* @param source
* the original Service name to be mapped
* @return the mapped UPnP-style Service name, if an alias mapping exists, or the original source value.
*/
public static final String mapServiceAlias(String source) {
String mapped = (String) aliasMap.get(source);
return (mapped == null) ? source : mapped;
}
/**
* Static constructor-method.
*
* @return an initialized MiOS Device Binding Configuration object.
*/
public static final MiosBindingConfig create(String context, String itemName, String unitName, int id,
String inStuff, Class<? extends Item> itemType, String commandTransform, String inTransform,
String outTransform) throws BindingConfigParseException {
try {
// Before we initialize, normalize the serviceId string used in any
// outgoing stuff.
String newInStuff = inStuff;
Matcher matcher;
// Extract and Map the inbound names.
String iName = null;
String iVar = null;
// Not a full match, will only modify things that start with
// "/service/"
matcher = SERVICE_IN_PATTERN.matcher(newInStuff);
if (matcher.matches()) {
iName = matcher.group("serviceName");
iVar = matcher.group("serviceVar");
// Handle service name aliases.
iName = mapServiceAlias(iName);
// Rebuild, since we've normalized the name.
newInStuff = "service/" + iName + (iVar == null ? "" : '/' + iVar);
}
//
// Apply any "Default" values to the in:, out:, and command:
// transformations prior
// to converting them for internal usage.
//
ParameterDefaults pd = paramDefaults.get(newInStuff);
if (pd != null) {
logger.trace("Device ParameterDefaults FOUND '{}' for '{}', '{}'", itemName, newInStuff, pd);
if (commandTransform == null) {
commandTransform = pd.getCommandTransform();
logger.trace("Device ParameterDefaults '{}' defaulted command: to '{}'", itemName,
commandTransform);
}
if (inTransform == null) {
inTransform = pd.getInTransform();
logger.trace("Device ParameterDefaults '{}' defaulted in: to '{}'", itemName, inTransform);
}
if (outTransform == null) {
outTransform = pd.getOutTransform();
logger.trace("Device ParameterDefaults '{}' defaulted out: to '{}'", itemName, outTransform);
}
} else {
logger.trace("Device ParameterDefaults NOT FOUND '{}' for '{}'", itemName, newInStuff);
}
String cTransform = null;
String cParam = null;
Map<String, String> cMap = null;
String newCommandTransform = commandTransform;
if ("".equals(newCommandTransform)) {
// If it's present, but blank, use the global defaults.
cMap = COMMAND_DEFAULTS;
} else if (newCommandTransform != null) {
// Try for a match as a TransformationService.
matcher = SERVICE_COMMAND_TRANSFORM_PATTERN.matcher(newCommandTransform);
if (matcher.matches()) {
cTransform = matcher.group("transformCommand");
cParam = matcher.group("transformParam");
} else { // Try as an inline static command mapping.
String[] commandList = newCommandTransform.split("\\|");
String command;
Map<String, String> l = new HashMap<String, String>(commandList.length);
String mapName;
String serviceStr;
String serviceName;
String serviceAction;
for (int i = 0; i < commandList.length; i++) {
command = commandList[i];
matcher = SERVICE_COMMAND_INMAP_PATTERN.matcher(command);
if (matcher.matches()) {
mapName = matcher.group("mapName");
serviceName = matcher.group("serviceName");
serviceAction = matcher.group("serviceAction");
if (serviceName != null) {
// Handle any Service Aliases that might have been used in the inline Map.
serviceName = mapServiceAlias(serviceName);
serviceStr = serviceName + '/' + serviceAction;
} else {
serviceStr = null;
}
String oldMapName = l.put(mapName, serviceStr);
if (oldMapName != null) {
throw new BindingConfigParseException(
String.format("Duplicate inline Map entry '%s' in command: '%s'", oldMapName,
newCommandTransform));
}
} else {
throw new BindingConfigParseException(
String.format("Invalid command, parameter format '%s' in command: '%s'", command,
newCommandTransform));
}
}
cMap = l;
}
}
logger.trace("newInStuff '{}', iName '{}', iVar '{}'", new Object[] { newInStuff, iName, iVar });
DeviceBindingConfig c = new DeviceBindingConfig(context, itemName, unitName, id, newInStuff, itemType,
newCommandTransform, inTransform, outTransform);
c.initialize();
c.commandTransformName = cTransform;
c.commandTransformParam = cParam;
c.commandMap = cMap;
return c;
} catch (Exception e) {
logger.debug(e.toString());
throw new BindingConfigParseException(e.getMessage());
}
}
/**
* Returns the value "<code>device</code>".
*
* @return the value "<code>device</code>"
*/
@Override
public String getMiosType() {
return "device";
}
private String getCommandTransformName() {
return commandTransformName;
}
private String getCommandTransformParam() {
return commandTransformParam;
}
private Map<String, String> getCommandMap() {
return commandMap;
}
private TransformationService getCommandTransformationService() {
String name = getCommandTransformName();
if (name == null || name.equals("")) {
return null;
}
if (commandTransformationService != null) {
return commandTransformationService;
}
BundleContext context = MiosActivator.getContext();
commandTransformationService = TransformationHelper.getTransformationService(context, name);
if (commandTransformationService == null) {
logger.warn("Transformation Service (command) '{}' not found for declaration '{}'.", name,
getCommandTransform());
}
return commandTransformationService;
}
@Override
public String transformCommand(Command command) throws TransformationException {
// Quickly return null if we don't support commands.
if (getCommandTransform() == null) {
return null;
}
TransformationService ts = getCommandTransformationService();
String result;
String key = command.toString();
if (ts != null) {
result = ts.transform(getCommandTransformParam(), key);
// If we don't have a transform, look for a special one called
// "_default".
if (result == null || "".equals(result) || key.equals(result)) {
result = ts.transform(getCommandTransformParam(), DEFAULT_COMMAND_TRANSFORM);
}
} else {
Map<String, String> map = getCommandMap();
if (map != null) {
String value = map.get(key);
if (value != null) {
result = value;
} else { // Attempt to provide a default Mapping for it.
if (map.containsKey(key)) {
result = COMMAND_DEFAULTS.get(key);
} else {
result = key;
}
}
} else {
result = COMMAND_DEFAULTS.get(key);
}
}
return result;
}
@Override
public void validateItemType(Item item) throws BindingConfigParseException {
Class<? extends Item> t = getItemType();
// Add support for Color Device Items.
if (!(t == ColorItem.class)) {
super.validateItemType(item);
}
}
}