/**
* 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.pioneeravr.internal;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.pioneeravr.PioneerAvrBindingProvider;
import org.openhab.binding.pioneeravr.internal.ipcontrolprotocol.IpControl;
import org.openhab.binding.pioneeravr.internal.ipcontrolprotocol.IpControlCommand;
import org.openhab.binding.pioneeravr.internal.ipcontrolprotocol.IpControlCommandRef;
import org.openhab.binding.pioneeravr.internal.ipcontrolprotocol.IpControlDisplayInformation;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.binding.BindingChangeListener;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.items.Item;
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.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
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;
/**
* Binding listening to openHAB event bus and send commands to pioneerav devices when certain
* commands are received.
*
* @author Rainer Ostendorf
* @author based on the Onkyo binding by Pauli Anttila and others
* @since 1.4.0
*/
public class PioneerAvrBinding extends AbstractBinding<PioneerAvrBindingProvider>
implements ManagedService, BindingChangeListener, PioneerAvrEventListener {
private static final Logger logger = LoggerFactory.getLogger(PioneerAvrBinding.class);
protected static final String ADVANCED_COMMAND_KEY = "#";
protected static final String WILDCARD_COMMAND_KEY = "*";
/** RegEx to validate a config <code>'^(.*?)\\.(host|port|checkconn)$'</code> */
private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(host|port|checkconn)$");
/** pioneerav receiver default tcp port */
private final static int DEFAULT_PORT = IpControl.DEFAULT_IPCONTROL_PORT;
/** Map table to store all available receivers configured by the user */
protected Map<String, DeviceConfig> deviceConfigCache = null;
public PioneerAvrBinding() {
}
@Override
public void activate() {
logger.debug("Activate");
}
@Override
public void deactivate() {
logger.debug("Deactivate");
closeAllConnections();
}
/**
* {@inheritDoc}
*/
@Override
public void bindingChanged(BindingProvider provider, String itemName) {
logger.debug("bindingChanged {}", itemName);
initializeItem(itemName);
}
/**
* @{inheritDoc
*/
@Override
protected void internalReceiveCommand(String itemName, Command command) {
if (itemName != null) {
PioneerAvrBindingProvider provider = findFirstMatchingBindingProvider(itemName, command.toString());
if (provider == null) {
logger.warn("Doesn't find matching binding provider [itemName={}, command={}]", itemName, command);
return;
}
logger.debug("Received command (item='{}', state='{}', class='{}')",
new Object[] { itemName, command.toString(), command.getClass().toString() });
String tmp = provider.getDeviceCommand(itemName, command.toString());
if (tmp == null) {
tmp = provider.getDeviceCommand(itemName, WILDCARD_COMMAND_KEY);
}
String[] commandParts = tmp.split(":");
String deviceId = commandParts[0];
String deviceCmd = commandParts[1];
DeviceConfig device = deviceConfigCache.get(deviceId);
PioneerAvrConnection remoteController = device.getConnection();
if (device != null && remoteController != null) {
if (deviceCmd.startsWith(ADVANCED_COMMAND_KEY)) {
// advanced command
deviceCmd = deviceCmd.replace(ADVANCED_COMMAND_KEY, "");
if (deviceCmd.contains("%")) {
deviceCmd = convertOpenHabCommandToDeviceCommand(command, deviceCmd);
}
} else {
// normal command
IpControlCommand cmd = IpControlCommand.valueOf(deviceCmd);
deviceCmd = cmd.getCommand();
if (deviceCmd.contains("%")) {
deviceCmd = convertOpenHabCommandToDeviceCommand(command, deviceCmd);
}
}
if (deviceCmd != null) {
remoteController.send(deviceCmd);
} else {
logger.warn("Cannot convert value '{}' to IpControl format", command);
}
} else {
logger.warn("Cannot find connection details for device id '{}'", deviceId);
}
}
}
/**
* Convert OpenHAB commmand to pioneer avr receiver command.
*
* @param command the openhab command to send
* @param cmdTemplate the format string to used for converting the command
*
* @return the receiver command string
*/
private String convertOpenHabCommandToDeviceCommand(Command command, String cmdTemplate) {
String deviceCmd = null;
if (command instanceof OnOffType) {
deviceCmd = String.format(cmdTemplate, command == OnOffType.ON ? 1 : 0);
} else if (command instanceof StringType) {
deviceCmd = String.format(cmdTemplate, command);
} else if (command instanceof PercentType) {
// when settings the volume as percent, we need to map it
if (cmdTemplate.equals(IpControlCommand.VOLUME_SET.getCommand())) {
Integer percentValue = convertPercentToVolume(((DecimalType) command).intValue());
deviceCmd = String.format(cmdTemplate, percentValue);
} else {
deviceCmd = String.format(cmdTemplate, ((DecimalType) command).intValue());
}
} else if (command instanceof DecimalType) {
deviceCmd = String.format(cmdTemplate, ((DecimalType) command).intValue());
}
return deviceCmd;
}
/**
* Find the first matching {@link PioneerAvrBindingProvider} according to
* <code>itemName</code>.
*
* @param itemName
*
* @return the matching binding provider or <code>null</code> if no binding
* provider could be found
*/
private PioneerAvrBindingProvider findFirstMatchingBindingProvider(String itemName, String command) {
PioneerAvrBindingProvider firstMatchingProvider = null;
for (PioneerAvrBindingProvider provider : this.providers) {
String tmp = provider.getDeviceCommand(itemName, command.toString());
if (tmp != null) {
firstMatchingProvider = provider;
break;
}
}
if (firstMatchingProvider == null) {
for (PioneerAvrBindingProvider provider : this.providers) {
String tmp = provider.getDeviceCommand(itemName, WILDCARD_COMMAND_KEY);
if (tmp != null) {
firstMatchingProvider = provider;
break;
}
}
}
return firstMatchingProvider;
}
/**
* @{inheritDoc
*/
@Override
public void updated(Dictionary<String, ?> config) throws ConfigurationException {
logger.debug("Configuration updated, config {}", config != null ? true : false);
if (config != null) {
Enumeration<String> keys = config.keys();
if (deviceConfigCache == null) {
deviceConfigCache = new HashMap<String, DeviceConfig>();
}
while (keys.hasMoreElements()) {
String key = keys.nextElement();
// the config-key enumeration contains additional keys that we
// don't want to process here ...
if ("service.pid".equals(key)) {
continue;
}
Matcher matcher = EXTRACT_CONFIG_PATTERN.matcher(key);
if (!matcher.matches()) {
logger.debug("given config key '" + key
+ "' does not follow the expected pattern '<id>.<host|port|checkconn>'");
continue;
}
matcher.reset();
matcher.find();
String deviceId = matcher.group(1);
DeviceConfig deviceConfig = deviceConfigCache.get(deviceId);
if (deviceConfig == null) {
deviceConfig = new DeviceConfig(deviceId);
deviceConfigCache.put(deviceId, deviceConfig);
}
String configKey = matcher.group(2);
String value = (String) config.get(key);
if ("host".equals(configKey)) {
deviceConfig.host = value;
} else if ("port".equals(configKey)) {
deviceConfig.port = Integer.valueOf(value);
} else if ("checkconn".equals(configKey)) {
if (value.equals("0")) {
deviceConfig.connectionCheckActive = false;
} else {
deviceConfig.connectionCheckActive = true;
}
} else {
throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown");
}
}
// open connection to all receivers
for (String device : deviceConfigCache.keySet()) {
PioneerAvrConnection connection = deviceConfigCache.get(device).getConnection();
if (connection != null) {
connection.openConnection();
connection.addEventListener(this);
}
}
for (PioneerAvrBindingProvider provider : this.providers) {
for (String itemName : provider.getItemNames()) {
initializeItem(itemName);
}
}
}
}
private void closeAllConnections() {
if (deviceConfigCache != null) {
for (String device : deviceConfigCache.keySet()) {
PioneerAvrConnection connection = deviceConfigCache.get(device).getConnection();
if (connection != null) {
connection.closeConnection();
connection.removeEventListener(this);
}
}
deviceConfigCache = null;
}
}
/**
* Find receiver from device caache by ip address.
*
* @param ip
* @return
*/
private DeviceConfig findDevice(String ip) {
for (String device : deviceConfigCache.keySet()) {
DeviceConfig deviceConfig = deviceConfigCache.get(device);
if (deviceConfig != null) {
if (deviceConfig.getHost().equals(ip)) {
return deviceConfig;
}
}
}
return null;
}
@Override
public void statusUpdateReceived(EventObject event, String ip, String data) {
// find correct device from device cache
DeviceConfig deviceConfig = findDevice(ip);
if (deviceConfig != null) {
logger.debug("Received status update '{}' from device {}", data, deviceConfig.host);
for (PioneerAvrBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
// Update all items which refer to command
HashMap<String, String> values = provider.getDeviceCommands(itemName);
for (String cmd : values.keySet()) {
String[] commandParts = values.get(cmd).split(":");
String deviceCmd = commandParts[1];
boolean match = false;
if (deviceCmd.startsWith(ADVANCED_COMMAND_KEY)) {
// we currently have no info about the expected response string
// of a user configured "advanced" command, so skip this item
continue;
}
try {
String commandResponse = IpControlCommand.valueOf(deviceCmd).getResponse();
// when no respone is expected, the response string is empty
if (!commandResponse.isEmpty()) {
// compare response from network with response in enum
if (data.startsWith(commandResponse)) {
match = true;
}
}
} catch (Exception e) {
logger.error("Unregonized command '" + deviceCmd + "'", e);
}
if (match) {
Class<? extends Item> itemType = provider.getItemType(itemName);
State v = convertDeviceValueToOpenHabState(itemType, data,
IpControlCommand.valueOf(deviceCmd));
eventPublisher.postUpdate(itemName, v);
break;
}
}
}
}
}
}
/**
* Convert receiver value to OpenHAB state.
*
* @param itemType
* @param data
*
* @return
*/
private State convertDeviceValueToOpenHabState(Class<? extends Item> itemType, String data,
IpControlCommand cmdType) {
State state = UnDefType.UNDEF;
try {
int index;
// cut off the leading response identifier to get the payload string
String payloadSubstring = data.substring(cmdType.getResponse().length());
// if the response consisted just of the response-identifier and had
// no further value attached handle it as boolean of value 1
// this is used e.g. when switch items are used for selecting the source.
// the selected source will then be set to "ON"
if (payloadSubstring.length() == 0) {
payloadSubstring = "1";
}
// special case for display info query: convert to human readable string
if (cmdType.getCommandRef() == IpControlCommandRef.DISPLAY_INFO_QUERY) {
IpControlDisplayInformation displayInfo = new IpControlDisplayInformation(payloadSubstring);
payloadSubstring = displayInfo.getInfoText();
logger.debug("DisplayInfo: converted value '{}' to string '{}'", data, payloadSubstring);
}
if (itemType == SwitchItem.class) {
index = Integer.parseInt(payloadSubstring);
state = (index == 0) ? OnOffType.ON : OnOffType.OFF; // according to Spec: 0=ON, 1=OFF!
}
else if (itemType == DimmerItem.class) {
if (cmdType.getCommandRef().getCommand() == IpControlCommandRef.VOLUME_QUERY.getCommand()
|| cmdType.getCommandRef().getCommand() == IpControlCommandRef.VOLUME_SET.getCommand()) {
index = convertVolumeToPercent(Integer.parseInt(payloadSubstring));
} else {
index = Integer.parseInt(payloadSubstring);
}
state = new PercentType(index);
} else if (itemType == NumberItem.class) {
index = Integer.parseInt(payloadSubstring);
state = new DecimalType(index);
} else if (itemType == RollershutterItem.class) {
index = Integer.parseInt(payloadSubstring);
state = new PercentType(index);
} else if (itemType == StringItem.class) {
state = new StringType(payloadSubstring);
}
} catch (Exception e) {
logger.debug("Cannot convert value '{}' to data type {}", data, itemType);
}
return state;
}
/**
* map the receiver volume values to percent values
*
* receiver volume 0 is -80db and 0%
* receiver volume 185 is +12dB and 100% (at least for zone 1)
*
* @param volume the receiver volume value
*
*/
private Integer convertVolumeToPercent(Integer volume) {
Integer percent = Math.round((volume * 100) / 185);
logger.debug("converted volume '" + volume.toString() + "' to '" + percent.toString() + "%'");
return percent;
}
/**
* map percent values to receiver volumes
*
* receiver volume 0 is -80db and 0%
* receiver volume 185 is +12dB and 100% (at least for zone 1)
*
* @param volume the receiver volume value
*
*/
private Integer convertPercentToVolume(Integer percent) {
Integer volume = Math.round((percent * 185) / 100);
logger.debug("converted " + percent.toString() + "% to volume " + volume.toString());
return volume;
}
/**
* Initialize item value. Method send query to receiver if init query is configured to binding item configuration
*
* @param itemType
*
*/
private void initializeItem(String itemName) {
for (PioneerAvrBindingProvider provider : providers) {
String initCmd = provider.getItemInitCommand(itemName);
if (initCmd != null) {
logger.debug("Initialize item {}", itemName);
String[] commandParts = initCmd.split(":");
String deviceId = commandParts[0];
String deviceCmd = commandParts[1];
DeviceConfig device = deviceConfigCache.get(deviceId);
PioneerAvrConnection remoteController = device.getConnection();
if (device != null && remoteController != null) {
if (deviceCmd.startsWith(ADVANCED_COMMAND_KEY)) {
deviceCmd = deviceCmd.replace(ADVANCED_COMMAND_KEY, "");
} else {
IpControlCommand cmd = IpControlCommand.valueOf(deviceCmd);
deviceCmd = cmd.getCommand();
}
remoteController.send(deviceCmd);
} else {
logger.warn("Cannot find connection details for device id '{}'", deviceId);
}
}
}
}
/**
* Internal data structure which carries the connection details of one
* device (there could be several)
*/
static class DeviceConfig {
String host;
int port = DEFAULT_PORT;
PioneerAvrConnection connection = null;
String deviceId;
Boolean connectionCheckActive;
public DeviceConfig(String deviceId) {
this.deviceId = deviceId;
connectionCheckActive = true; // by default, the conn check is active
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
@Override
public String toString() {
return "Device [id=" + deviceId + ", host=" + host + ", port=" + port + "]";
}
PioneerAvrConnection getConnection() {
if (connection == null) {
connection = new PioneerAvrConnection(host, port, connectionCheckActive);
}
return connection;
}
}
}