/**
* 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;
import java.util.Calendar;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.openhab.binding.mios.MiosActionProvider;
import org.openhab.binding.mios.MiosBindingProvider;
import org.openhab.binding.mios.internal.config.DeviceBindingConfig;
import org.openhab.binding.mios.internal.config.MiosBindingConfig;
import org.openhab.binding.mios.internal.config.SceneBindingConfig;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
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;
/**
* The MiOS Binding is responsible for coordinating changes to openHAB Items from the corresponding/bound information
* from each configured MiOS Unit.
*
* The Binding allows information from a MiOS Unit to be bound to openHAB Items, as well a allowing openHAB Commands to
* be propagated back to the MiOS Unit under control.
*
* The following types of information from a MiOS Unit can be bound to openHAB Items:
* <p>
*
* <ul>
* <li>Device Attributes & State Variables
* <li>Scene Attributes
* <li>System Attributes
* </ul>
* <p>
*
* Similarly, through a configurable set of openHAB Transformations, any Commands sent to these Items can be proxied
* back to the corresponding MiOS Unit.
* <p>
*
* Data flowing between the MiOS Unit and openHAB can be transformed as it flows between the two systems. This
* transformation is configurable, and is expressed in the Item Binding using standard openHAB
* {@code TransformationService} expressions.
* <p>
*
* Example MAP-based Transformation files are provided for commonly required transformations. <br>
* eg. For Switch Data flowing into openHAB {@code MAP(miosSwitchIn.map)}, and for Switch Commands flowing in to MiOS
* {@code MAP(miosSwitchOut.map)}
* <p>
*
* The Binding follows the general interaction principals outlined in the MiOS
* {@link <a href="http://wiki.micasaverde.com/index.php/UI_Simple">UI Simple</a>} documentation.
* <p>
*
* In effect, the binding behaves like a "remote control" to one or more configured MiOS Units, utilizing a HTTP-based
* Long-poll to receive updates occurring within each Unit, and transforming them into corresponding updates to the
* openHAB Items that have been bound.
* <p>
*
* All updates are received asynchronously from the MiOS Units. This interaction is managed by a per MiOS Unit
* {@link MiosUnitConnector} Polling Thread object that utilizes a {@link MiosUnit MiOS Unit} configuration object to
* determine the location of the MiOS Unit.
* <p>
*
* @author Mark Clark
* @since 1.6.0
*/
public class MiosBinding extends AbstractBinding<MiosBindingProvider>implements ManagedService, MiosActionProvider {
private static final Logger logger = LoggerFactory.getLogger(MiosBinding.class);
private Map<String, MiosUnitConnector> connectors = new HashMap<String, MiosUnitConnector>();
private Map<String, MiosUnit> nameUnitMapper = null;
private String getName() {
return "MiosBinding";
}
/**
* Invoked by OSGi Framework, once per instance, during the Binding activation process.
*
* OSGi is configured to do this in OSGI-INF/activebinding.xml
*/
@Override
public void activate() {
logger.debug(getName() + " activate()");
}
/**
* Invoked by the OSGi Framework, once per instance, during the Binding deactivation process.
*
* Internally this is used to close out any resources used by the MiOS Binding.
*
* OSGi is configured to do this in OSGI-INF/activebinding.xml
*/
@Override
public void deactivate() {
logger.debug(getName() + " deactivate()");
// close any open connections
for (MiosUnitConnector connector : connectors.values()) {
if (connector.isConnected()) {
connector.close();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void bindingChanged(BindingProvider provider, String itemName) {
logger.debug("bindingChanged: start provider '{}', itemName '{}'", provider, itemName);
if (provider instanceof MiosBindingProvider) {
registerItemWatch((MiosBindingProvider) provider, itemName);
}
}
/**
* {@inheritDoc}
*/
@Override
public void allBindingsChanged(BindingProvider provider) {
logger.debug("allBindingsChanged: start provider '{}'", provider);
registerProviderWatch(provider);
}
private void registerProviderWatch(BindingProvider provider) {
logger.debug("registerProviderWatch: start miosProvider '{}'", provider);
if (provider instanceof MiosBindingProvider) {
MiosBindingProvider miosProvider = (MiosBindingProvider) provider;
for (String itemName : provider.getItemNames()) {
registerItemWatch(miosProvider, itemName);
}
}
}
private void registerItemWatch(MiosBindingProvider miosProvider, String itemName) {
logger.debug("registerItemWatch: start miosProvider '{}', itemName '{}'", miosProvider, itemName);
MiosUnitConnector connector = getMiosConnector(miosProvider.getMiosUnitName(itemName));
if (connector != null) {
connector.restart();
} else {
logger.debug("registerItemWatch: no connector miosProvider '{}', itemName='{}'", miosProvider, itemName);
}
}
private String getMiosUnitName(String itemName) {
logger.trace("getMiosUnitName: start itemName '{}'", itemName);
for (BindingProvider provider : providers) {
if (provider instanceof MiosBindingProvider) {
if (provider.getItemNames().contains(itemName)) {
return ((MiosBindingProvider) provider).getMiosUnitName(itemName);
}
}
}
return null;
}
private MiosUnitConnector getMiosConnector(String unitName) {
logger.trace("getMiosConnector: start unitName '{}'", unitName);
// sanity check
if (unitName == null) {
return null;
}
// check if the connector for this unit already exists
MiosUnitConnector connector = connectors.get(unitName);
if (connector != null) {
return connector;
}
MiosUnit miosUnit;
// NOTE: We deviate from the XBMC Binding, in that we only accept
// "names" presented in the openHAB configuration files.
// check if we have been initialized yet - can't process
// named units until we have read the binding config.
if (nameUnitMapper == null) {
logger.trace("Attempting to access the named MiOS Unit '{}' before the binding config has been loaded",
unitName);
return null;
}
miosUnit = nameUnitMapper.get(unitName);
// Check this Unit name exists in our config
if (miosUnit == null) {
logger.error("Named MiOS Unit '{}' does not exist in the binding config", unitName);
return null;
}
// create a new connection handler
logger.debug("Creating new MiosConnector for '{}' on {}", unitName, miosUnit.getHostname());
connector = new MiosUnitConnector(miosUnit, this);
connectors.put(unitName, connector);
// attempt to open the connection straight away
try {
connector.open();
} catch (Exception e) {
logger.error("Connection failed for '{}' on {}", unitName, miosUnit.getHostname());
}
return connector;
}
/**
* {@inheritDoc}
*/
@Override
protected void internalReceiveCommand(String itemName, Command command) {
try {
logger.debug("internalReceiveCommand: itemName '{}', command '{}'", itemName, command);
// Lookup the MiOS Unit name and property for this item
String unitName = getMiosUnitName(itemName);
MiosUnitConnector connector = getMiosConnector(unitName);
if (connector == null) {
logger.warn("Received command ({}) for item '{}' but no connector found for MiOS Unit '{}', ignoring",
new Object[] { command.toString(), itemName, unitName });
return;
}
if (!connector.isConnected()) {
logger.warn(
"Received command ({}) for item '{}' but the connection to the MiOS Unit '{}' is down, ignoring",
new Object[] { command.toString(), itemName, unitName });
return;
}
for (BindingProvider provider : providers) {
if (provider instanceof MiosBindingProvider) {
MiosBindingProviderImpl miosProvider = (MiosBindingProviderImpl) provider;
MiosBindingConfig config = miosProvider.getMiosBindingConfig(itemName);
if (config != null) {
ItemRegistry reg = miosProvider.getItemRegistry();
if (reg != null) {
Item item = reg.getItem(config.getItemName());
State state = item.getState();
connector.invokeCommand(config, command, state);
} else {
logger.warn("internalReceiveCommand: Missing ItemRegistry for item '{}' command '{}'",
itemName, command);
}
} else {
logger.trace("internalReceiveCommand: Missing BindingConfig for item '{}' command '{}'",
itemName, command);
}
}
}
} catch (Exception e) {
logger.error("Error handling command", e);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void internalReceiveUpdate(String itemName, State newState) {
logger.trace("internalReceiveUpdate: itemName '{}', newState '{}', class '{}'",
new Object[] { itemName, newState, newState.getClass() });
// No need to implement this for MiOS Bridge Binding since anything
// that needs to be sent back to the MiOS System will be done via a
// Command.
//
// We leave this here to aid debugging. We get more
// information out of the above logger call than we get from openHAB.
}
protected void addBindingProvider(MiosBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(MiosBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/**
* {@inheritDoc}
*/
@Override
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
logger.trace(getName() + " updated()");
// Under openHAB 2.0, we get called shortly after activate(), but mios.cfg
// hasn't yet been loaded, so we're passed a null properties object.
// We're called again later, so it'll get established correctly at that point.
if (properties == null) {
return;
}
Map<String, MiosUnit> units = new HashMap<String, MiosUnit>();
Enumeration<String> keys = properties.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
if ("service.pid".equals(key)) {
continue;
}
// Only apply this pattern once, as the leading-edge may not be the
// name of a unit. We support two forms:
//
// mios:lounge.host=...
// mios:host=
//
// The latter form refers to the "default" Unit, which can be used
// in bindings to make things simpler for single-unit owners.
//
String unitName = null;
String value = ((String) properties.get(key)).trim();
String[] parts = key.split("\\.", 2);
if (parts.length != 1) {
unitName = parts[0];
key = parts[1];
}
boolean created = false;
String hackUnitName = (unitName == null) ? MiosUnit.CONFIG_DEFAULT_UNIT : unitName;
MiosUnit unit = units.get(hackUnitName);
if (unit == null) {
unit = new MiosUnit(hackUnitName);
created = true;
}
if ("host".equals(key)) {
unit.setHostname(value);
} else if ("port".equals(key)) {
unit.setPort(Integer.valueOf(value));
} else if ("timeout".equals(key)) {
unit.setTimeout(Integer.valueOf(value));
} else if ("minimumDelay".equals(key)) {
unit.setMinimumDelay(Integer.valueOf(value));
} else if ("refreshCount".equals(key)) {
unit.setRefreshCount(Integer.valueOf(value));
} else if ("errorCount".equals(key)) {
unit.setErrorCount(Integer.valueOf(value));
} else {
logger.warn("Unexpected configuration parameter {}", key);
created = false;
}
// Only bother to put it back if we created a new one, otherwise
// it's already there!
if (created) {
logger.debug("updated: Created Unit '{}'", hackUnitName);
units.put(hackUnitName, unit);
}
}
// Close out pre-existing connections
if (nameUnitMapper != null) {
MiosUnitConnector connector;
for (String unitName: nameUnitMapper.keySet()) {
connector = connectors.get(unitName);
if (connector != null) {
try {
connector.close();
} catch (Exception e) {
// Suppress
}
}
}
}
nameUnitMapper = units;
// Reregister, since the connections will change.
for (BindingProvider provider : providers) {
logger.debug("updated: provider '{}'", provider.getClass());
registerProviderWatch(provider);
}
}
/**
* Push a value into all openHAB Items that match a given MiOS Property name (from the Item Binding declaration).
* <p>
* In the process, this routine will perform Datatype conversions from Java types to openHAB's type system. These
* conversions are as follows:
* <p>
* <ul>
* <li>{@code String} -> {@code StringType}
* <li>{@code Integer} -> {@code DecimalType}
* <li>{@code Double} -> {@code DecimalType}
* <li>{@code Boolean} -> {@code StringType} (true == ON, false == OFF)
* <li>{@code Calendar} -> {@code DateTimeType}
* </ul>
*
* @param property
* the MiOS Property name
* @param value
* the value to push, per the supported types.
* @exception IllegalArgumentException
* thrown if the value isn't one of the supported types.
*/
public void postPropertyUpdate(String property, Object value, boolean incremental) throws Exception {
if (value instanceof String) {
internalPropertyUpdate(property, new StringType(value == null ? "" : (String) value), incremental);
} else if (value instanceof Integer) {
internalPropertyUpdate(property, new DecimalType((Integer) value), incremental);
} else if (value instanceof Calendar) {
internalPropertyUpdate(property, new DateTimeType((Calendar) value), incremental);
} else if (value instanceof Double) {
internalPropertyUpdate(property, new DecimalType((Double) value), incremental);
} else if (value instanceof Boolean) {
postPropertyUpdate(property,
((Boolean) value).booleanValue() ? OnOffType.ON.toString() : OnOffType.OFF.toString(), incremental);
} else {
throw new IllegalArgumentException(String.format("Unexpected Datatype, property=%s datatype=%s", property,
value.getClass().toString()));
}
}
private void internalPropertyUpdate(String property, State value, boolean incremental) throws Exception {
int bound = 0;
if (value == null) {
logger.trace("internalPropertyUpdate: Value is null for Property '{}', ignored.", property);
return;
}
for (BindingProvider provider : providers) {
if (provider instanceof MiosBindingProvider) {
MiosBindingProviderImpl miosProvider = (MiosBindingProviderImpl) provider;
for (String itemName : miosProvider.getItemNamesForProperty(property)) {
MiosBindingConfig config = miosProvider.getMiosBindingConfig(itemName);
if (config != null) {
// Transform whatever value we have, based upon the
// Transformation Service specified in the Binding
// Config.
State newValue = config.transformIn(value);
if (newValue != value) {
logger.trace("internalPropertyUpdate: transformation performed, from '{}' to '{}'", value,
newValue);
}
//
// Set the value only if:
// * we're running Incrementally OR;
// * the CURRENT value is UNDEFINED OR;
// * the CURRENT value is different from the NEW value
//
// This is to handle a case where the MiOS Unit
// "restarts" and floods us with a bunch of the same
// data. In this case, we don't want to flood the Items,
// since it may re-trigger a bunch of Rules in an
// unnecessary manner.
//
if (incremental) {
logger.debug("internalPropertyUpdate: BOUND (Incr) Updating '{} {mios=\"{}\"}' to '{}'",
itemName, property, newValue);
eventPublisher.postUpdate(itemName, newValue);
} else {
ItemRegistry reg = miosProvider.getItemRegistry();
State oldValue = reg.getItem(itemName).getState();
if ((oldValue == null && newValue != null)
|| (UnDefType.UNDEF.equals(oldValue) && !UnDefType.UNDEF.equals(newValue))
|| !oldValue.equals(newValue)) {
logger.debug(
"internalPropertyUpdate: BOUND (Full) Updating '{} {mios=\"{}\"}' to '{}', was '{}'",
new Object[] { itemName, property, newValue, oldValue });
eventPublisher.postUpdate(itemName, newValue);
} else {
logger.trace(
"internalPropertyUpdate: BOUND (Full) Ignoring '{} {mios=\"{}\"}' to '{}', was '{}'",
new Object[] { itemName, property, newValue, oldValue });
}
}
bound++;
} else {
logger.trace("internalPropertyUpdate: Found null BindingConfig for item '{}' property '{}'",
itemName, property);
}
}
}
}
if (bound == 0) {
logger.trace("internalPropertyUpdate: NOT BOUND {mios=\"{}\"}, value={}", property, value);
} else {
logger.trace("internalPropertyUpdate: BOUND {mios=\"{}\"}, value={}, bound {} time(s)",
new Object[] { property, value, bound });
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean invokeMiosScene(String itemName) {
try {
logger.debug("invokeMiosScene item {}", itemName);
boolean sent = false;
// Lookup the MiOS Unit name and property for this item
String unitName = getMiosUnitName(itemName);
MiosUnitConnector connector = getMiosConnector(unitName);
if (connector == null) {
logger.warn(
"invokeMiosScene: Scene call for item '{}' but no connector found for MiOS Unit '{}', ignoring",
itemName, unitName);
return false;
}
if (!connector.isConnected()) {
logger.warn(
"invokeMiosScene: Scene call for item '{}' but the connection to the MiOS Unit '{}' is down, ignoring",
itemName, unitName);
return false;
}
for (BindingProvider provider : providers) {
if (provider instanceof MiosBindingProvider) {
MiosBindingProviderImpl miosProvider = (MiosBindingProviderImpl) provider;
MiosBindingConfig config = miosProvider.getMiosBindingConfig(itemName);
if ((config != null) && (config instanceof SceneBindingConfig)) {
connector.invokeScene((SceneBindingConfig) config);
sent = true;
} else {
logger.error(
"invokeMiosScene: Missing BindingConfig for item '{}', or not bound to a MiOS Scene.",
itemName);
}
}
}
return sent;
} catch (Exception e) {
logger.error("invokeMiosScene: Error handling command", e);
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean invokeMiosAction(String itemName, String actionName, List<Entry<String, Object>> params) {
try {
logger.debug("invokeMiosAction item {}, action {}, params {}",
new Object[] { itemName, actionName, Integer.valueOf((params == null) ? 0 : params.size()) });
boolean sent = false;
// Lookup the MiOS Unit name and property for this item
String unitName = getMiosUnitName(itemName);
MiosUnitConnector connector = getMiosConnector(unitName);
if (connector == null) {
logger.warn(
"invokeMiosAction: Action call for item '{}' but no connector found for MiOS Unit '{}', ignoring",
itemName, unitName);
return false;
}
if (!connector.isConnected()) {
logger.warn(
"invokeMiosAction: Action call for item '{}' but the connection to the MiOS Unit '{}' is down, ignoring",
itemName, unitName);
return false;
}
for (BindingProvider provider : providers) {
if (provider instanceof MiosBindingProvider) {
MiosBindingProviderImpl miosProvider = (MiosBindingProviderImpl) provider;
MiosBindingConfig config = miosProvider.getMiosBindingConfig(itemName);
if ((config != null) && (config instanceof DeviceBindingConfig)) {
connector.invokeAction((DeviceBindingConfig) config, actionName, params);
sent = true;
} else {
logger.error(
"invokeMiosAction: Missing BindingConfig for item '{}', or not bound to a MiOS Device.",
itemName);
}
}
}
return sent;
} catch (Exception e) {
logger.error("invokeMiosScene: Error handling command", e);
return false;
}
}
}