/**
* 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.sonance.internal;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.sonance.SonanceBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.binding.BindingProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Sonance Binding communicates with Sonance Amplifiers like the DSP 2-150,
* DSP 8-130 and the DSP 2-750
*
* @author Laurens Van Acker
* @since 1.8.0
*/
public class SonanceBinding extends AbstractActiveBinding<SonanceBindingProvider> {
private Map<String, Socket> socketCache = new HashMap<String, Socket>();
private Map<String, DataOutputStream> outputStreamCache = new HashMap<String, DataOutputStream>();
private Map<String, BufferedReader> bufferedReaderCache = new HashMap<String, BufferedReader>();
private static Pattern volumePattern = Pattern.compile(".*Vol=(-?\\d{1,2}).*");
private static final Logger logger = LoggerFactory.getLogger(SonanceBinding.class);
/**
* the refresh interval which is used to poll values from the Sonance server
* (optional, defaults to 60000ms)
*/
private long refreshInterval = 60000;
/**
* Nothing happens in the constructor
*/
public SonanceBinding() {
}
/**
* Called by the SCR to activate the component with the refresh Interval. To
* override the default refresh interval one has to add a parameter to
* openhab.cfg like Sonance:refresh=<intervalInMs>.
*
* @param bundleContext
* BundleContext of the Bundle that defines this component
* @param configuration
* Configuration properties for this component obtained from the
* ConfigAdmin service
*/
public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) {
String refreshIntervalString = (String) configuration.get("refresh");
if (StringUtils.isNotBlank(refreshIntervalString)) {
refreshInterval = Long.parseLong(refreshIntervalString);
}
setProperlyConfigured(true);
}
/**
* Deallocate socket connection, output stream and buffered reader caches
*
* @param reason
* Reason code for the deactivation:<br>
* <ul>
* <li>0 – Unspecified
* <li>1 – The component was disabled
* <li>2 – A reference became unsatisfied
* <li>3 – A configuration was changed
* <li>4 – A configuration was deleted
* <li>5 – The component was disposed
* <li>6 – The bundle was stopped
* </ul>
*/
public void deactivate(final int reason) {
socketCache.clear();
outputStreamCache.clear();
bufferedReaderCache.clear();
}
/*
* {@inheritDoc}
*/
@Override
protected long getRefreshInterval() {
return refreshInterval;
}
/*
* {@inheritDoc}
*/
@Override
protected String getName() {
return "Sonance Refresh Service";
}
/*
* {@inheritDoc}
*/
@Override
protected void execute() {
if (!bindingsExist()) {
logger.debug("There is no existing Sonance binding configuration => refresh cycle aborted!");
return;
}
logger.info("Refreshing all items");
List<String> offlineEndPoints = new ArrayList<String>();
for (SonanceBindingProvider provider : providers) {
for (String itemName : provider.getItemNames()) {
String group = provider.getGroup(itemName);
String ip = provider.getIP(itemName);
int port = provider.getPort(itemName);
String key = ip + ":" + port;
if (!offlineEndPoints.contains(key)) {
try {
if (!socketCache.containsKey(key)) {
socketCache.put(key, new Socket(ip, port));
outputStreamCache.put(key, new DataOutputStream(socketCache.get(key).getOutputStream()));
bufferedReaderCache.put(key,
new BufferedReader(new InputStreamReader(socketCache.get(key).getInputStream())));
logger.debug("New socket created ({}:{})", ip, port);
}
if (provider.isMute(itemName)) {
sendMuteCommand(itemName, SonanceConsts.MUTE_QUERY + group, outputStreamCache.get(key),
bufferedReaderCache.get(key));
} else if (provider.isVolume(itemName)) {
sendVolumeCommand(itemName, SonanceConsts.VOLUME_QUERY + group, outputStreamCache.get(key),
bufferedReaderCache.get(key));
} else if (provider.isPower(itemName)) {
sendPowerCommand(itemName, SonanceConsts.POWER_QUERY, outputStreamCache.get(key),
bufferedReaderCache.get(key));
}
} catch (UnknownHostException e) {
logger.error("UnknownHostException occured when connecting to amplifier {}:{}.", ip, port);
} catch (IOException e) {
logger.debug("Amplifier ({},{}) is offline, status can't be updated at this moment.", ip, port);
try {
socketCache.get(key).close();
} catch (Exception ex) {
}
socketCache.remove(key);
outputStreamCache.remove(key);
bufferedReaderCache.remove(key);
offlineEndPoints.add(key); // Stop trying to fetch other
// values from this end
// point until next execute
// cycle
}
}
}
}
}
protected void addBindingProvider(SonanceBindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(SonanceBindingProvider bindingProvider) {
super.removeBindingProvider(bindingProvider);
}
/*
* {@inheritDoc}
*/
@Override
public void bindingChanged(BindingProvider provider, String itemName) {
super.bindingChanged(provider, itemName);
for (Map.Entry<String, Socket> entry : socketCache.entrySet()) {
try {
entry.getValue().close();
} catch (IOException e) {
logger.error("Can't close a socket when binding changed.");
}
}
// Cleanup all sockets
socketCache.clear();
outputStreamCache.clear();
bufferedReaderCache.clear();
}
/*
* {@inheritDoc}
*/
@Override
protected void internalReceiveCommand(String itemName, Command command) {
logger.debug("Command received ({}, {})", itemName, command);
SonanceBindingProvider provider = findFirstMatchingBindingProvider(itemName);
String group = provider.getGroup(itemName);
String ip = provider.getIP(itemName);
int port = provider.getPort(itemName);
Socket s = null;
try {
s = new Socket(ip, port);
DataOutputStream outToServer = new DataOutputStream(s.getOutputStream());
BufferedReader i = new BufferedReader(new InputStreamReader(s.getInputStream()));
if (provider.isMute(itemName)) {
if (command.equals(OnOffType.OFF)) {
sendMuteCommand(itemName, SonanceConsts.MUTE_ON + group, outToServer, i);
} else if (command.equals(OnOffType.ON)) {
sendMuteCommand(itemName, SonanceConsts.MUTE_OFF + group, outToServer, i);
} else {
logger.error("I don't know what to do with the command \"{}\"", command);
}
} else if (provider.isPower(itemName)) {
if (command.equals(OnOffType.OFF)) {
sendPowerCommand(itemName, SonanceConsts.POWER_OFF, outToServer, i);
} else if (command.equals(OnOffType.ON)) {
sendPowerCommand(itemName, SonanceConsts.POWER_ON, outToServer, i);
} else {
logger.error("I don't know what to do with the command \"{}\"", command);
}
} else if (provider.isVolume(itemName)) {
if (command.equals(UpDownType.UP)) {
sendVolumeCommand(itemName, SonanceConsts.VOLUME_UP + group, outToServer, i);
} else if (command.equals(UpDownType.DOWN)) {
sendVolumeCommand(itemName, SonanceConsts.VOLUME_DOWN + group, outToServer, i);
} else {
try {
Double d = Double.parseDouble(command.toString());
setVolumeCommand(itemName, group, d.intValue(), outToServer, i, ip + ":" + port);
} catch (NumberFormatException nfe) {
logger.error("I don't know what to do with the volume command \"{}\" ({})", command,
nfe.getMessage());
}
}
}
s.close();
} catch (IOException e) {
logger.debug("IO Exception when sending command. Exception: {}", e.getMessage());
} finally {
closeSilently(s);
}
}
/**
* Closes a socket
*
* @param s
* socket to close
*/
private void closeSilently(Socket s) {
try {
if (s != null) {
s.close();
}
} catch (IOException e) {
}
}
/*
* {@inheritDoc}
*/
@Override
protected void internalReceiveUpdate(String itemName, State newState) {
logger.debug("Update received ({},{})", itemName, newState);
SonanceBindingProvider provider = findFirstMatchingBindingProvider(itemName);
String group = provider.getGroup(itemName);
String ip = provider.getIP(itemName);
int port = provider.getPort(itemName);
ip = null;
group = null; // cleanup
Socket s = null;
try {
s = new Socket(ip, port);
DataOutputStream outToServer = new DataOutputStream(s.getOutputStream());
BufferedReader i = new BufferedReader(new InputStreamReader(s.getInputStream()));
if (provider.isMute(itemName)) {
if (newState.equals(OnOffType.OFF)) {
sendMuteCommand(itemName, SonanceConsts.MUTE_ON + group, outToServer, i);
} else if (newState.equals(OnOffType.ON)) {
sendMuteCommand(itemName, SonanceConsts.MUTE_OFF + group, outToServer, i);
} else {
logger.error("I don't know what to do with this new state \"{}\"", newState);
}
}
if (provider.isPower(itemName)) {
if (newState.equals(OnOffType.OFF)) {
sendPowerCommand(itemName, SonanceConsts.POWER_OFF, outToServer, i);
} else if (newState.equals(OnOffType.ON)) {
sendPowerCommand(itemName, SonanceConsts.POWER_ON, outToServer, i);
} else {
logger.error("I don't know what to do with this new state \"{}\"", newState);
}
} else if (provider.isVolume(itemName)) {
if (newState.equals(IncreaseDecreaseType.INCREASE)) {
sendVolumeCommand(itemName, SonanceConsts.VOLUME_UP + group, outToServer, i);
} else if (newState.equals(IncreaseDecreaseType.DECREASE)) {
sendVolumeCommand(itemName, SonanceConsts.VOLUME_DOWN + group, outToServer, i);
} else {
logger.error("I don't know what to do with this new state \"{}\"", newState);
}
s.close();
}
} catch (IOException e) {
logger.error("IO Exception when received internal command. Message: {}", e.getMessage());
} finally {
closeSilently(s);
}
}
/**
* Send volume commands to groups (music zones)
*
* @param itemName
* item name to send update to
* @param command
* Sonance IP code to execute
* @param outToServer
* date output stream we can write to
* @param i
* bufered reader where we can read from
* @throws IOException
* throws an exception when we can't reach to amplifier
*/
private void sendVolumeCommand(String itemName, String command, DataOutputStream outToServer, BufferedReader i)
throws IOException {
char[] cbuf = new char[50]; // Response is always 50 characters
logger.debug("Sending volume command {}", command);
outToServer.write(hexStringToByteArray(command));
i.read(cbuf, 0, 50);
Matcher m = volumePattern.matcher(new String(cbuf));
if (m.find()) {
String volume = m.group(1);
eventPublisher.postUpdate(itemName, new DecimalType(volume));
logger.debug("Setting volume for item {} on {}", itemName, volume);
} else {
logger.error("Error sending regular volume command {}, received this: {}", command, new String(cbuf));
}
}
/**
* Enable or disable specific groups (music zones)
*
* @param itemName
* item name to send update to
* @param command
* Sonance IP code to execute
* @param outToServer
* date output stream we can write to
* @param i
* bufered reader where we can read from
* @throws IOException
* throws an exception when we can't reach to amplifier
*/
private void sendMuteCommand(String itemName, String command, DataOutputStream outToServer, BufferedReader i)
throws IOException {
char[] cbuf = new char[50]; // Response is always 50 characters
logger.debug("Sending mute command {}", command);
outToServer.write(hexStringToByteArray(command));
i.read(cbuf, 0, 50);
String result = new String(cbuf);
logger.trace("Received this result: {}", result);
if (result.contains("Mute=on") || result.contains("MuteOn")) {
eventPublisher.postUpdate(itemName, OnOffType.OFF);
logger.debug("Setting mute item {} on OFF", itemName);
} else if (result.contains("Mute=off") || result.contains("MuteOff")) {
eventPublisher.postUpdate(itemName, OnOffType.ON);
logger.debug("Setting mute item {} on ON", itemName);
} else {
logger.error("Error sending mute command {}, received this: {}", command, result);
}
}
/**
* Wake up or put amplifier to sleep
*
* @param itemName
* item name to send update to
* @param command
* Sonance IP code to execute
* @param outToServer
* date output stream we can write to
* @param i
* bufered reader where we can read from
* @throws IOException
* throws an exception when we can't reach to amplifier
*/
private void sendPowerCommand(String itemName, String command, DataOutputStream outToServer, BufferedReader i)
throws IOException {
char[] cbuf = new char[50]; // Response is always 50 characters
logger.debug("Sending power command {}", command);
outToServer.write(hexStringToByteArray(command));
i.read(cbuf, 0, 50);
String result = new String(cbuf);
logger.trace("Received power response: {}", result);
if (result.contains("Off")) {
eventPublisher.postUpdate(itemName, OnOffType.OFF);
logger.debug("Setting power item {} on OFF", itemName);
} else if (result.contains("On")) {
eventPublisher.postUpdate(itemName, OnOffType.ON);
logger.debug("Setting power item {} on ON", itemName);
} else {
logger.trace("Error sending power command {}, received this: {}", command, result);
}
}
/**
* Sets the group to the specified target volume. Amplifier doesn't support
* direct volume commands, so a loop is needed
*
* @param itemName
* item to publish result to
* @param group
* target group
* @param targetVolume
* target volume
* @param outToServer
* data output stream where we can write to
* @param i
* buffered reader where we can read from
* @param endpoint
* ip:port
* @throws IOException
* throws an IOException when we can't reach the amplifier
*/
private void setVolumeCommand(String itemName, String group, int targetVolume, DataOutputStream outToServer,
BufferedReader i, String endpoint) throws IOException {
char[] cbuf = new char[50]; // Response is always 50 characters
String question = String.format("%s%s%s", SonanceConsts.DIRECT_VOLUME_QUERY,
Integer.toHexString(183 + targetVolume), group);
logger.trace("Sending this to amplifier: {}", question);
outToServer.write(hexStringToByteArray(question));
i.read(cbuf, 0, 50);
String result = new String(cbuf);
logger.trace("Received this as response : {}", result);
Matcher m = volumePattern.matcher(result);
if (m.find()) {
double currentVolume = Integer.parseInt(m.group(1));
eventPublisher.postUpdate(itemName, new DecimalType(currentVolume));
logger.debug("Updating {} with new volume {}", itemName, currentVolume);
} else {
logger.error("Error sending volume command, received this: {}", new String(cbuf));
}
}
/**
* Get binding provider for that item
*
* @param itemName
* name of the item where we need to binding provder for
* @return SonanceBindingProvider
*/
protected SonanceBindingProvider findFirstMatchingBindingProvider(String itemName) {
SonanceBindingProvider firstMatchingProvider = null;
for (SonanceBindingProvider provider : providers) {
if (provider.providesBindingFor(itemName)) {
firstMatchingProvider = provider;
break;
}
}
return firstMatchingProvider;
}
/**
* Function to convert strings to hexadecimal bytes.
*
* @param s
* the string to convert to a hexadecimal byte array
* @return hexadecimal byte array
*/
private static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
}