/**
* 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.anel.internal;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
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.State;
import org.openhab.core.types.UnDefType;
/**
* Class for parsing data packets from Anel NET-PwrCtrl device.
*
* @since 1.6.0
* @author paphko
*/
public class AnelDataParser {
private final static Pattern TEMPERATURE_PATTERN = Pattern.compile("\\d\\d\\.\\d");
/**
* Parse data package from Anel NET-PwrCtrl device and update
* {@link AnelState}. For all changes, {@link AnelCommandType}s are created
* and returned. The expected format is as follows, separated with colons:
* <ul>
* <li>0. 'NET-PwrCtrl'
* <li>1. <name> (may contain trailing spaces)
* <li>2. <ip>
* <li>3. <netmask>
* <li>4. <gateway>
* <li>5. <mac addess>
* <li>6-13. <name of switch n>,<state 0 or 1>
* <li>14. <locked switches>
* <li>15. <http port>
* <li>16-23. <name of IO n>,<direction in=1 or out=0>,<state
* 0 or 1>
* <li>24. <temperature>
* <li>25. <firmware version> (may contain trailing line break)
* </ul>
* Source: <a
* href="http://www.anel-elektronik.de/forum_new/viewtopic.php?f=16&t=207"
* >Anel forum (German)</a>
* <p>
* It turned out that the HOME variant has a different format which contains
* only the first 16 segments. If that is the case, the remaining fields of
* {@link AnelState} are simply ignored (and remain unset).
* </p>
* Source: <a href="https://github.com/openhab/openhab/issues/2068">Issue
* 2068</a>
*
* @param data
* The data received from {@link AnelUDPConnector}.
* @param state
* The internal (cached) state of the device.
* @return A map of commands to the new openHAB {@link State}s.
* @throws Exception
* If the data is invalid or corrupt.
*/
public static Map<AnelCommandType, State> parseData(byte[] data, AnelState state) throws Exception {
final String string = new String(data);
final String[] arr = string.split(":");
if (arr.length != 28 && arr.length != 26 && arr.length != 16) {
throw new IllegalArgumentException(
"Data with 16, 26, or 28 values expected but " + arr.length + " received: " + string);
}
if (!arr[0].equals("NET-PwrCtrl")) {
throw new IllegalArgumentException("Data must start with 'NET-PwrCtrl' but it didn't: " + arr[0]);
}
if (!state.host.equals(arr[2]) && !state.host.equalsIgnoreCase(arr[1].trim())) {
return Collections.emptyMap(); // this came from another device
}
final Map<AnelCommandType, State> result = new LinkedHashMap<AnelCommandType, State>();
// check for switch changes, update cached state, and prepare command if
// needed
final int locked = Integer.parseInt(arr[14]);
for (int nr = 0; nr < 8; nr++) {
final String[] swState = arr[6 + nr].split(",");
if (swState.length == 2) {
// expected format
addCommand(state.switchName, nr, swState[0], "F" + (nr + 1) + "NAME", result);
addCommand(state.switchState, nr, "1".equals(swState[1]), "F" + (nr + 1), result);
} else {
// unexpected format, set states to null
addCommand(state.switchName, nr, null, "F" + (nr + 1) + "NAME", result);
addCommand(state.switchState, nr, null, "F" + (nr + 1), result);
}
addCommand(state.switchLocked, nr, (locked & (1 << nr)) > 0, "F" + (nr + 1) + "LOCKED", result);
}
// IO and temperature is only available if array has length 24
if (arr.length > 16) {
// check for IO changes, update cached state, and prepare commands
// if needed
for (int nr = 0; nr < 8; nr++) {
final String[] ioState = arr[16 + nr].split(",");
if (ioState.length == 3) {
// expected format
addCommand(state.ioName, nr, ioState[0], "IO" + (nr + 1) + "NAME", result);
addCommand(state.ioIsInput, nr, "1".equals(ioState[1]), "IO" + (nr + 1) + "ISINPUT", result);
addCommand(state.ioState, nr, "1".equals(ioState[2]), "IO" + (nr + 1), result);
} else {
// unexpected format, set states to null
addCommand(state.ioName, nr, null, "IO" + (nr + 1) + "NAME", result);
addCommand(state.ioIsInput, nr, null, "IO" + (nr + 1) + "ISINPUT", result);
addCommand(state.ioState, nr, null, "IO" + (nr + 1), result);
}
}
// example temperature string: '26.4°C'
// '°' is caused by some different encoding, so cut last 2 chars
final String temperature = arr[24].substring(0, arr[24].length() - 2);
if (hasTemperaturChanged(state, temperature)) {
result.put(AnelCommandType.TEMPERATURE, new DecimalType(temperature));
state.temperature = temperature;
}
}
// maybe the device's name changed?!
final String name = arr[1];
if (!name.equals(state.name)) {
result.put(AnelCommandType.NAME, new StringType(name));
state.name = name;
}
return result;
}
private static boolean hasTemperaturChanged(AnelState state, final String temperature) {
if (state == null || state.temperature == null || state.temperature.isEmpty()) {
return true; // no calculation needed if cached state is empty
}
if (temperature.equals(state.temperature)) {
return false; // if it equals, nothing changed
}
// report only changes of more than 0.1 degrees
if (TEMPERATURE_PATTERN.matcher(temperature).matches()
&& TEMPERATURE_PATTERN.matcher(state.temperature).matches()) {
final int intTemperature = Integer.parseInt(temperature.replace(".", ""));
final int stateTemperature = Integer.parseInt(state.temperature.replace(".", ""));
return !(intTemperature + 1 == stateTemperature || intTemperature - 1 == stateTemperature);
}
// pattern does not match or temperature differs more than 0.1 degrees
// from last update
return true;
}
private static <T> void addCommand(T[] cache, int index, T newValue, String commandType,
Map<AnelCommandType, State> result) {
if (newValue != null) {
if (!newValue.equals(cache[index])) {
final AnelCommandType cmd = AnelCommandType.getCommandType(commandType);
final State state;
if (newValue instanceof String) {
state = new StringType((String) newValue);
} else if (newValue instanceof Boolean) {
state = (Boolean) newValue ? OnOffType.ON : OnOffType.OFF;
} else {
throw new UnsupportedOperationException(
"TODO: implement value to state conversion for: " + newValue.getClass().getCanonicalName());
}
result.put(cmd, state);
cache[index] = newValue;
}
} else if (cache[index] != null) {
result.put(AnelCommandType.getCommandType(commandType), UnDefType.UNDEF);
cache[index] = null;
}
}
}