/**
* 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.mochadx10.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.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.mochadx10.MochadX10BindingProvider;
import org.openhab.binding.mochadx10.commands.MochadX10Address;
import org.openhab.binding.mochadx10.commands.MochadX10Command;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.types.Command;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This binding is able to do the following tasks with the mochad X10 System:
*
* <ul>
* <li>X10 Appliance modules: switch on, switch off.</li>
* <li>X10 Dimmer modules: switch on, switch off, dim</li>
* <li>X10 Shutter modules: open, close, stop, move</li>
* </ul>
*
* @author Jack Sleuters
* @since 1.7.0
*/
public class MochadX10Binding extends AbstractBinding<MochadX10BindingProvider> implements ManagedService {
static final Logger logger = LoggerFactory.getLogger(MochadX10Binding.class);
/**
* The ip address of the Mochad X10 host (default 127.0.0.1)
*/
private String hostIp = "127.0.0.1";
/**
* The port number of the Mochad X10 host (default 1099)
*/
private int hostPort = 1099;
/**
* To implement the stop command for x10 modules like Rollershutters, it is required
* to know whether the last issued command was a 'dim' or a 'bright' command. If it
* was a 'dim' command, the 'bright' command has to be issued to realize stop functionality.
* If it was a 'bright' command, the 'dim' command has to be issued.
*
* This map keeps track of the last issued command per X10 address
*/
private Map<String, Command> lastIssuedCommand = new HashMap<String, Command>(); // required to determine the X10
// command for STOP
/**
* Not every X10 module keeps its state. Furthermore, some X10 commands like 'dim' or 'bright' change the
* brightness level in a relative way. Therefore, it is required to keep the absolute level of such module.
* According to discussions on the openhab forums, it is bad practice to use the item registry to retrieve
* the current level of a module. Therefore, it is stored internally in this binding.
*
* 'currentLevel' maps an X10 address string on a level value
*
* One could initialize this map with all possible X10 addresses and a default level value. However,
* in practice a lot less than 256 X10 modules will be connected. To keep memory usage as low as possible,
* the map is not initialized. When the current level of a module with an address that is not yet in the map
* is requested, a value of '-1' will be returned.
*/
private Map<String, Integer> currentLevel = new HashMap<String, Integer>();
/**
* The socket used to communicate with the Mochad X10 host
*/
private Socket client;
/**
* Used to receive messages from the Mochad X10 host
*/
private BufferedReader in;
/**
* Used to send commands to the Mochad X10 host
*/
private DataOutputStream out;
/**
* Receiving of messages from the Mochad X10 host is asynchronous. To make sure we
* capture incoming message, a separate receive thread is used.
*/
private ReceiveThread receiveThread;
/**
* Keeps track of the latest used X10 address. This is required because the following messages
* can be received from the Mochad X10 server. The first message specifies the full address 'D1'
* the second message only specifies the 'houseCode' part of the address. This means that
* the unitCode of the previous X10 address should be used.
*
* 03/11 22:08:01 Rx PL HouseUnit: D1
* 03/11 22:08:40 Rx PL House: D Func: Bright(1)
*
*/
private MochadX10Address previousX10Address;
/**
* Used to prevent reconnection when shutting down
*/
private boolean isShuttingDown = false;
/**
* The regular expression to check whether the ip address of the host is correct
*/
private static final String IPADDRESS_PATTERN = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
/**
* This thread asynchronously receives messages from the Mochad X10 Host. It
* parses the message and posts the appropriate event on the openHAB event bus.
*
* @author Jack Sleuters
* @since 1.7.0
*
*/
private class ReceiveThread extends Thread {
private MochadX10Binding binding;
/**
* The command parser parses messages received from the host and transforms them
* into MochadX10Commands.
*/
private MochadX10CommandParser commandParser = new MochadX10CommandParser(eventPublisher);
/**
* Constructor
*
* @param binding This binding
*/
public ReceiveThread(MochadX10Binding binding) {
this.binding = binding;
}
/**
* Process a message received by the Mochad X10 host
*
* @param msg string containing the received message
*/
private void processIncomingMessage(String msg) {
logger.debug("Received message: " + msg);
MochadX10Command command = commandParser.parse(msg);
if (command != null) {
logger.debug("Command: " + command.toString());
String itemName = getItemNameForAddress(command.getAddress());
if (itemName != null) {
command.postCommand(itemName, getCurrentLevel(command.getAddress().toString()));
logger.debug("Address " + command.getAddress() + " level set to " + command.getLevel());
binding.previousX10Address = command.getAddress();
logger.debug("ReceiveThread: previous X10 address set to " + previousX10Address.toString());
}
}
}
/**
* Run method. Runs the actual receiving process.
*/
@Override
public void run() {
String msg = null;
logger.debug("Starting Mochad X10 Receive thread");
while (!interrupted()) {
try {
// Blocking read, reading messages coming from Mochad X10 host
msg = in.readLine();
if (msg != null) {
processIncomingMessage(msg);
} else {
logger.error("Received a \"null\" message");
reconnectToMochadX10Server();
}
} catch (IOException e) {
logger.trace("Caught IOException " + e.getMessage());
reconnectToMochadX10Server();
}
}
logger.debug("Stopped Mochad X10 Receive thread");
}
}
protected void addBindingProvider(MochadX10BindingProvider bindingProvider) {
super.addBindingProvider(bindingProvider);
}
protected void removeBindingProvider(MochadX10BindingProvider bindingProvider) {
logger.trace("Mochad X10 Binding removeBindingProvider called");
super.removeBindingProvider(bindingProvider);
}
@Override
public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
if (properties != null) {
String ip = (String) properties.get("hostIp");
if (StringUtils.isNotBlank(ip)) {
if (isValidIpAddress(ip)) {
this.hostIp = ip;
} else {
throw new ConfigurationException(hostIp,
"The specified hostIp address \"" + ip + "\" is not a valid ip address");
}
}
String port = (String) properties.get("hostPort");
if (StringUtils.isNotBlank(port)) {
if (isValidPort(port)) {
this.hostPort = Integer.parseInt(port);
} else {
throw new ConfigurationException(port,
"The specified port \"" + port + "\" is not a valid port number.");
}
}
initializeBinding();
}
}
@Override
public void deactivate() {
// Close the connection with the Mochad X10 Server
logger.trace("Mochad X10 deactivate called");
isShuttingDown = true;
disconnectFromMochadX10Server();
super.deactivate();
}
/**
* Initialize the binding. Connection to the Mochad X10 host is established and after that
* the receive thread is started.
*/
private void initializeBinding() {
previousX10Address = new MochadX10Address("a1");
connectToMochadX10Server();
receiveThread = new ReceiveThread(this);
receiveThread.start();
}
/**
* Connect to the Mochad X10 host
*/
private void connectToMochadX10Server() {
logger.trace("Mochad X10 - connectToMochadX10Server called");
try {
client = new Socket(hostIp, hostPort);
in = new BufferedReader(new InputStreamReader(client.getInputStream()));
out = new DataOutputStream(client.getOutputStream());
logger.debug("Connected to Mochad X10 server");
} catch (UnknownHostException e) {
logger.error("Unknown host: " + hostIp + ":" + hostPort);
} catch (IOException e) {
logger.error("IOException: " + e.getMessage() + " while trying to connect to Mochad X10 host: " + hostIp
+ ":" + hostPort);
}
}
/**
* Disconnect from the Mochad X10 host. This method is required to clean up the connection
* after the binding was disconnected from the host.
*/
private void disconnectFromMochadX10Server() {
logger.trace("disconnectFromMochadX10Server called");
try {
logger.trace("Closing socket");
client.close();
logger.trace("Closing BufferedReader");
in.close();
logger.trace("Closing DataOutputStream");
out.close();
logger.debug("Disconnected from Mochad X10 server");
} catch (IOException e) {
logger.error("IOException: " + e.getMessage() + " while trying to disconnect from Mochad X10 host: "
+ hostIp + ":" + hostPort);
}
}
/**
* Reconnect to the Mochad X10 host after the connection to it was lost.
*/
private void reconnectToMochadX10Server() {
if (!isShuttingDown) {
logger.trace("reconnectToMochadX10Server called");
disconnectFromMochadX10Server();
try {
Thread.sleep(20000); // Wait 20 secs before retrying
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
connectToMochadX10Server();
logger.trace("Reconnected to Mochad X10 server");
} else {
logger.trace("Aborting reconnect to Mochad X10 server as deactivate in progress");
}
}
/**
* Check if the specified port number is a valid.
*
* @param port The port number to check
* @return true when valid, false otherwise
*/
private boolean isValidPort(String port) {
try {
int portNumber = Integer.parseInt(port);
if ((portNumber >= 1024) && (portNumber <= 65535)) {
return true;
}
} catch (NumberFormatException e) {
return false;
}
return false;
}
/**
* Check if the specified ip address is a valid.
*
* @param hostIp The ip address to check to check
* @return true when valid, false otherwise
*/
private boolean isValidIpAddress(String hostIp) {
if (hostIp.matches(IPADDRESS_PATTERN)) {
return true;
}
return false;
}
/**
* Given an X10 address (<houseCode><unitCode>) find the name of the
* corresponding bounded item
*
* @param address The X10 address
* @return The name of the corresponding item, null if no corresponding
* item could be found.
*/
private String getItemNameForAddress(MochadX10Address address) {
for (MochadX10BindingProvider provider : this.providers) {
Collection<String> itemNames = provider.getItemNames();
for (String itemName : itemNames) {
MochadX10BindingConfig bindingConfig = provider.getItemConfig(itemName);
if (bindingConfig.getAddress().equals(address.toString())) {
return bindingConfig.getItemName();
}
}
}
logger.warn("No item name found for address '" + address.toString() + "'");
return null;
}
/**
* Lookup of the configuration of the named item.
*
* @param itemName
* @return The configuration, null otherwise.
*/
private MochadX10BindingConfig getConfigForItemName(String itemName) {
for (MochadX10BindingProvider provider : this.providers) {
if (provider.getItemConfig(itemName) != null) {
return provider.getItemConfig(itemName);
}
}
logger.warn("No configuration found for item '" + itemName + "'");
return null;
}
@Override
protected void internalReceiveCommand(String itemName, Command command) {
MochadX10BindingConfig deviceConfig = getConfigForItemName(itemName);
if (deviceConfig == null) {
return;
}
String address = deviceConfig.getAddress();
String tm = deviceConfig.getTransmitMethod();
String commandStr = "none";
Command previousCommand = lastIssuedCommand.get(address);
int level = -1;
if (command instanceof OnOffType) {
commandStr = OnOffType.ON.equals(command) ? "on" : "off";
level = OnOffType.ON.equals(command) ? 100 : 0;
} else if (command instanceof UpDownType) {
commandStr = UpDownType.UP.equals(command) ? "bright" : "dim";
level = UpDownType.UP.equals(command) ? 100 : 0;
} else if (command instanceof StopMoveType) {
if (StopMoveType.STOP.equals(command)) {
commandStr = UpDownType.UP.equals(previousCommand) ? "dim" : "bright";
} else {
// Move not supported yet
commandStr = "none";
}
} else if (command instanceof PercentType) {
if (deviceConfig.getItemType() == DimmerItem.class) {
level = ((PercentType) command).intValue();
if (((PercentType) command).intValue() == 0) {
// If percent value equals 0 the x10 "off" command is used instead of the dim command
commandStr = "off";
} else {
long dim_value = 0;
if (deviceConfig.getDimMethod().equals("xdim")) {
// 100% maps to value (XDIM_LEVELS - 1) so we need to do scaling
dim_value = Math.round(
((PercentType) command).doubleValue() * (MochadX10Command.XDIM_LEVELS - 1) / 100);
commandStr = "xdim " + dim_value;
} else {
// 100% maps to value (DIM_LEVELS - 1) so we need to do scaling
Integer currentValue = currentLevel.get(address);
if (currentValue == null) {
currentValue = 0;
}
logger.debug("Address " + address + " current level " + currentValue);
int newValue = ((PercentType) command).intValue();
int relativeValue;
if (newValue > currentValue) {
relativeValue = (int) Math
.round((newValue - currentValue) * ((MochadX10Command.DIM_LEVELS - 1) * 1.0 / 100));
commandStr = "bright " + relativeValue;
} else if (currentValue > newValue) {
relativeValue = (int) Math
.round((currentValue - newValue) * ((MochadX10Command.DIM_LEVELS - 1) * 1.0 / 100));
commandStr = "dim " + relativeValue;
} else {
// If there is no change in state, do nothing
commandStr = "none";
}
}
}
} else if (deviceConfig.getItemType() == RollershutterItem.class) {
level = ((PercentType) command).intValue();
Double invert_level = 100 - ((PercentType) command).doubleValue();
long newlevel = Math.round(invert_level * 25.0 / 100);
commandStr = "extended_code_1 0 1 " + newlevel;
}
} else if (command instanceof IncreaseDecreaseType) {
// Increase decrease not yet supported
commandStr = "none";
}
try {
if (!commandStr.equals("none")) {
out.writeBytes(tm + " " + address + " " + commandStr + "\n");
logger.debug(tm + " " + address + " " + commandStr);
out.flush();
previousX10Address.setAddress(address);
logger.debug("Previous X10 address set to " + previousX10Address.toString());
if (level != -1) {
currentLevel.put(address, level);
logger.debug("Address " + address + " level set to " + level);
}
}
} catch (IOException e) {
reconnectToMochadX10Server();
logger.error("IOException: " + e.getMessage() + " while trying to send a command to Mochad X10 host: "
+ hostIp + ":" + hostPort);
}
lastIssuedCommand.put(address, command);
}
/**
* Retrieve the current level [0..100] of the X10 module identified by 'address'
*
* @param address the X10 address
* @return if the level was stored at least once, the current level otherwise -1
*/
private int getCurrentLevel(String address) {
if (currentLevel.get(address) == null) {
return -1;
}
return currentLevel.get(address);
}
/**
* Retrieve the output stream of the established socket connection
*
* @return the output stream
*/
public DataOutputStream getOut() {
return out;
}
}