added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.max-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-max" description="MAX! Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.max/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,121 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.actions;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.actions.IMaxCubeActions;
import org.openhab.binding.max.internal.handler.MaxCubeBridgeHandler;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxCubeActions} class defines rule actions for MAX! Cube
*
* @author Christoph Weitkamp - Initial contribution
*/
@ThingActionsScope(name = "max-cube")
@NonNullByDefault
public class MaxCubeActions implements ThingActions, IMaxCubeActions {
private final Logger logger = LoggerFactory.getLogger(MaxCubeActions.class);
private @Nullable MaxCubeBridgeHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof MaxCubeBridgeHandler) {
this.handler = (MaxCubeBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
@Override
@RuleAction(label = "Backup Cube Data", description = "Creates a backup of the MAX! Cube data.")
public @ActionOutput(name = "success", type = "java.lang.Boolean") Boolean backup() {
MaxCubeBridgeHandler actionsHandler = handler;
if (actionsHandler == null) {
logger.info("MaxCubeActions: Action service ThingHandler is null!");
return false;
}
actionsHandler.backup();
return true;
}
public static boolean backup(@Nullable ThingActions actions) {
return invokeMethodOf(actions).backup();
}
@Override
@RuleAction(label = "Reset Cube Configuration", description = "Resets the MAX! Cube room and device information. Devices will need to be included again!")
public @ActionOutput(name = "success", type = "java.lang.Boolean") Boolean resetConfig() {
MaxCubeBridgeHandler actionsHandler = handler;
if (actionsHandler == null) {
logger.info("MaxCubeActions: Action service ThingHandler is null!");
return false;
}
actionsHandler.cubeConfigReset();
return true;
}
public static boolean reset(@Nullable ThingActions actions) {
return invokeMethodOf(actions).resetConfig();
}
@Override
@RuleAction(label = "Restart Cube", description = "Restarts the MAX! Cube.")
public @ActionOutput(name = "success", type = "java.lang.Boolean") Boolean reboot() {
MaxCubeBridgeHandler actionsHandler = handler;
if (actionsHandler == null) {
logger.info("MaxCubeActions: Action service ThingHandler is null!");
return false;
}
actionsHandler.cubeReboot();
return true;
}
public static boolean reboot(@Nullable ThingActions actions) {
return invokeMethodOf(actions).reboot();
}
private static IMaxCubeActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(MaxCubeActions.class.getName())) {
if (actions instanceof IMaxCubeActions) {
return (IMaxCubeActions) actions;
} else {
return (IMaxCubeActions) Proxy.newProxyInstance(IMaxCubeActions.class.getClassLoader(),
new Class[] { IMaxCubeActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of MaxCubeActions");
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.actions;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.actions.IMaxDevicesActions;
import org.openhab.binding.max.internal.handler.MaxDevicesHandler;
import org.openhab.core.automation.annotation.ActionOutput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxDevicesActions} class defines rule actions for MAX! devices
*
* @author Christoph Weitkamp - Initial contribution
*/
@ThingActionsScope(name = "max-devices")
@NonNullByDefault
public class MaxDevicesActions implements ThingActions, IMaxDevicesActions {
private final Logger logger = LoggerFactory.getLogger(MaxDevicesActions.class);
private @Nullable MaxDevicesHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof MaxDevicesHandler) {
this.handler = (MaxDevicesHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
@Override
@RuleAction(label = "Delete Device from Cube", description = "Deletes the device from the MAX! Cube. Device will need to be included again!")
public @ActionOutput(name = "success", type = "java.lang.Boolean") Boolean deleteFromCube() {
MaxDevicesHandler actionsHandler = handler;
if (actionsHandler == null) {
logger.info("MaxDevicesActions: Action service ThingHandler is null!");
return false;
}
actionsHandler.deviceDelete();
return true;
}
public static boolean deleteFromCube(@Nullable ThingActions actions) {
return invokeMethodOf(actions).deleteFromCube();
}
private static IMaxDevicesActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(MaxDevicesActions.class.getName())) {
if (actions instanceof IMaxDevicesActions) {
return (IMaxDevicesActions) actions;
} else {
return (IMaxDevicesActions) Proxy.newProxyInstance(IMaxDevicesActions.class.getClassLoader(),
new Class[] { IMaxDevicesActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of MaxDevicesActions");
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.ConfigConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxBackupUtils} class supports the backup of cube data
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MaxBackupUtils {
private final Logger logger = LoggerFactory.getLogger(MaxBackupUtils.class);
private static final String BACKUP_PATH = "max";
private final String dbFolderName;
private final String backupId;
private boolean inProgress = false;
private StringBuilder msg = new StringBuilder();
private String cube = "";
public MaxBackupUtils(String backupId) {
this.backupId = backupId;
dbFolderName = ConfigConstants.getUserDataFolder() + File.separator + BACKUP_PATH;
File folder = new File(dbFolderName);
if (!folder.exists()) {
folder.mkdirs();
}
}
public MaxBackupUtils() {
this("auto" + new SimpleDateFormat("MM").format(Calendar.getInstance().getTime()));
}
public void buildBackup(String msgLine) {
if (msgLine.startsWith("H:")) {
msg = new StringBuilder();
cube = msgLine.substring(2).split(",")[0];
inProgress = true;
}
if (inProgress) {
msg.append(msgLine);
msg.append(System.lineSeparator());
}
if (inProgress && msgLine.startsWith("L:")) {
inProgress = false;
saveMsg(msg.toString(), cube);
}
}
private void saveMsg(String data, String cube) {
File dataFile = new File(dbFolderName + File.separator + "backup-" + backupId + "-" + cube + ".txt");
try (FileWriter writer = new FileWriter(dataFile)) {
writer.write(data);
logger.debug("MAX! backup saved to {}", dataFile.getAbsolutePath());
} catch (IOException e) {
logger.warn("MAX! Failed to write backup file '{}': {}", dataFile.getName(), e.getMessage());
}
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link MaxBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MaxBindingConstants {
public static final String BINDING_ID = "max";
// List of main device types
public static final String DEVICE_THERMOSTAT = "thermostat";
public static final String DEVICE_THERMOSTATPLUS = "thermostatplus";
public static final String DEVICE_WALLTHERMOSTAT = "wallthermostat";
public static final String DEVICE_ECOSWITCH = "ecoswitch";
public static final String DEVICE_SHUTTERCONTACT = "shuttercontact";
public static final String BRIDGE_MAXCUBE = "bridge";
// List of all Thing Type UIDs
public static final ThingTypeUID HEATINGTHERMOSTAT_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_THERMOSTAT);
public static final ThingTypeUID HEATINGTHERMOSTATPLUS_THING_TYPE = new ThingTypeUID(BINDING_ID,
DEVICE_THERMOSTATPLUS);
public static final ThingTypeUID WALLTHERMOSTAT_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_WALLTHERMOSTAT);
public static final ThingTypeUID ECOSWITCH_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_ECOSWITCH);
public static final ThingTypeUID SHUTTERCONTACT_THING_TYPE = new ThingTypeUID(BINDING_ID, DEVICE_SHUTTERCONTACT);
public static final ThingTypeUID CUBEBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, BRIDGE_MAXCUBE);
// List of all Channel ids
public static final String CHANNEL_VALVE = "valve";
public static final String CHANNEL_BATTERY = "battery_low";
public static final String CHANNEL_MODE = "mode";
public static final String CHANNEL_ACTUALTEMP = "actual_temp";
public static final String CHANNEL_SETTEMP = "set_temp";
public static final String CHANNEL_LOCKED = "locked";
public static final String CHANNEL_CONTACT_STATE = "contact_state";
public static final String CHANNEL_FREE_MEMORY = "free_mem";
public static final String CHANNEL_DUTY_CYCLE = "duty_cycle";
// Custom Properties
public static final String PROPERTY_IP_ADDRESS = "ipAddress";
public static final String PROPERTY_VENDOR_NAME = "eQ-3 AG";
public static final String PROPERTY_RFADDRESS = "rfAddress";
public static final String PROPERTY_ROOMNAME = "room";
public static final String PROPERTY_ROOMID = "roomId";
public static final String PROPERTY_DEVICENAME = "name";
public static final String PROPERTY_REFRESH_ACTUAL_RATE = "refreshActualRate";
public static final String PROPERTY_NTP_SERVER1 = "ntpServer1";
public static final String PROPERTY_NTP_SERVER2 = "ntpServer2";
// Thermostat settings properties
public static final String PROPERTY_THERMO_COMFORT_TEMP = "comfortTemp";
public static final String PROPERTY_THERMO_ECO_TEMP = "ecoTemp";
public static final String PROPERTY_THERMO_MAX_TEMP_SETPOINT = "maxTempSetpoint";
public static final String PROPERTY_THERMO_MIN_TEMP_SETPOINT = "minTempSetpoint";
public static final String PROPERTY_THERMO_OFFSET_TEMP = "offsetTemp";
public static final String PROPERTY_THERMO_WINDOW_OPEN_TEMP = "windowOpenTemp";
public static final String PROPERTY_THERMO_WINDOW_OPEN_DURATION = "windowOpenDuration";
public static final String PROPERTY_THERMO_DECALCIFICATION = "decalcification";
public static final String PROPERTY_THERMO_VALVE_MAX = "valveMaximum";
public static final String PROPERTY_THERMO_VALVE_OFFSET = "valveOffset";
public static final String PROPERTY_THERMO_BOOST_DURATION = "boostDuration";
public static final String PROPERTY_THERMO_BOOST_VALVEPOS = "boostValvePos";
public static final String PROPERTY_THERMO_PROGRAM_DATA = "programData";
// List of actions
public static final String ACTION_CUBE_REBOOT = "action-cubeReboot";
public static final String ACTION_CUBE_RESET = "action-cubeReset";
public static final String ACTION_DEVICE_DELETE = "action-deviceDelete";
public static final String BUTTON_ACTION_VALUE = "1234";
public static final int BUTTON_NOACTION_VALUE = -1;
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.of(HEATINGTHERMOSTAT_THING_TYPE, HEATINGTHERMOSTATPLUS_THING_TYPE, WALLTHERMOSTAT_THING_TYPE,
ECOSWITCH_THING_TYPE, SHUTTERCONTACT_THING_TYPE).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_BRIDGE_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(CUBEBRIDGE_THING_TYPE).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
Stream.concat(SUPPORTED_DEVICE_THING_TYPES_UIDS.stream(), SUPPORTED_BRIDGE_THING_TYPES_UIDS.stream())
.collect(Collectors.toSet()));
}

View File

@@ -0,0 +1,139 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal;
import static org.openhab.binding.max.internal.MaxBindingConstants.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.handler.MaxCubeBridgeHandler;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
import org.openhab.core.io.console.extensions.ConsoleCommandExtension;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link MaxConsoleCommandExtension} class provides additional options through the console command line.
*
* @author Marcel Verpaalen - Initial contribution
*/
@Component(service = ConsoleCommandExtension.class)
@NonNullByDefault
public class MaxConsoleCommandExtension extends AbstractConsoleCommandExtension {
private static final String SUBCMD_BACKUP = "backup";
private static final String SUBCMD_REBOOT = "reboot";
private final ThingRegistry thingRegistry;
@Activate
public MaxConsoleCommandExtension(@Reference ThingRegistry thingRegistry) {
super("max", "Additional EQ3 MAX! commands.");
this.thingRegistry = thingRegistry;
}
@Override
public void execute(String[] args, Console console) {
if (args.length > 0) {
switch (args[0]) {
case SUBCMD_BACKUP:
handleBackup(console);
break;
case SUBCMD_REBOOT:
handleReboot(args, console);
break;
default:
console.println(String.format("Unknown MAX! sub command '%s'", args[0]));
printUsage(console);
break;
}
} else {
printUsage(console);
printMaxDevices(console, SUPPORTED_THING_TYPES_UIDS);
}
}
private void handleBackup(Console console) {
for (Thing thing : findDevices(SUPPORTED_BRIDGE_THING_TYPES_UIDS)) {
MaxCubeBridgeHandler handler = getHandler(thing.getUID().toString());
if (handler != null) {
handler.backup();
console.println(String.format("Creating backup for %s", thing.getUID().toString()));
}
}
}
private void handleReboot(String[] args, Console console) {
if (args.length > 1) {
MaxCubeBridgeHandler handler = getHandler(args[1]);
if (handler != null) {
handler.cubeReboot();
} else {
console.println(String.format("Could not find MAX! cube %s", args[1]));
printMaxDevices(console, SUPPORTED_BRIDGE_THING_TYPES_UIDS);
}
} else {
console.println("Specify MAX! cube to reboot.");
printMaxDevices(console, SUPPORTED_BRIDGE_THING_TYPES_UIDS);
}
}
private List<Thing> findDevices(Set<ThingTypeUID> deviceTypes) {
List<Thing> devs = new ArrayList<>();
for (Thing thing : thingRegistry.getAll()) {
if (deviceTypes.contains(thing.getThingTypeUID())) {
devs.add(thing);
}
}
return devs;
}
private @Nullable MaxCubeBridgeHandler getHandler(String thingId) {
MaxCubeBridgeHandler handler = null;
try {
ThingUID bridgeUID = new ThingUID(thingId);
Thing thing = thingRegistry.get(bridgeUID);
if ((thing != null) && (thing.getHandler() != null)
&& (thing.getHandler() instanceof MaxCubeBridgeHandler)) {
handler = (MaxCubeBridgeHandler) thing.getHandler();
}
} catch (Exception e) {
handler = null;
}
return handler;
}
private void printMaxDevices(Console console, Set<ThingTypeUID> deviceTypes) {
console.println("Known MAX! devices: ");
for (Thing thing : findDevices(deviceTypes)) {
console.println(String.format("MAX! %s device: %s%s", thing.getThingTypeUID().getId(),
thing.getUID().toString(), ((thing.getHandler() != null) ? "" : " (without handler)")));
}
}
@Override
public List<String> getUsages() {
return Arrays.asList(new String[] { buildCommandUsage(SUBCMD_BACKUP, "Backup MAX! cube data"),
buildCommandUsage(SUBCMD_REBOOT + " <thingUID>", "Reset MAX! cube") });
}
}

View File

@@ -0,0 +1,182 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utility class for common tasks within the MAX! binding package.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*
*/
@NonNullByDefault
public final class Utils {
/**
* Returns the integer value of an hexadecimal number (base 16).
*
* @param hex
* the hex value to be parsed into an integer
* @return the given hex value as integer
*/
public static final int fromHex(String hex) {
return Integer.parseInt(hex, 16);
}
/**
* Returns the hexadecimal number of a number of integer values.
*
* @param values
* the integer values to be converted into hexadecimal numbers
* @return the given numbers as hexadecimal number
*/
public static final String toHex(int... values) {
String returnValue = "";
for (int v : values) {
returnValue += v < 16 ? "0" + Integer.toHexString(v).toUpperCase() : Integer.toHexString(v).toUpperCase();
}
return returnValue;
}
/**
* Returns the hexadecimal number of a bit array .
*
* @param bits
* the boolean array representing the bits to be converted into
* hexadecimal numbers
* @return the hexadecimal number
*/
public static final String toHex(boolean[] bits) {
int retVal = 0;
for (int i = 0; i < bits.length; ++i) {
retVal |= (bits[i] ? 1 : 0) << i;
}
return toHex(retVal);
}
/**
* Converts an Java signed byte into its general (unsigned) value as being
* used in other programming languages and platforms.
*
* @param b
* the byte to be converted into its integer value
* @return the integer value represented by the given byte
*/
public static final int fromByte(byte b) {
return b & 0xFF;
}
/**
* Resolves the date and time based based on a three byte encoded within a
* MAX! Cube L message.
*
* Date decoding (two byte)
*
* <pre>
* Hex Binary
* 9D0B 1001110100001011
* MMMDDDDDM YYYYYY
* 100 0 = 1000b = 8d = month
* 11101 = 11101b = 29d = day
* 001011 = 1011b = 11d = year-2000
* </pre>
*
* Time decoding (one byte)
*
* <pre>
* Hex Decimal
* 1F 31 * 0.5 hours = 15:30
* </pre>
*
* @param date
* the date to be converted based on two bytes
* @param time
* the time to be converted based on a single byte
* @return the date time based on the values provided
*/
@SuppressWarnings("deprecation")
public static Date resolveDateTime(int date, int time) {
int month = ((date & 0xE000) >> 12) + ((date & 0x80) >> 7);
int day = (date & 0x1F00) >> 8;
int year = (date & 0x0F) + 2000;
int hours = (int) (time * 0.5);
int minutes = (int) (60 * ((time * 0.5) - hours));
return new Date(year, month, day, hours, minutes);
}
/**
* Returns a bit representation as boolean values in a reversed manner. A
* bit string <code>0001 0010</code> would be returnd as
* <code>0100 1000</code>. That way, the least significant bit can be
* addressed by bits[0], the second by bits[1] and so on.
*
* @param value
* the integer value to be converted in a bit array
* @return the bit array of the input value in a reversed manner.
*/
public static boolean[] getBits(int value) {
boolean[] bits = new boolean[8];
for (int i = 0; i < 8; i++) {
bits[i] = (((value >> i) & 0x1) == 1);
}
return bits;
}
/**
* Convert a string representation of hexadecimal to a byte array.
*
* For example: String s = "00010203" returned byte array is {0x00, 0x01,
* 0x03}
*
* @param s
* @return byte array equivalent to hex string
*/
public 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;
}
/**
* Convert a byte array to a string representation of hexadecimals.
*
* For example: byte array is {0x00, 0x01, 0x03} returned String s =
* "00 01 02 03"
*
* @param byte array
* @return String equivalent to hex string
*/
static final String HEXES = "0123456789ABCDEF";
public static String getHex(byte[] raw) {
final StringBuilder hex = new StringBuilder(3 * raw.length);
for (final byte b : raw) {
hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).append(" ");
}
hex.delete(hex.length() - 1, hex.length());
return hex.toString();
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.actions.MaxCubeActions;
/**
* The {@link IMaxCubeActions} defines the interface for all thing actions supported by the binding.
* These methods, parameters, and return types are explained in {@link MaxCubeActions}.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public interface IMaxCubeActions {
Boolean backup();
Boolean resetConfig();
Boolean reboot();
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.actions.MaxDevicesActions;
/**
* The {@link IMaxDevicesActions} defines the interface for all thing actions supported by the binding.
* These methods, parameters, and return types are explained in {@link MaxDevicesActions}.
*
* @author Christoph Weitkamp - Initial contribution
*/
@NonNullByDefault
public interface IMaxDevicesActions {
Boolean deleteFromCube();
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ACommand} deletes the device and room configuration from the Cube.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class ACommand extends CubeCommand {
@Override
public String getCommandString() {
return "a:" + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "A:";
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link C_CubeCommand} to request configuration of a new MAX! device after inclusion.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class CCommand extends CubeCommand {
private final String rfAddress;
public CCommand(String rfAddress) {
this.rfAddress = rfAddress;
}
@Override
public String getCommandString() {
String cmd = "c:" + rfAddress + '\r' + '\n';
return cmd;
}
@Override
public String getReturnStrings() {
return "C:";
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link CubeCommand} is the base class for commands to be send to the MAX! Cube.
*
* @author Marcel Verpaalen - Initial contribution
*
*/
@NonNullByDefault
public abstract class CubeCommand {
/**
* @return the String to be send to the MAX! Cube
*/
public abstract String getCommandString();
/**
* @return the String expected to be received from the Cube to signify the
* end of the message
*/
public abstract String getReturnStrings();
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link F_CubeCommand} is used to query and update the NTP servers used by the Cube.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class FCommand extends CubeCommand {
private String ntpServer1 = "";
private String ntpServer2 = "";
/**
* Queries the Cube for the NTP info
*/
public FCommand() {
}
/**
* Updates the Cube the NTP info
*/
public FCommand(@Nullable String ntpServer1, @Nullable String ntpServer2) {
this.ntpServer1 = ntpServer1 != null ? ntpServer1 : "";
this.ntpServer2 = ntpServer2 != null ? ntpServer2 : "";
}
@Override
public String getCommandString() {
final String servers;
if (ntpServer1.length() > 0 && ntpServer2.length() > 0) {
servers = ntpServer1 + "," + ntpServer2;
} else {
servers = ntpServer1 + ntpServer2;
}
return "f:" + servers + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "F:";
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LCommand} request a status update for MAX! devices.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class LCommand extends CubeCommand {
@Override
public String getCommandString() {
return "l:" + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "L:";
}
}

View File

@@ -0,0 +1,184 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.device.RoomInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MCommand} Creates the MAX! Cube the room & device name information update message.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class MCommand extends CubeCommand {
private final Logger logger = LoggerFactory.getLogger(MCommand.class);
private static final byte MAGIC_NR = 86;
private static final byte M_VERSION = 2;
private static final int MAX_NAME_LENGTH = 32;
private static final int MAX_GROUP_COUNT = 20;
private static final int MAX_DEVICES_COUNT = 140;
private static final int MAX_MSG_LENGTH = 1900;
private final List<Device> devices;
public final List<RoomInformation> rooms;
public MCommand(List<Device> devices) {
this(devices, new ArrayList<>());
}
public MCommand(List<Device> devices, List<RoomInformation> rooms) {
this.devices = new ArrayList<>(devices);
this.rooms = new ArrayList<>(rooms);
roombuilder();
}
public void listRooms() {
for (RoomInformation room : rooms) {
logger.debug("M Command room info: {}", room);
}
}
public List<RoomInformation> getRooms() {
return rooms;
}
private void roombuilder() {
for (Device di : devices) {
boolean foundRoom = false;
for (RoomInformation room : rooms) {
if (room.getPosition() == di.getRoomId()) {
foundRoom = true;
}
}
// Add new rooms based on device information.
// TODO check if it is allowed to have any device creating a room, or should it be a thermostat
if (!foundRoom && di.getRoomId() != 0 && rooms.size() < MAX_GROUP_COUNT) {
RoomInformation room = new RoomInformation(di.getRoomId(), di.getRoomName(), di.getRFAddress());
rooms.add(room);
}
}
}
public byte[] concatenateByteArrays(List<byte[]> blocks) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (byte[] b : blocks) {
os.write(b, 0, b.length);
}
return os.toByteArray();
}
@Override
public String getCommandString() {
int deviceCount = 0;
int roomCount = 0;
ByteArrayOutputStream message = new ByteArrayOutputStream();
try {
byte[] header = { MAGIC_NR, M_VERSION, (byte) rooms.size() };
message.write(header);
Set<Integer> sortedRooms = new TreeSet<>();
for (RoomInformation room : rooms) {
sortedRooms.add(room.getPosition());
}
for (Integer roomPos : sortedRooms) {
for (RoomInformation room : rooms) {
if (room.getPosition() == roomPos) {
if (roomCount < MAX_GROUP_COUNT) {
byte[] roomName = StringUtils.abbreviate(room.getName(), MAX_NAME_LENGTH)
.getBytes(StandardCharsets.UTF_8);
byte[] nameLength = new byte[] { (byte) roomName.length };
byte[] rfAddress = Utils.hexStringToByteArray(room.getRFAddress());
message.write(roomPos.byteValue());
message.write(nameLength);
message.write(roomName);
message.write(rfAddress);
} else {
logger.warn("{} exceeds max number of rooms ({}). Ignored", room, MAX_GROUP_COUNT);
}
roomCount++;
}
}
}
for (Device di : devices) {
if (deviceCount < MAX_DEVICES_COUNT) {
deviceCount++;
} else {
logger.warn("{} exceeds max number of devices ({}). Ignored", di, MAX_DEVICES_COUNT);
}
}
message.write((byte) deviceCount);
for (Device di : devices) {
if (deviceCount > 0) {
byte[] deviceType = { (byte) di.getType().getValue() };
byte[] rfAddress = Utils.hexStringToByteArray(di.getRFAddress());
byte[] deviceName = StringUtils.abbreviate(di.getName(), MAX_NAME_LENGTH)
.getBytes(StandardCharsets.UTF_8);
byte[] nameLength = { (byte) deviceName.length };
byte[] serialNumber = di.getSerialNumber().getBytes(StandardCharsets.UTF_8);
byte[] roomId = { (byte) di.getRoomId() };
message.write(deviceType);
message.write(rfAddress);
message.write(serialNumber);
message.write(nameLength);
message.write(deviceName);
message.write(roomId);
} else {
logger.warn("{} exceeds max number of devices ({}). Ignored", di, MAX_DEVICES_COUNT);
}
deviceCount--;
}
byte[] dst = { 0x01 };
message.write(dst);
} catch (IOException e) {
logger.debug("Error while generating m: command: {}", e.getMessage(), e);
}
final String encodedString = Base64.getEncoder().encodeToString(message.toByteArray());
final StringBuilder commandStringBuilder = new StringBuilder();
int parts = (int) Math.round(encodedString.length() / MAX_MSG_LENGTH + 0.5);
for (int i = 0; i < parts; i++) {
String partString = StringUtils.abbreviate(encodedString.substring((i) * MAX_MSG_LENGTH), MAX_MSG_LENGTH);
commandStringBuilder.append("m:").append(String.format("%02d", i)).append(",").append(partString)
.append('\r').append('\n');
}
return commandStringBuilder.toString();
}
@Override
public String getReturnStrings() {
return "A:";
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link N_CubeCommand} starts the inclusion mode for new MAX! devices.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class NCommand extends CubeCommand {
// Example n:003c = start inclusion, timeout 003c = 60 sec
// Timeout for inclusion mode in seconds
private static final int DEFAULT_TIMEOUT = 60;
@Override
public String getCommandString() {
return "n:" + String.format("%02x", DEFAULT_TIMEOUT) + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "N:";
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link QCommand} Quits the connection to the MAX! Cube.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class QCommand extends CubeCommand {
@Override
public String getCommandString() {
return "q:" + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "";
}
}

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.ThermostatModeType;
/**
* {@link SCommand} for setting MAX! thermostat temperature & mode.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update + simplification
*/
@NonNullByDefault
public class SCommand extends CubeCommand {
private static final String BASE_STRING_S = "000040000000"; // for single devices
private static final String BASE_STRING_G = "000440000000"; // for group/room devices
private final boolean[] bits;
private final String rfAddress;
private final int roomId;
/**
* Creates a new instance of the MAX! protocol S command.
*
* @param rfAddress
* the RF address the command is for
* @param roomId
* the room ID the RF address is mapped to
* @param setpointTemperature
* the desired setpoint temperature for the device.
*/
public SCommand(String rfAddress, int roomId, ThermostatModeType mode, double setpointTemperature) {
this.rfAddress = rfAddress;
this.roomId = roomId;
// Temperature setpoint, Temp uses 6 bits (bit 0:5),
// 20 deg C = bits 101000 = dec 40/2 = 20 deg C,
// you need 8 bits to send so add the 2 bits below (sample 10101000 = hex A8)
// bit 0,1 = 00 = Auto weekprog (no temp is needed)
int setpointValue = (int) (setpointTemperature * 2);
bits = Utils.getBits(setpointValue);
// default to perm setting
// AB => bit mapping
// 01 = Permanent
// 10 = Temporarily
// 11 = Boost
switch (mode) {
case MANUAL:
bits[7] = false; // A (MSB)
bits[6] = true; // B
break;
case AUTOMATIC:
bits[7] = false; // A (MSB)
bits[6] = false; // B
break;
case BOOST:
bits[7] = true; // A (MSB)
bits[6] = true; // B
break;
case VACATION:
// not implemented needs time
default:
// no further modes supported
}
}
/**
* Returns the Base64 encoded command string to be sent via the MAX!
* protocol.
*
* @return the string representing the command
*/
@Override
public String getCommandString() {
final String baseString;
if (roomId == 0) {
baseString = BASE_STRING_S;
} else {
baseString = BASE_STRING_G;
}
final String commandString = baseString + rfAddress + Utils.toHex(roomId) + Utils.toHex(bits);
final String encodedString = Base64.getEncoder().encodeToString(Utils.hexStringToByteArray(commandString));
return "s:" + encodedString + "\r\n";
}
@Override
public String getReturnStrings() {
return "S:";
}
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SConfigCommand} for setting MAX! thermostat configuration.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class SConfigCommand extends CubeCommand {
private String baseString = "";
private final String rfAddress;
private final int roomId;
private byte[] commandBytes = new byte[0];
private final Logger logger = LoggerFactory.getLogger(SConfigCommand.class);
public enum ConfigCommandType {
Temperature,
Valve,
SetRoom,
RemoveRoom,
ProgramData
}
private final ConfigCommandType configCommandType;
/**
* Creates a base command with rfAddress and roomID.
*
* @param rfAddress
* the RF address the command is for
* @param roomId
* the room ID the RF address is mapped to
* @param configCommandType
* the Type of config command to be send
*/
public SConfigCommand(String rfAddress, int roomId, ConfigCommandType configCommandType) {
this.rfAddress = rfAddress;
this.roomId = roomId;
this.configCommandType = configCommandType;
if (configCommandType == ConfigCommandType.Temperature) {
setTempConfigDefault();
} else if (configCommandType == ConfigCommandType.SetRoom) {
baseString = "000022000000";
commandBytes = new byte[] { 0 };
} else if (configCommandType == ConfigCommandType.RemoveRoom) {
baseString = "000023000000";
commandBytes = new byte[] { 0 };
} else {
logger.debug("Config Command {} not implemented", configCommandType);
}
}
/**
* Set the Thermostat temperature configuration
*/
public SConfigCommand(String rfAddress, int roomId, double tempComfort, double tempEco, double tempSetpointMax,
double tempSetpointMin, double tempOffset, double tempOpenWindow, int durationOpenWindow) {
this.rfAddress = rfAddress;
this.roomId = roomId;
this.configCommandType = ConfigCommandType.Temperature;
setTempConfig(tempComfort, tempEco, tempSetpointMax, tempSetpointMin, tempOffset, tempOpenWindow,
durationOpenWindow);
}
/**
* Set the thermostat default temperature config.
*/
public void setTempConfigDefault() {
double tempComfort = 21.0; // 0x2a
double tempEco = 17.0; // 0x28
double tempSetpointMax = 30.0; // 0x3d
double tempSetpointMin = 4.5; // 0x09
double tempOffset = 3.5; // 0x07
double tempOpenWindow = 12.0; // 0x18;
int durationOpenWindow = 3; // 0x03;
setTempConfig(tempComfort, tempEco, tempSetpointMax, tempSetpointMin, tempOffset, tempOpenWindow,
durationOpenWindow);
}
/**
* Set the thermostat temperature config.
*
* @param tempComfort
* the Comfort temperature
* @param tempEco
* the ECO temperature
* @param tempSetpointMax
* the Max temperature
* @param tempSetpointMin
* the min temperature
* @param tempOffset
* the offset temperature
* @param tempOpenWindow
* the window open temperature
* @param durationOpenWindow
* the window open duration in minutes
*/
public void setTempConfig(double tempComfort, double tempEco, double tempSetpointMax, double tempSetpointMin,
double tempOffset, double tempOpenWindow, int durationOpenWindow) {
baseString = "000011000000";
byte tempComfortByte = (byte) (tempComfort * 2);
byte tempEcoByte = (byte) (tempEco * 2);
byte tempSetpointMaxByte = (byte) (tempSetpointMax * 2);
byte tempSetpointMinByte = (byte) (tempSetpointMin * 2);
byte tempOffsetByte = (byte) ((tempOffset + 3.5) * 2);
byte tempOpenWindowByte = (byte) (tempOpenWindow * 2);
byte durationOpenWindowByte = (byte) (durationOpenWindow / 5);
commandBytes = new byte[] { tempComfortByte, tempEcoByte, tempSetpointMaxByte, tempSetpointMinByte,
tempOffsetByte, tempOpenWindowByte, durationOpenWindowByte };
logger.debug(
"Thermostat Config Command: confTemp: {}, ecoTemp: {}, setMax: {}, setMin: {}, offset: {}, windowTemp: {}, windowDur:{}",
tempComfort, tempEco, tempSetpointMax, tempSetpointMin, tempOffset, tempOpenWindow, durationOpenWindow);
}
/**
* Returns the Base64 encoded command string to be sent via the MAX! Cube.
*
* @return the string representing the command
*/
@Override
public String getCommandString() {
final StringBuilder commandConfigString = new StringBuilder();
for (byte b : commandBytes) {
commandConfigString.append(String.format("%02X", b));
}
String commandString = baseString + rfAddress;
if (configCommandType == ConfigCommandType.SetRoom || configCommandType == ConfigCommandType.RemoveRoom) {
commandString = commandString + commandConfigString + Utils.toHex(roomId);
} else {
commandString = commandString + Utils.toHex(roomId) + commandConfigString;
}
String encodedString = Base64.getEncoder().encodeToString(Utils.hexStringToByteArray(commandString));
return "s:" + encodedString + "\r\n";
}
@Override
public String getReturnStrings() {
return "S:";
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.apache.commons.lang.ArrayUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
/**
* The {@link T_CubeCommand} is used to unlink MAX! devices from the Cube.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class TCommand extends CubeCommand {
private static final int FORCE_UPDATE = 1;
private static final int NO_FORCE_UPDATE = 0;
private final List<String> rfAddresses = new ArrayList<>();
private final boolean forceUpdate;
public TCommand(String rfAddress, boolean forceUpdate) {
this.rfAddresses.add(rfAddress);
this.forceUpdate = forceUpdate;
}
/**
* Adds a rooms for deletion
*/
public void addRoom(String rfAddress) {
this.rfAddresses.add(rfAddress);
}
@Override
public String getCommandString() {
final int updateForced = forceUpdate ? FORCE_UPDATE : NO_FORCE_UPDATE;
byte[] commandArray = null;
for (String rfAddress : rfAddresses) {
commandArray = ArrayUtils.addAll(Utils.hexStringToByteArray(rfAddress), commandArray);
}
String encodedString = Base64.getEncoder().encodeToString(commandArray);
return "t:" + String.format("%02d", rfAddresses.size()) + "," + updateForced + "," + encodedString + "\r\n";
}
@Override
public String getReturnStrings() {
return "A:";
}
}

View File

@@ -0,0 +1,249 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UdpCubeCommand} is responsible for sending UDP commands to the MAX!
* Cube LAN gateway.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class UdpCubeCommand {
private static final String MAXCUBE_COMMAND_STRING = "eQ3Max*\0";
private final Logger logger = LoggerFactory.getLogger(UdpCubeCommand.class);
protected static boolean commandRunning;
private final UdpCommandType commandType;
private final String serialNumber;
private Map<String, String> commandResponse = new HashMap<>();
@Nullable
private String ipAddress;
public UdpCubeCommand(UdpCommandType commandType, @Nullable String serialNumber) {
this.commandType = commandType;
if (serialNumber == null || serialNumber.isEmpty()) {
this.serialNumber = "**********";
} else {
this.serialNumber = serialNumber;
}
}
/**
* UDP command types
* REBOOT - R Reboot
* DISCOVERY - I Identify
* NETWORK - N Get network address
* URL - h get URL information
* DEFAULTNET - c get network default info
*/
public enum UdpCommandType {
REBOOT,
DISCOVERY,
NETWORK,
URL,
DEFAULTNET
}
/**
* Executes the composed {@link UdpCubeCommand} command
*/
public synchronized boolean send() {
String commandString;
if (commandType.equals(UdpCommandType.REBOOT)) {
commandString = MAXCUBE_COMMAND_STRING + serialNumber + "R";
} else if (commandType.equals(UdpCommandType.DISCOVERY)) {
commandString = MAXCUBE_COMMAND_STRING + serialNumber + "I";
} else if (commandType.equals(UdpCommandType.NETWORK)) {
commandString = MAXCUBE_COMMAND_STRING + serialNumber + "N";
} else if (commandType.equals(UdpCommandType.URL)) {
commandString = MAXCUBE_COMMAND_STRING + serialNumber + "h";
} else if (commandType.equals(UdpCommandType.DEFAULTNET)) {
commandString = MAXCUBE_COMMAND_STRING + serialNumber + "c";
} else {
logger.debug("Unknown Command {}", commandType);
return false;
}
commandResponse.clear();
logger.debug("Send {} command to MAX! Cube {}", commandType, serialNumber);
sendUdpCommand(commandString, ipAddress);
logger.trace("Done sending command.");
receiveUdpCommandResponse();
logger.debug("Done receiving response.");
return true;
}
private void receiveUdpCommandResponse() {
commandRunning = true;
try (DatagramSocket bcReceipt = new DatagramSocket(23272)) {
bcReceipt.setReuseAddress(true);
bcReceipt.setSoTimeout(5000);
while (commandRunning) {
// Wait for a response
byte[] recvBuf = new byte[1500];
DatagramPacket receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
bcReceipt.receive(receivePacket);
// We have a response
String message = new String(receivePacket.getData(), receivePacket.getOffset(),
receivePacket.getLength(), StandardCharsets.UTF_8);
if (logger.isDebugEnabled()) {
logger.debug("Broadcast response from {} : {} '{}'", receivePacket.getAddress(), message.length(),
message);
}
// Check if the message is correct
if (message.startsWith("eQ3Max") && !message.equals(MAXCUBE_COMMAND_STRING)) {
commandResponse.put("maxCubeIP", receivePacket.getAddress().getHostAddress().toString());
commandResponse.put("maxCubeState", message.substring(0, 8));
commandResponse.put("serialNumber", message.substring(8, 18));
commandResponse.put("msgValidid", message.substring(18, 19));
String requestType = message.substring(19, 20);
commandResponse.put("requestType", requestType);
if (requestType.equals("I")) {
commandResponse.put("rfAddress",
Utils.getHex(message.substring(21, 24).getBytes(StandardCharsets.UTF_8))
.replace(" ", "").toLowerCase());
commandResponse.put("firmwareVersion", Utils
.getHex(message.substring(24, 26).getBytes(StandardCharsets.UTF_8)).replace(" ", "."));
} else {
// TODO: Further parsing of the other message types
commandResponse.put("messageResponse",
Utils.getHex(message.substring(20).getBytes(StandardCharsets.UTF_8)));
}
commandRunning = false;
if (logger.isDebugEnabled()) {
final StringBuilder builder = new StringBuilder();
for (final Map.Entry<String, String> entry : commandResponse.entrySet()) {
builder.append(String.format("%s: %s\n", entry.getKey(), entry.getValue()));
}
logger.debug("MAX! UDP response {}", builder);
}
}
}
} catch (SocketTimeoutException e) {
logger.trace("No further response");
commandRunning = false;
} catch (IOException e) {
logger.debug("IO error during MAX! Cube response: {}", e.getMessage());
commandRunning = false;
}
}
/**
* Send broadcast message over all active interfaces
*
* @param commandString string to be used for the discovery
* @param ipAddress IP address of the MAX! Cube
*
*/
private void sendUdpCommand(String commandString, @Nullable String ipAddress) {
DatagramSocket bcSend = null;
try {
bcSend = new DatagramSocket();
bcSend.setBroadcast(true);
byte[] sendData = commandString.getBytes(StandardCharsets.UTF_8);
// Broadcast the message over all the network interfaces
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue;
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress[] broadcast = new InetAddress[3];
if (ipAddress != null && !ipAddress.isEmpty()) {
broadcast[0] = InetAddress.getByName(ipAddress);
} else {
broadcast[0] = InetAddress.getByName("224.0.0.1");
broadcast[1] = InetAddress.getByName("255.255.255.255");
broadcast[2] = interfaceAddress.getBroadcast();
}
for (InetAddress bc : broadcast) {
// Send the broadcast package!
if (bc != null) {
try {
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc, 23272);
bcSend.send(sendPacket);
} catch (IOException e) {
logger.debug("IO error during MAX! Cube UDP command sending: {}", e.getMessage());
} catch (Exception e) {
logger.debug("{}", e.getMessage(), e);
}
logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
networkInterface.getDisplayName());
}
}
}
}
logger.trace("Done looping over all network interfaces. Now waiting for a reply!");
} catch (IOException e) {
logger.debug("IO error during MAX! Cube UDP command sending: {}", e.getMessage());
} finally {
try {
if (bcSend != null) {
bcSend.close();
}
} catch (Exception e) {
// Ignore
}
}
}
/**
* Set the IP address to send the command to. The command will be send to the address and broadcasted over all
* active interfaces
*
* @param ipAddress IP address of the MAX! Cube
*
*/
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
/**
* Response of the MAX! Cube on the
*
*/
public Map<String, String> getCommandResponse() {
return commandResponse;
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
/**
* The {@link ZCommand} send a wakeup request to MAX! devices.
*
* @author Marcel Verpaalen - Initial Contribution
*/
@NonNullByDefault
public class ZCommand extends CubeCommand {
public enum WakeUpType {
ALL,
ROOM,
DEVICE
}
private static final int DEFAULT_WAKETIME = 30;
private final String address;
private final WakeUpType wakeUpType;
private final int wakeUpTime;
public ZCommand(WakeUpType wakeUpType, String address, int wakeupTime) {
this.address = address;
this.wakeUpType = wakeUpType;
this.wakeUpTime = wakeupTime;
}
public static ZCommand wakeupRoom(int roomId) {
return new ZCommand(WakeUpType.ROOM, String.format("%02d", roomId), DEFAULT_WAKETIME);
}
public static ZCommand wakeupRoom(int roomId, int wakeupTime) {
return new ZCommand(WakeUpType.ROOM, String.format("%02d", roomId), wakeupTime);
}
public static ZCommand wakeupDevice(String rfAddress) {
return new ZCommand(WakeUpType.DEVICE, rfAddress, DEFAULT_WAKETIME);
}
public static ZCommand wakeupDevice(String rfAddress, int wakeupTime) {
return new ZCommand(WakeUpType.DEVICE, rfAddress, wakeupTime);
}
public static ZCommand wakeupAllDevices() {
return new ZCommand(WakeUpType.ALL, "0", DEFAULT_WAKETIME);
}
public static ZCommand wakeupAllDevices(int wakeupTime) {
return new ZCommand(WakeUpType.ALL, "0", wakeupTime);
}
@Override
public String getCommandString() {
final String commandString;
switch (wakeUpType) {
case ALL:
commandString = "A";
break;
case ROOM:
commandString = "G," + address;
break;
case DEVICE:
commandString = "D," + address;
break;
default:
throw new IllegalStateException("Unknown wakeup type: " + wakeUpType);
}
return "z:" + Utils.toHex(wakeUpTime) + "," + commandString + '\r' + '\n';
}
@Override
public String getReturnStrings() {
return "A:";
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.config;
import org.openhab.binding.max.internal.MaxBindingConstants;
/**
* Configuration class for {@link MaxBindingConstants} bridge used to connect to the
* maxCube device.
*
* @author Marcel Verpaalen - Initial contribution
*/
public class MaxCubeBridgeConfiguration {
/** The IP address of the MAX! Cube LAN gateway */
public String ipAddress;
/**
* The port of the MAX! Cube LAN gateway as provided at
* http://www.elv.de/controller.aspx?cid=824&detail=10&detail2=3484
*/
public Integer port;
/** The refresh interval in seconds which is used to poll given MAX! Cube */
public Integer refreshInterval;
/** The unique serial number for a device */
public String serialNumber;
/**
* If set to true, the binding will leave the connection to the cube open
* and just request new informations. This allows much higher poll rates and
* causes less load than the non-exclusive polling but has the drawback that
* no other apps (i.E. original software) can use the cube while this
* binding is running.
*/
public boolean exclusive = false;
/**
* in exclusive mode, how many requests are allowed until connection is
* closed and reopened
*/
public Integer maxRequestsPerConnection;
public Integer cubeReboot;
/** NTP Server 1 hostname */
public String ntpServer1;
/** NTP Server 2 hostname */
public String ntpServer2;
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* Cube Lan Gateway.
*
* @author Marcel Verpaalen - Initial contribution
*/
public class Cube extends Device {
private boolean portalEnabled;
private String portalUrl;
private String timeZoneWinter;
private String timeZoneDaylightSaving;
public Cube(DeviceConfiguration c) {
super(c);
}
@Override
public DeviceType getType() {
return DeviceType.Cube;
}
@Override
public String getName() {
return "MAX! Cube Lan interface";
}
public boolean isPortalEnabled() {
return portalEnabled;
}
public void setPortalEnabled(boolean portalEnabled) {
this.portalEnabled = portalEnabled;
}
public String getPortalUrl() {
return portalUrl;
}
public void setPortalUrl(String portalUrl) {
this.portalUrl = portalUrl;
}
public String getTimeZoneWinter() {
return timeZoneWinter;
}
public void setTimeZoneWinter(String timeZoneWinter) {
this.timeZoneWinter = timeZoneWinter;
}
public String getTimeZoneDaylightSaving() {
return timeZoneDaylightSaving;
}
public void setTimeZoneDaylightSaving(String timeZoneDaylightSaving) {
this.timeZoneDaylightSaving = timeZoneDaylightSaving;
}
}

View File

@@ -0,0 +1,394 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openhab.binding.max.internal.Utils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for devices provided by the MAX! protocol.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update + enhancements
*/
public abstract class Device {
private static final Logger LOGGER = LoggerFactory.getLogger(Device.class);
private final String serialNumber;
private String rfAddress;
private int roomId;
private String roomName;
private String name;
private boolean updated;
private boolean batteryLow;
private boolean initialized;
private boolean answer;
private boolean error;
private boolean valid;
private boolean dstSettingsActive;
private boolean gatewayKnown;
private boolean panelLocked;
private boolean linkStatusError;
private Map<String, Object> properties = new HashMap<>();
public Device(DeviceConfiguration c) {
this.serialNumber = c.getSerialNumber();
this.rfAddress = c.getRFAddress();
this.roomId = c.getRoomId();
this.roomName = c.getRoomName();
this.name = c.getName();
this.setProperties(new HashMap<>(c.getProperties()));
}
public abstract DeviceType getType();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static Device create(String rfAddress, List<DeviceConfiguration> configurations) {
Device returnValue = null;
for (DeviceConfiguration c : configurations) {
if (c.getRFAddress().toUpperCase().equals(rfAddress.toUpperCase())) {
return create(c);
}
}
return returnValue;
}
/**
* Creates a new device
*
* @param DeviceConfiguration
* @return Device
*/
public static Device create(DeviceConfiguration c) {
{
switch (c.getDeviceType()) {
case HeatingThermostatPlus:
case HeatingThermostat:
HeatingThermostat thermostat = new HeatingThermostat(c);
thermostat.setType(c.getDeviceType());
return thermostat;
case EcoSwitch:
return new EcoSwitch(c);
case ShutterContact:
return new ShutterContact(c);
case WallMountedThermostat:
return new WallMountedThermostat(c);
case Cube:
return new Cube(c);
default:
return new UnsupportedDevice(c);
}
}
}
public static Device create(byte[] raw, List<DeviceConfiguration> configurations) {
if (raw.length == 0) {
return null;
}
String rfAddress = Utils.toHex(raw[0] & 0xFF, raw[1] & 0xFF, raw[2] & 0xFF);
// Based on the RF address and the corresponding configuration,
// create the device based on the type specified in it's configuration
Device device = Device.create(rfAddress, configurations);
if (device == null) {
LOGGER.debug("Can't create device from received message, returning 'null'.");
return null;
}
return Device.update(raw, configurations, device);
}
public static Device update(byte[] raw, List<DeviceConfiguration> configurations, Device device) {
String rfAddress = device.getRFAddress();
// byte 4 is skipped
// multiple device information are encoded in those particular bytes
boolean[] bits1 = Utils.getBits(Utils.fromByte(raw[4]));
boolean[] bits2 = Utils.getBits(Utils.fromByte(raw[5]));
device.setInitialized(bits1[1]);
device.setAnswer(bits1[2]);
device.setError(bits1[3]);
device.setValid(bits1[4]);
device.setDstSettingActive(bits2[3]);
device.setGatewayKnown(bits2[4]);
device.setPanelLocked(bits2[5]);
device.setLinkStatusError(bits2[6]);
device.setBatteryLow(bits2[7]);
LOGGER.trace("Device {} ({}): L Message length: {} content: {}", rfAddress, device.getType(), raw.length,
Utils.getHex(raw));
// TODO move the device specific readings into the sub classes
switch (device.getType()) {
case WallMountedThermostat:
case HeatingThermostat:
case HeatingThermostatPlus:
HeatingThermostat heatingThermostat = (HeatingThermostat) device;
// "xxxx xx00 = automatic, xxxx xx01 = manual, xxxx xx10 = vacation, xxxx xx11 = boost":
if (!bits2[1] && !bits2[0]) {
heatingThermostat.setMode(ThermostatModeType.AUTOMATIC);
} else if (!bits2[1] && bits2[0]) {
heatingThermostat.setMode(ThermostatModeType.MANUAL);
} else if (bits2[1] && !bits2[0]) {
heatingThermostat.setMode(ThermostatModeType.VACATION);
} else if (bits2[1] && bits2[0]) {
heatingThermostat.setMode(ThermostatModeType.BOOST);
} else {
LOGGER.debug("Device {} ({}): Unknown mode", rfAddress, device.getType());
}
heatingThermostat.setValvePosition(raw[6] & 0xFF);
heatingThermostat.setTemperatureSetpoint(raw[7] & 0x7F);
// 9 2 858B Date until (05-09-2011) (see Encoding/Decoding
// date/time)
// B 1 2E Time until (23:00) (see Encoding/Decoding date/time)
String hexDate = Utils.toHex(raw[8] & 0xFF, raw[9] & 0xFF);
int dateValue = Utils.fromHex(hexDate);
int timeValue = raw[10] & 0xFF;
Date date = Utils.resolveDateTime(dateValue, timeValue);
heatingThermostat.setDateSetpoint(date);
int actualTemp = 0;
if (device.getType() == DeviceType.WallMountedThermostat) {
actualTemp = (raw[11] & 0xFF) + (raw[7] & 0x80) * 2;
} else {
if (heatingThermostat.getMode() != ThermostatModeType.VACATION
&& heatingThermostat.getMode() != ThermostatModeType.BOOST) {
actualTemp = (raw[8] & 0xFF) * 256 + (raw[9] & 0xFF);
} else {
LOGGER.debug("Device {} ({}): No temperature reading in {} mode", rfAddress, device.getType(),
heatingThermostat.getMode());
}
}
LOGGER.debug("Device {} ({}): Actual Temperature : {}", rfAddress, device.getType(),
(double) actualTemp / 10);
heatingThermostat.setTemperatureActual((double) actualTemp / 10);
break;
case EcoSwitch:
String eCoSwitchData = Utils.toHex(raw[3] & 0xFF, raw[4] & 0xFF, raw[5] & 0xFF);
LOGGER.trace("Device {} ({}): Status bytes : {}", rfAddress, device.getType(), eCoSwitchData);
EcoSwitch ecoswitch = (EcoSwitch) device;
// xxxx xx10 = shutter open, xxxx xx00 = shutter closed
if (bits2[1] && !bits2[0]) {
ecoswitch.setEcoMode(OnOffType.ON);
LOGGER.trace("Device {} ({}): status: ON", rfAddress, device.getType());
} else if (!bits2[1] && !bits2[0]) {
ecoswitch.setEcoMode(OnOffType.OFF);
LOGGER.trace("Device {} ({}): Status: OFF", rfAddress, device.getType());
} else {
LOGGER.trace("Device {} ({}): Status switch status Unknown (true-true)", rfAddress,
device.getType());
}
break;
case ShutterContact:
ShutterContact shutterContact = (ShutterContact) device;
// xxxx xx10 = shutter open, xxxx xx00 = shutter closed
if (bits2[1] && !bits2[0]) {
shutterContact.setShutterState(OpenClosedType.OPEN);
LOGGER.debug("Device {} ({}): Status: Open", rfAddress, device.getType());
} else if (!bits2[1] && !bits2[0]) {
shutterContact.setShutterState(OpenClosedType.CLOSED);
LOGGER.debug("Device {} ({}): Status: Closed", rfAddress, device.getType());
} else {
LOGGER.trace("Device {} ({}): Status switch status Unknown (true-true)", rfAddress,
device.getType());
}
break;
default:
LOGGER.debug("Unhandled Device. DataBytes: {}", Utils.getHex(raw));
break;
}
return device;
}
private final void setBatteryLow(boolean batteryLow) {
if (this.batteryLow != batteryLow) {
this.updated = true;
}
this.batteryLow = batteryLow;
}
public final OnOffType getBatteryLow() {
return (this.batteryLow ? OnOffType.ON : OnOffType.OFF);
}
public final String getRFAddress() {
return this.rfAddress;
}
public final void setRFAddress(String rfAddress) {
this.rfAddress = rfAddress;
}
public final int getRoomId() {
return roomId;
}
public final void setRoomId(int roomId) {
this.roomId = roomId;
}
public final String getRoomName() {
return roomName;
}
public final void setRoomName(String roomName) {
this.roomName = roomName;
}
private void setLinkStatusError(boolean linkStatusError) {
if (this.linkStatusError != linkStatusError) {
this.updated = true;
}
this.linkStatusError = linkStatusError;
}
private void setPanelLocked(boolean panelLocked) {
if (this.panelLocked != panelLocked) {
this.updated = true;
}
this.panelLocked = panelLocked;
}
private void setGatewayKnown(boolean gatewayKnown) {
if (this.gatewayKnown != gatewayKnown) {
this.updated = true;
}
this.gatewayKnown = gatewayKnown;
}
private void setDstSettingActive(boolean dstSettingsActive) {
if (this.dstSettingsActive != dstSettingsActive) {
this.updated = true;
}
this.dstSettingsActive = dstSettingsActive;
}
public boolean isDstSettingsActive() {
return dstSettingsActive;
}
private void setValid(boolean valid) {
if (this.valid != valid) {
this.updated = true;
}
this.valid = valid;
}
public void setError(boolean error) {
if (this.error != error) {
this.updated = true;
}
this.error = error;
}
public String getSerialNumber() {
return serialNumber;
}
private void setInitialized(boolean initialized) {
if (this.initialized != initialized) {
this.updated = true;
}
this.initialized = initialized;
}
private void setAnswer(boolean answer) {
if (this.answer != answer) {
this.updated = true;
}
this.answer = answer;
}
public boolean isUpdated() {
return updated;
}
public void setUpdated(boolean updated) {
this.updated = updated;
}
public boolean isInitialized() {
return initialized;
}
public boolean isAnswer() {
return answer;
}
public boolean isError() {
return error;
}
public boolean isValid() {
return valid;
}
public boolean isGatewayKnown() {
return gatewayKnown;
}
public boolean isPanelLocked() {
return panelLocked;
}
public boolean isLinkStatusError() {
return linkStatusError;
}
/**
* @return the properties
*/
public Map<String, Object> getProperties() {
return properties;
}
/**
* @param properties the properties to set
*/
public void setProperties(Map<String, Object> properties) {
this.properties = new HashMap<>(properties);
}
@Override
public String toString() {
return this.getType().toString() + " (" + rfAddress + ") '" + this.getName() + "'";
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import java.util.HashMap;
import java.util.Map;
import org.openhab.binding.max.internal.message.CMessage;
import org.openhab.binding.max.internal.message.Message;
/**
* Base class for configuration provided by the MAX! Cube C Message.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
public final class DeviceConfiguration {
private DeviceType deviceType;
private String rfAddress;
private String serialNumber;
private String name;
private int roomId = -1;
private String roomName;
/** Extended configuration properties **/
private Map<String, Object> properties = new HashMap<>();
private DeviceConfiguration() {
}
public static DeviceConfiguration create(Message message) {
final DeviceConfiguration configuration = new DeviceConfiguration();
configuration.setValues((CMessage) message);
return configuration;
}
public static DeviceConfiguration create(DeviceInformation di) {
DeviceConfiguration configuration = new DeviceConfiguration();
configuration.setValues(di.getRFAddress(), di.getDeviceType(), di.getSerialNumber(), di.getRoomId(),
di.getName());
return configuration;
}
public void setValues(CMessage message) {
setValues(message.getRFAddress(), message.getDeviceType(), message.getSerialNumber(), message.getRoomID());
properties = new HashMap<>(message.getProperties());
}
private void setValues(String rfAddress, DeviceType deviceType, String serialNumber, int roomId, String name) {
setValues(rfAddress, deviceType, serialNumber, roomId);
this.name = name;
}
private void setValues(String rfAddress, DeviceType deviceType, String serialNumber, int roomId) {
this.rfAddress = rfAddress;
this.deviceType = deviceType;
this.serialNumber = serialNumber;
this.roomId = roomId;
}
public String getRFAddress() {
return rfAddress;
}
public DeviceType getDeviceType() {
return deviceType;
}
public String getSerialNumber() {
return serialNumber;
}
public String getName() {
if (name == null) {
return "";
} else {
return name;
}
}
public void setName(String name) {
this.name = name;
}
public int getRoomId() {
return roomId;
}
public void setRoomId(int roomId) {
this.roomId = roomId;
}
public String getRoomName() {
if (roomName == null) {
return "";
} else {
return roomName;
}
}
public void setRoomName(String roomName) {
this.roomName = roomName;
}
/**
* @return the properties
*/
public Map<String, Object> getProperties() {
return properties;
}
/**
* @param properties the properties to set
*/
public void setProperties(HashMap<String, Object> properties) {
this.properties = properties;
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* Device information provided by the M message meta information.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
public class DeviceInformation {
private final DeviceType deviceType;
private final String serialNumber;
private final String rfAddress;
private final String name;
private final int roomId;
public DeviceInformation(DeviceType deviceType, String serialNumber, String rfAddress, String name, int roomId) {
this.deviceType = deviceType;
this.serialNumber = serialNumber;
this.rfAddress = rfAddress;
this.name = name;
this.roomId = roomId;
}
public String getRFAddress() {
return rfAddress;
}
public DeviceType getDeviceType() {
return deviceType;
}
public String getSerialNumber() {
return serialNumber;
}
public int getRoomId() {
return roomId;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* This enumeration represents the different message types provided by the MAX! Cube protocol.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
public enum DeviceType {
Invalid(256),
Cube(0),
HeatingThermostat(1),
HeatingThermostatPlus(2),
WallMountedThermostat(3),
ShutterContact(4),
EcoSwitch(5);
private final int value;
private DeviceType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static DeviceType create(int value) {
switch (value) {
case 0:
return Cube;
case 1:
return HeatingThermostat;
case 2:
return HeatingThermostatPlus;
case 3:
return WallMountedThermostat;
case 4:
return ShutterContact;
case 5:
return EcoSwitch;
default:
return Invalid;
}
}
@Override
public String toString() {
switch (value) {
case 0:
return "Cube";
case 1:
return "Thermostat";
case 2:
return "Thermostat+";
case 3:
return "Wallmounted Thermostat";
case 4:
return "Shutter Contact";
case 5:
return "Eco Switch";
default:
return "Invalid";
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import org.openhab.core.library.types.OnOffType;
/**
* MAX! EcoSwitch.
*
* @author Marcel Verpaalen - Initial contribution
*/
public class EcoSwitch extends ShutterContact {
private OnOffType ecoMode;
public EcoSwitch(DeviceConfiguration c) {
super(c);
}
@Override
public DeviceType getType() {
return DeviceType.EcoSwitch;
}
public OnOffType getEcoMode() {
return ecoMode;
}
public void setEcoMode(OnOffType ecoMode) {
this.ecoMode = ecoMode;
}
}

View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
/**
* MAX! Heating thermostat & Heating thermostat+ .
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*/
public class HeatingThermostat extends Device {
private ThermostatModeType mode;
/** Valve position in % */
private int valvePosition;
/** Temperature setpoint in degrees celcius */
private double temperatureSetpoint;
/** Actual Temperature in degrees celcius */
private double temperatureActual;
/** Date setpoint until the temperature setpoint is valid */
private Date dateSetpoint;
/** Device type for this thermostat **/
private DeviceType deviceType = DeviceType.HeatingThermostat;
/** Date/Time the actual temperature was last updated */
private Date actualTempLastUpdated;
public HeatingThermostat(DeviceConfiguration c) {
super(c);
}
@Override
public DeviceType getType() {
return deviceType;
}
/**
* Sets the DeviceType for this thermostat.
*
* @param DeviceType as provided by the C message
*/
void setType(DeviceType type) {
this.deviceType = type;
}
/**
* Returns the current mode of the thermostat.
*/
public String getModeString() {
return this.mode.toString();
}
/**
* Returns the current mode of the thermostat.
*/
public ThermostatModeType getMode() {
return this.mode;
}
public void setMode(ThermostatModeType mode) {
if (this.mode != mode) {
setUpdated(true);
}
this.mode = mode;
}
/**
* Sets the valve position for this thermostat.
*
* @param valvePosition the valve position as provided by the L message
*/
public void setValvePosition(int valvePosition) {
if (this.valvePosition != valvePosition) {
setUpdated(true);
}
this.valvePosition = valvePosition;
}
/**
* Returns the current valve position of this thermostat in percent.
*
* @return
* the valve position as <code>DecimalType</code>
*/
public int getValvePosition() {
return this.valvePosition;
}
public void setDateSetpoint(Date date) {
this.dateSetpoint = date;
}
public Date getDateSetpoint() {
return dateSetpoint;
}
/**
* Sets the actual temperature for this thermostat.
*
* @param value the actual temperature raw value as provided by the L message
*/
public void setTemperatureActual(double value) {
if (this.temperatureActual != value) {
setUpdated(true);
this.actualTempLastUpdated = Calendar.getInstance().getTime();
}
this.temperatureActual = value;
}
/**
* Returns the measured temperature of this thermostat.
* 0<>C is displayed if no actual is measured. Temperature is only updated after valve position changes
*
* @return
* the actual temperature as <code>QuantityType</code>
*/
public double getTemperatureActual() {
return BigDecimal.valueOf(this.temperatureActual).setScale(1, RoundingMode.HALF_UP).doubleValue();
}
/**
* Sets the setpoint temperature for this thermostat.
*
* @param value the setpoint temperature raw value as provided by the L message
*/
public void setTemperatureSetpoint(int value) {
if (Math.abs(this.temperatureSetpoint - (value / 2.0)) > 0.1) {
setUpdated(true);
}
this.temperatureSetpoint = value / 2.0;
}
/**
* Returns the setpoint temperature of this thermostat.
* 4.5<EFBFBD>C is displayed as OFF, 30.5<EFBFBD>C is displayed as On at the thermostat display.
*
* @return
* the setpoint temperature as <code>QuantityType</code>
*/
public double getTemperatureSetpoint() {
return this.temperatureSetpoint;
}
/**
* @return the Date the actual Temperature was last Updated
*/
public Date getActualTempLastUpdated() {
return actualTempLastUpdated;
}
/**
* @param actualTempLastUpdated the Date the actual Temperature was last Updated
*/
public void setActualTempLastUpdated(Date actualTempLastUpdated) {
this.actualTempLastUpdated = actualTempLastUpdated;
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* Room information provided by the M message meta information.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen (marcel@verpaalen.com) - OH2 update
*/
public class RoomInformation {
private int position;
private String name;
private String rfAddress;
public RoomInformation(int position, String name, String rfAddress) {
this.position = position;
this.name = name;
this.rfAddress = rfAddress;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getRFAddress() {
return rfAddress;
}
public void setRFAddress(String rfAddress) {
this.rfAddress = rfAddress;
}
@Override
public String toString() {
return "Room " + position + " (" + rfAddress + ") ='" + name + "'";
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import org.openhab.core.library.types.OpenClosedType;
/**
* MAX! Shutter contact device.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*/
public class ShutterContact extends Device {
private OpenClosedType shutterState;
public ShutterContact(DeviceConfiguration c) {
super(c);
}
public void setShutterState(OpenClosedType shutterState) {
if (this.shutterState != shutterState) {
setUpdated(true);
}
this.shutterState = shutterState;
}
public OpenClosedType getShutterState() {
return shutterState;
}
@Override
public DeviceType getType() {
return DeviceType.ShutterContact;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
import org.openhab.core.types.Command;
import org.openhab.core.types.PrimitiveType;
import org.openhab.core.types.State;
/**
* This enumeration represents the different mode types of a MAX! heating thermostat.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*/
public enum ThermostatModeType implements PrimitiveType, State, Command {
AUTOMATIC,
MANUAL,
VACATION,
BOOST;
@Override
public String format(String pattern) {
return String.format(pattern, this.toString());
}
@Override
public String toFullString() {
return toString();
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* Unsupported devices.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*/
public class UnsupportedDevice extends Device {
public UnsupportedDevice(DeviceConfiguration c) {
super(c);
}
@Override
public DeviceType getType() {
return DeviceType.Invalid;
}
@Override
public String getName() {
return "Unsupported device";
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.device;
/**
* MAX! wall mounted thermostat.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
public class WallMountedThermostat extends HeatingThermostat {
public WallMountedThermostat(DeviceConfiguration c) {
super(c);
}
@Override
public DeviceType getType() {
return DeviceType.WallMountedThermostat;
}
}

View File

@@ -0,0 +1,220 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.discovery;
import static org.openhab.binding.max.internal.MaxBindingConstants.*;
import static org.openhab.core.thing.Thing.PROPERTY_SERIAL_NUMBER;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.max.internal.Utils;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxCubeBridgeDiscovery} is responsible for discovering new MAX!
* Cube LAN gateway devices on the network
*
* @author Marcel Verpaalen - Initial contribution
*/
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.max")
public class MaxCubeBridgeDiscovery extends AbstractDiscoveryService {
private static final String MAXCUBE_DISCOVER_STRING = "eQ3Max*\0**********I";
private static final int SEARCH_TIME = 15;
private final Logger logger = LoggerFactory.getLogger(MaxCubeBridgeDiscovery.class);
protected static boolean discoveryRunning;
/** The refresh interval for discovery of MAX! Cubes */
private static final long SEARCH_INTERVAL = 600;
private ScheduledFuture<?> cubeDiscoveryJob;
public MaxCubeBridgeDiscovery() {
super(SEARCH_TIME);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return SUPPORTED_BRIDGE_THING_TYPES_UIDS;
}
@Override
public void startScan() {
logger.debug("Start MAX! Cube discovery");
discoverCube();
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stop MAX! Cube background discovery");
if (cubeDiscoveryJob != null && !cubeDiscoveryJob.isCancelled()) {
cubeDiscoveryJob.cancel(true);
cubeDiscoveryJob = null;
}
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Start MAX! Cube background discovery");
if (cubeDiscoveryJob == null || cubeDiscoveryJob.isCancelled()) {
cubeDiscoveryJob = scheduler.scheduleWithFixedDelay(this::discoverCube, 0, SEARCH_INTERVAL,
TimeUnit.SECONDS);
}
}
private synchronized void discoverCube() {
logger.debug("Run MAX! Cube discovery");
sendDiscoveryMessage(MAXCUBE_DISCOVER_STRING);
logger.trace("Done sending broadcast discovery messages.");
receiveDiscoveryMessage();
logger.debug("Done receiving discovery messages.");
}
private void receiveDiscoveryMessage() {
try (final DatagramSocket bcReceipt = new DatagramSocket(23272)) {
discoveryRunning = true;
bcReceipt.setReuseAddress(true);
bcReceipt.setSoTimeout(5000);
while (discoveryRunning) {
// Wait for a response
final byte[] recvBuf = new byte[1500];
final DatagramPacket receivePacket = new DatagramPacket(recvBuf, recvBuf.length);
bcReceipt.receive(receivePacket);
// We have a response
final byte[] messageBuf = Arrays.copyOfRange(receivePacket.getData(), receivePacket.getOffset(),
receivePacket.getOffset() + receivePacket.getLength());
final String message = new String(messageBuf, StandardCharsets.UTF_8);
logger.trace("Broadcast response from {} : {} '{}'", receivePacket.getAddress(), message.length(),
message);
// Check if the message is correct
if (message.startsWith("eQ3Max") && !message.equals(MAXCUBE_DISCOVER_STRING)) {
String maxCubeIP = receivePacket.getAddress().getHostAddress();
String maxCubeState = message.substring(0, 8);
String serialNumber = message.substring(8, 18);
String msgValidid = message.substring(18, 19);
String requestType = message.substring(19, 20);
String rfAddress = "";
logger.debug("MAX! Cube found on network");
logger.debug("Found at : {}", maxCubeIP);
logger.trace("Cube State: {}", maxCubeState);
logger.debug("Serial : {}", serialNumber);
logger.trace("Msg Valid : {}", msgValidid);
logger.trace("Msg Type : {}", requestType);
if (requestType.equals("I")) {
rfAddress = Utils.getHex(Arrays.copyOfRange(messageBuf, 21, 24)).replace(" ", "").toLowerCase();
String firmwareVersion = Utils.getHex(Arrays.copyOfRange(messageBuf, 24, 26)).replace(" ", ".");
logger.debug("RF Address: {}", rfAddress);
logger.debug("Firmware : {}", firmwareVersion);
}
discoveryResultSubmission(maxCubeIP, serialNumber, rfAddress);
}
}
} catch (SocketTimeoutException e) {
logger.trace("No further response");
discoveryRunning = false;
} catch (IOException e) {
logger.debug("IO error during MAX! Cube discovery: {}", e.getMessage());
discoveryRunning = false;
}
}
private void discoveryResultSubmission(String IpAddress, String cubeSerialNumber, String rfAddress) {
if (cubeSerialNumber != null) {
logger.trace("Adding new MAX! Cube Lan Gateway on {} with id '{}' to Smarthome inbox", IpAddress,
cubeSerialNumber);
Map<String, Object> properties = new HashMap<>(2);
properties.put(PROPERTY_IP_ADDRESS, IpAddress);
properties.put(PROPERTY_SERIAL_NUMBER, cubeSerialNumber);
properties.put(PROPERTY_RFADDRESS, rfAddress);
ThingUID uid = new ThingUID(CUBEBRIDGE_THING_TYPE, cubeSerialNumber);
thingDiscovered(DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(PROPERTY_SERIAL_NUMBER).withThingType(CUBEBRIDGE_THING_TYPE)
.withLabel("MAX! Cube LAN Gateway").build());
}
}
/**
* Send broadcast message over all active interfaces
*
* @param discoverString
* String to be used for the discovery
*/
private void sendDiscoveryMessage(String discoverString) {
// Find the MaxCube using UDP broadcast
try (DatagramSocket bcSend = new DatagramSocket()) {
bcSend.setBroadcast(true);
byte[] sendData = discoverString.getBytes(StandardCharsets.UTF_8);
// Broadcast the message over all the network interfaces
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
continue;
}
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
InetAddress[] broadcast = new InetAddress[3];
broadcast[0] = InetAddress.getByName("224.0.0.1");
broadcast[1] = InetAddress.getByName("255.255.255.255");
broadcast[2] = interfaceAddress.getBroadcast();
for (InetAddress bc : broadcast) {
// Send the broadcast package!
if (bc != null) {
try {
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, bc, 23272);
bcSend.send(sendPacket);
} catch (IOException e) {
logger.debug("IO error during MAX! Cube discovery: {}", e.getMessage());
} catch (Exception e) {
logger.debug("{}", e.getMessage(), e);
}
logger.trace("Request packet sent to: {} Interface: {}", bc.getHostAddress(),
networkInterface.getDisplayName());
}
}
}
}
logger.trace("Done looping over all network interfaces. Now waiting for a reply!");
} catch (IOException e) {
logger.debug("IO error during MAX! Cube discovery: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,157 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.discovery;
import java.util.Date;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.MaxBindingConstants;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.handler.DeviceStatusListener;
import org.openhab.binding.max.internal.handler.MaxCubeBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxDeviceDiscoveryService} class is used to discover MAX! Cube
* devices that are connected to the Lan gateway.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MaxDeviceDiscoveryService extends AbstractDiscoveryService
implements DeviceStatusListener, DiscoveryService, ThingHandlerService {
private static final int SEARCH_TIME = 60;
private final Logger logger = LoggerFactory.getLogger(MaxDeviceDiscoveryService.class);
private @Nullable MaxCubeBridgeHandler maxCubeBridgeHandler;
public MaxDeviceDiscoveryService() {
super(MaxBindingConstants.SUPPORTED_DEVICE_THING_TYPES_UIDS, SEARCH_TIME, true);
}
@Override
public void setThingHandler(@NonNullByDefault({}) ThingHandler handler) {
if (handler instanceof MaxCubeBridgeHandler) {
this.maxCubeBridgeHandler = (MaxCubeBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return maxCubeBridgeHandler;
}
@Override
public void activate() {
MaxCubeBridgeHandler localMaxCubeBridgeHandler = maxCubeBridgeHandler;
if (localMaxCubeBridgeHandler != null) {
localMaxCubeBridgeHandler.registerDeviceStatusListener(this);
}
}
@Override
public void deactivate() {
MaxCubeBridgeHandler localMaxCubeBridgeHandler = maxCubeBridgeHandler;
if (localMaxCubeBridgeHandler != null) {
localMaxCubeBridgeHandler.unregisterDeviceStatusListener(this);
removeOlderResults(new Date().getTime(), localMaxCubeBridgeHandler.getThing().getUID());
}
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return MaxBindingConstants.SUPPORTED_DEVICE_THING_TYPES_UIDS;
}
@Override
public void onDeviceAdded(Bridge bridge, Device device) {
logger.trace("Adding new MAX! {} with id '{}' to smarthome inbox", device.getType(), device.getSerialNumber());
ThingUID thingUID = null;
switch (device.getType()) {
case WallMountedThermostat:
thingUID = new ThingUID(MaxBindingConstants.WALLTHERMOSTAT_THING_TYPE, bridge.getUID(),
device.getSerialNumber());
break;
case HeatingThermostat:
thingUID = new ThingUID(MaxBindingConstants.HEATINGTHERMOSTAT_THING_TYPE, bridge.getUID(),
device.getSerialNumber());
break;
case HeatingThermostatPlus:
thingUID = new ThingUID(MaxBindingConstants.HEATINGTHERMOSTATPLUS_THING_TYPE, bridge.getUID(),
device.getSerialNumber());
break;
case ShutterContact:
thingUID = new ThingUID(MaxBindingConstants.SHUTTERCONTACT_THING_TYPE, bridge.getUID(),
device.getSerialNumber());
break;
case EcoSwitch:
thingUID = new ThingUID(MaxBindingConstants.ECOSWITCH_THING_TYPE, bridge.getUID(),
device.getSerialNumber());
break;
default:
break;
}
if (thingUID != null) {
String name = device.getName();
if (name.isEmpty()) {
name = device.getSerialNumber();
}
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withProperty(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber()).withBridge(bridge.getUID())
.withLabel(device.getType() + ": " + name).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER)
.build();
thingDiscovered(discoveryResult);
} else {
logger.debug("Discovered MAX! device is unsupported: type '{}' with id '{}'", device.getType(),
device.getSerialNumber());
}
}
@Override
protected void startScan() {
MaxCubeBridgeHandler localMaxCubeBridgeHandler = maxCubeBridgeHandler;
if (localMaxCubeBridgeHandler != null) {
localMaxCubeBridgeHandler.clearDeviceList();
localMaxCubeBridgeHandler.deviceInclusion();
}
}
@Override
public void onDeviceStateChanged(ThingUID bridge, Device device) {
// this can be ignored here
}
@Override
public void onDeviceRemoved(MaxCubeBridgeHandler bridge, Device device) {
// this can be ignored here
}
@Override
public void onDeviceConfigUpdate(Bridge bridge, Device device) {
// this can be ignored here
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to put a new message line into the message processor,
* but the processor is currently processing an other message type.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class IncompleteMessageException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -8882867114144637120L;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to put a new message line into the message processor,
* but the processor is currently processing an other message type.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class IncorrectMultilineIndexException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = 3102159039702650238L;
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to put a new message line into the message processor,
* but the processor is not yet ready to handle new lines because there is already a message that
* has be pulled before.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class MessageIsWaitingException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -7317329978634583853L;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to pull a message from the message processor,
* but the processor does not yet have a complete message.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class NoMessageAvailableException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -7663390696233390452L;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to put a new message line into the message processor,
* the processor detects a known message indicator, but the message could not be parsed correctly.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class UnprocessableMessageException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -9071779402960309265L;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.exceptions;
/**
* Will be thrown when there is an attempt to put a new message line into the message processor,
* but the line starts with an unknown message indicator.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
public class UnsupportedMessageTypeException extends Exception {
/**
* required variable to avoid IncorrectMultilineIndexException warning
*/
private static final long serialVersionUID = -5163044407682700913L;
}

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.factory;
import static org.openhab.binding.max.internal.MaxBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.handler.MaxCubeBridgeHandler;
import org.openhab.binding.max.internal.handler.MaxDevicesHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxCubeHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.max")
public class MaxCubeHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(MaxCubeHandlerFactory.class);
@Override
public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
if (CUBEBRIDGE_THING_TYPE.equals(thingTypeUID)) {
ThingUID cubeBridgeUID = getBridgeThingUID(thingTypeUID, thingUID, configuration);
return super.createThing(thingTypeUID, configuration, cubeBridgeUID, null);
}
if (supportsThingType(thingTypeUID) && bridgeUID != null) {
ThingUID deviceUID = getMaxCubeDeviceUID(thingTypeUID, thingUID, configuration, bridgeUID);
return super.createThing(thingTypeUID, configuration, deviceUID, bridgeUID);
}
throw new IllegalArgumentException("The thing type " + thingTypeUID + " is not supported by the binding.");
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
private ThingUID getBridgeThingUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
Configuration configuration) {
if (thingUID == null) {
String serialNumber = (String) configuration.get(Thing.PROPERTY_SERIAL_NUMBER);
return new ThingUID(thingTypeUID, serialNumber);
}
return thingUID;
}
private ThingUID getMaxCubeDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
Configuration configuration, ThingUID bridgeUID) {
if (thingUID == null) {
String serialNumber = (String) configuration.get(Thing.PROPERTY_SERIAL_NUMBER);
return new ThingUID(thingTypeUID, serialNumber, bridgeUID.getId());
}
return thingUID;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (CUBEBRIDGE_THING_TYPE.equals(thingTypeUID)) {
return new MaxCubeBridgeHandler((Bridge) thing);
} else if (SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new MaxDevicesHandler(thing);
} else {
logger.debug("ThingHandler not found for {}", thingTypeUID);
return null;
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
/**
* The {@link DeviceStatusListener} is notified when a device status has changed
* or a device has been removed or added.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public interface DeviceStatusListener {
/**
* This method is called whenever the state of the given device has changed.
*
* @param bridge The MAX! Cube bridge the changed device is connected to
* @param device The device which received the state update
*/
void onDeviceStateChanged(ThingUID bridge, Device device);
/**
* This method is called whenever a device is removed.
*
* @param bridge The MAX! Cube bridge the removed device was connected to
* @param device The device which is removed
*/
void onDeviceRemoved(MaxCubeBridgeHandler bridge, Device device);
/**
* This method is called whenever a device is added.
*
* @param bridge The MAX! Cube bridge the added device was connected to
* @param device The device which is added
*/
void onDeviceAdded(Bridge bridge, Device device);
/**
* This method is called whenever a device config is updated.
*
* @param bridgeThe MAX! Cube bridge the device was connected to
* @param device The device which config is changed
*/
void onDeviceConfigUpdate(Bridge bridge, Device device);
}

View File

@@ -0,0 +1,580 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.handler;
import static org.openhab.binding.max.internal.MaxBindingConstants.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.max.actions.MaxDevicesActions;
import org.openhab.binding.max.internal.command.CCommand;
import org.openhab.binding.max.internal.command.QCommand;
import org.openhab.binding.max.internal.command.SConfigCommand;
import org.openhab.binding.max.internal.command.SConfigCommand.ConfigCommandType;
import org.openhab.binding.max.internal.command.ZCommand;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.device.DeviceType;
import org.openhab.binding.max.internal.device.EcoSwitch;
import org.openhab.binding.max.internal.device.HeatingThermostat;
import org.openhab.binding.max.internal.device.ShutterContact;
import org.openhab.binding.max.internal.device.ThermostatModeType;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MaxDevicesHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Marcel Verpaalen - Initial contribution
*/
public class MaxDevicesHandler extends BaseThingHandler implements DeviceStatusListener {
private final Logger logger = LoggerFactory.getLogger(MaxDevicesHandler.class);
private MaxCubeBridgeHandler bridgeHandler;
private String maxDeviceSerial;
private String rfAddress;
private boolean propertiesSet;
private boolean configSet;
// actual refresh variables
public static final int REFRESH_ACTUAL_MIN_RATE = 10; // minutes
public static final int REFRESH_ACTUAL_DURATION = 120; // seconds
private static final long COMMUNICATION_DELAY_TIME = 120;
private int refreshActualRate;
private boolean refreshingActuals;
private ScheduledFuture<?> refreshActualsJob;
private double originalSetTemp;
private ThermostatModeType originalMode;
public MaxDevicesHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
try {
final Configuration config = getThing().getConfiguration();
final String configDeviceId = (String) config.get(Thing.PROPERTY_SERIAL_NUMBER);
try {
refreshActualRate = ((BigDecimal) config.get(PROPERTY_REFRESH_ACTUAL_RATE)).intValueExact();
} catch (Exception e) {
refreshActualRate = 0;
}
if (configDeviceId != null) {
maxDeviceSerial = configDeviceId;
}
if (maxDeviceSerial != null) {
logger.debug("Initialized MAX! device handler for {}.", maxDeviceSerial);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Initialized MAX! device missing serialNumber configuration");
}
propertiesSet = false;
configSet = false;
getMaxCubeBridgeHandler();
} catch (Exception e) {
logger.debug("Exception occurred during initialize : {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
@Override
public void dispose() {
logger.debug("Disposing MAX! device {} {}.", getThing().getUID(), maxDeviceSerial);
if (refreshingActuals) {
refreshActualsRestore();
}
if (refreshActualsJob != null && !refreshActualsJob.isCancelled()) {
refreshActualsJob.cancel(true);
refreshActualsJob = null;
}
if (bridgeHandler != null) {
logger.trace("Clear MAX! device {} {} from bridge.", getThing().getUID(), maxDeviceSerial);
bridgeHandler.clearDeviceList();
bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
}
logger.debug("Disposed MAX! device {} {}.", getThing().getUID(), maxDeviceSerial);
super.dispose();
}
@Override
public void thingUpdated(Thing thing) {
configSet = false;
super.thingUpdated(thing);
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
logger.debug("MAX! Device {}: Configuration update received", getThing().getUID());
boolean temperaturePropertyUpdateNeeded = false;
final Device device = getMaxCubeBridgeHandler().getDevice(maxDeviceSerial);
final Map<String, Object> deviceProperties = device == null ? new HashMap<>() : device.getProperties();
final Configuration configuration = editConfiguration();
for (final Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
logger.debug("MAX! Device {}: Configuration update {} to {}", getThing().getUID(),
configurationParameter.getKey(), configurationParameter.getValue());
// Test if it is a part of the configuration properties.
// As paperUI sends all parameters as changed, we need to determine which ones really changed.
if (deviceProperties.containsKey(configurationParameter.getKey())) {
if (deviceProperties.get(configurationParameter.getKey()).equals(configurationParameter.getValue())) {
logger.trace("Device {} Property {} value {} unchanged.", getThing().getUID(),
configurationParameter.getKey(), configurationParameter.getValue());
} else if (configurationParameter.getValue().getClass() == BigDecimal.class
&& ((BigDecimal) deviceProperties.get(configurationParameter.getKey()))
.compareTo((BigDecimal) configurationParameter.getValue()) == 0) {
logger.trace("Device {} Property {} value {} unchanged.", getThing().getUID(),
configurationParameter.getKey(), configurationParameter.getValue());
} else {
logger.debug("Device {} Property {} value {} -> {} changed.", getThing().getUID(),
configurationParameter.getKey(), deviceProperties.get(configurationParameter.getKey()),
configurationParameter.getValue());
temperaturePropertyUpdateNeeded = true;
}
}
if (configurationParameter.getKey().equals(PROPERTY_DEVICENAME)
|| configurationParameter.getKey().equals(PROPERTY_ROOMID)) {
updateDeviceName(configurationParameter);
}
if (configurationParameter.getKey().startsWith("action-")) {
if (configurationParameter.getValue().toString().equals(BUTTON_ACTION_VALUE)) {
configurationParameter.setValue(BigDecimal.valueOf(BUTTON_NOACTION_VALUE));
if (configurationParameter.getKey().equals(ACTION_DEVICE_DELETE)) {
deviceDelete();
}
}
}
configuration.put(configurationParameter.getKey(), configurationParameter.getValue());
}
// Persist changes and restart with new parameters
updateConfiguration(configuration);
if (temperaturePropertyUpdateNeeded) {
sendPropertyUpdate(configurationParameters, deviceProperties);
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(MaxDevicesActions.class);
}
private void sendPropertyUpdate(Map<String, Object> configurationParameters, Map<String, Object> deviceProperties) {
if (getMaxCubeBridgeHandler() == null) {
logger.debug("MAX! Cube LAN gateway bridge handler not found. Cannot handle update without bridge.");
return;
}
try {
Device device = getMaxCubeBridgeHandler().getDevice(maxDeviceSerial);
rfAddress = device.getRFAddress();
int roomId = device.getRoomId();
BigDecimal tempComfort = (BigDecimal) configurationParameters.getOrDefault(PROPERTY_THERMO_COMFORT_TEMP,
deviceProperties.get(PROPERTY_THERMO_COMFORT_TEMP));
BigDecimal tempEco = (BigDecimal) configurationParameters.getOrDefault(PROPERTY_THERMO_ECO_TEMP,
deviceProperties.get(PROPERTY_THERMO_ECO_TEMP));
BigDecimal tempSetpointMax = (BigDecimal) configurationParameters.getOrDefault(
PROPERTY_THERMO_MAX_TEMP_SETPOINT, deviceProperties.get(PROPERTY_THERMO_MAX_TEMP_SETPOINT));
BigDecimal tempSetpointMin = (BigDecimal) configurationParameters.getOrDefault(
PROPERTY_THERMO_MIN_TEMP_SETPOINT, deviceProperties.get(PROPERTY_THERMO_MIN_TEMP_SETPOINT));
BigDecimal tempOffset = (BigDecimal) configurationParameters.getOrDefault(PROPERTY_THERMO_OFFSET_TEMP,
deviceProperties.get(PROPERTY_THERMO_OFFSET_TEMP));
BigDecimal tempOpenWindow = (BigDecimal) configurationParameters.getOrDefault(
PROPERTY_THERMO_WINDOW_OPEN_TEMP, deviceProperties.get(PROPERTY_THERMO_WINDOW_OPEN_TEMP));
BigDecimal durationOpenWindow = (BigDecimal) configurationParameters.getOrDefault(
PROPERTY_THERMO_WINDOW_OPEN_DURATION, deviceProperties.get(PROPERTY_THERMO_WINDOW_OPEN_DURATION));
SConfigCommand cmd = new SConfigCommand(rfAddress, roomId, tempComfort.doubleValue(), tempEco.doubleValue(),
tempSetpointMax.doubleValue(), tempSetpointMin.doubleValue(), tempOffset.doubleValue(),
tempOpenWindow.doubleValue(), durationOpenWindow.intValue());
bridgeHandler.queueCommand(new SendCommand(maxDeviceSerial, cmd, "Update Thermostat Properties"));
sendCCommand();
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
}
}
/**
* Trigger update by sending C command.
* This command is delayed as it takes time to have the updates back from the thermostat
*/
private void sendCCommand() {
scheduler.schedule(() -> {
CCommand cmd = new CCommand(rfAddress);
bridgeHandler.queueCommand(new SendCommand(maxDeviceSerial, cmd, "Refresh Thermostat Properties"));
configSet = false;
}, COMMUNICATION_DELAY_TIME, TimeUnit.SECONDS);
}
/**
* sends the T command to the Cube to disassociate the device from the MAX! Cube.
*/
public void deviceDelete() {
MaxCubeBridgeHandler maxCubeBridge = getMaxCubeBridgeHandler();
if (maxCubeBridge != null) {
maxCubeBridge.sendDeviceDelete(maxDeviceSerial);
dispose();
}
}
/**
* Updates the device & roomname
*/
private void updateDeviceName(Entry<String, Object> configurationParameter) {
try {
final Device device = getMaxCubeBridgeHandler().getDevice(maxDeviceSerial);
if (device == null) {
logger.debug("MAX! Cube LAN gateway bridge handler not found. Cannot handle update without bridge.");
return;
}
switch (configurationParameter.getKey()) {
case PROPERTY_DEVICENAME:
final String name = configurationParameter.getValue().toString();
if (!name.equals(device.getName())) {
logger.debug("Updating device name for {} to {}", getThing().getUID(), name);
device.setName(name);
bridgeHandler.sendDeviceAndRoomNameUpdate(name);
bridgeHandler.queueCommand(new SendCommand(maxDeviceSerial, new QCommand(), "Reload Data"));
}
break;
case PROPERTY_ROOMID: // fall-through
case PROPERTY_ROOMNAME:
final int roomId = ((BigDecimal) configurationParameter.getValue()).intValue();
if (roomId != device.getRoomId()) {
logger.debug("Updating room for {} to {}", getThing().getUID().getAsString(), roomId);
device.setRoomId(roomId);
// TODO: handle if a room has no more devices, probably should be deleted. Also handle if room
// rfId
// is no longer valid as the related device is movd to another room
bridgeHandler.sendDeviceAndRoomNameUpdate(Integer.toString(roomId));
SendCommand sendCommand = new SendCommand(maxDeviceSerial,
ZCommand.wakeupDevice(device.getRFAddress()),
"WakeUp device" + getThing().getUID().getAsString());
bridgeHandler.queueCommand(sendCommand);
sendCommand = new SendCommand(maxDeviceSerial,
new SConfigCommand(device.getRFAddress(), roomId, ConfigCommandType.SetRoom),
"Set Room");
bridgeHandler.queueCommand(sendCommand);
sendCommand = new SendCommand(maxDeviceSerial, new QCommand(), "Reload Data");
bridgeHandler.queueCommand(sendCommand);
sendCCommand();
}
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
}
}
private synchronized MaxCubeBridgeHandler getMaxCubeBridgeHandler() {
if (this.bridgeHandler == null) {
final Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Required bridge not defined for device {}.", maxDeviceSerial);
return null;
}
final ThingHandler handler = bridge.getHandler();
if (!(handler instanceof MaxCubeBridgeHandler)) {
logger.debug("No available bridge handler found for {} bridge {} .", maxDeviceSerial, bridge.getUID());
return null;
}
this.bridgeHandler = (MaxCubeBridgeHandler) handler;
this.bridgeHandler.registerDeviceStatusListener(this);
}
return this.bridgeHandler;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
final MaxCubeBridgeHandler maxCubeBridge = getMaxCubeBridgeHandler();
if (maxCubeBridge == null) {
logger.debug("MAX! Cube LAN gateway bridge handler not found. Cannot handle command without bridge.");
return;
}
if (command instanceof RefreshType) {
maxCubeBridge.handleCommand(channelUID, command);
return;
}
if (maxDeviceSerial == null) {
logger.warn("Serial number missing. Can't send command to device '{}'", getThing());
return;
}
switch (channelUID.getId()) {
case CHANNEL_SETTEMP:
if (refreshingActuals) {
refreshActualsRestore();
}
maxCubeBridge.queueCommand(new SendCommand(maxDeviceSerial, channelUID, command));
break;
case CHANNEL_MODE:
if (refreshingActuals) {
refreshActualsRestore();
}
maxCubeBridge.queueCommand(new SendCommand(maxDeviceSerial, channelUID, command));
break;
default:
logger.warn("Setting of channel '{}' not possible, channel is read-only.", channelUID);
break;
}
}
@Override
public void onDeviceStateChanged(ThingUID bridge, Device device) {
if (!device.getSerialNumber().equals(maxDeviceSerial)) {
return;
}
if (device.isError() || device.isLinkStatusError()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
} else if (!refreshingActuals) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Updating Actual Temperature");
}
if (!propertiesSet) {
setProperties(device);
}
if (!configSet) {
setDeviceConfiguration(device);
}
if (refreshActualRate >= REFRESH_ACTUAL_MIN_RATE && (device.getType() == DeviceType.HeatingThermostat
|| device.getType() == DeviceType.HeatingThermostatPlus)) {
refreshActualCheck((HeatingThermostat) device);
}
logger.debug("Updating states of {} {} ({}) id: {}", device.getType(), device.getName(),
device.getSerialNumber(), getThing().getUID());
switch (device.getType()) {
case WallMountedThermostat: // fall-through
case HeatingThermostat: // fall-through
case HeatingThermostatPlus:
updateState(new ChannelUID(getThing().getUID(), CHANNEL_LOCKED),
((HeatingThermostat) device).isPanelLocked() ? OpenClosedType.CLOSED : OpenClosedType.OPEN);
updateState(new ChannelUID(getThing().getUID(), CHANNEL_SETTEMP),
new QuantityType<>(((HeatingThermostat) device).getTemperatureSetpoint(), CELSIUS));
updateState(new ChannelUID(getThing().getUID(), CHANNEL_MODE),
new StringType(((HeatingThermostat) device).getModeString()));
updateState(new ChannelUID(getThing().getUID(), CHANNEL_BATTERY),
((HeatingThermostat) device).getBatteryLow());
updateState(new ChannelUID(getThing().getUID(), CHANNEL_VALVE),
new DecimalType(((HeatingThermostat) device).getValvePosition()));
double actualTemp = ((HeatingThermostat) device).getTemperatureActual();
if (actualTemp != 0) {
updateState(new ChannelUID(getThing().getUID(), CHANNEL_ACTUALTEMP),
new QuantityType<>(actualTemp, CELSIUS));
}
break;
case ShutterContact:
updateState(new ChannelUID(getThing().getUID(), CHANNEL_CONTACT_STATE),
((ShutterContact) device).getShutterState());
updateState(new ChannelUID(getThing().getUID(), CHANNEL_BATTERY),
((ShutterContact) device).getBatteryLow());
break;
case EcoSwitch:
updateState(new ChannelUID(getThing().getUID(), CHANNEL_BATTERY), ((EcoSwitch) device).getBatteryLow());
break;
default:
logger.debug("Unhandled Device {}.", device.getType());
break;
}
device.setUpdated(false);
}
private void refreshActualCheck(HeatingThermostat device) {
if (device.getActualTempLastUpdated() == null) {
Calendar t = Calendar.getInstance();
t.add(Calendar.MINUTE, REFRESH_ACTUAL_MIN_RATE * -1);
device.setActualTempLastUpdated(t.getTime());
logger.debug("Actual date reset for {} {} ({}) id: {}", device.getType(), device.getName(),
device.getSerialNumber(), getThing().getUID());
}
long timediff = Calendar.getInstance().getTime().getTime() - device.getActualTempLastUpdated().getTime();
if (timediff > ((long) refreshActualRate) * 1000 * 60) {
if (!refreshingActuals) {
logger.debug("Actual needs updating for {} {} ({}) id: {}", device.getType(), device.getName(),
device.getSerialNumber(), getThing().getUID());
originalSetTemp = device.getTemperatureSetpoint();
originalMode = device.getMode();
if (originalMode == ThermostatModeType.MANUAL || originalMode == ThermostatModeType.AUTOMATIC) {
double tempSetTemp = originalSetTemp + 0.5;
logger.debug("Actuals Refresh: Setting Temp {}", tempSetTemp);
handleCommand(new ChannelUID(getThing().getUID(), CHANNEL_SETTEMP),
new QuantityType<>(tempSetTemp, CELSIUS));
refreshingActuals = true;
} else {
logger.debug("Defer Actuals refresh. Only manual refresh for mode AUTOMATIC & MANUAL");
device.setActualTempLastUpdated(Calendar.getInstance().getTime());
}
if (refreshingActuals) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "Updating Actual Temperature");
if (refreshActualsJob == null || refreshActualsJob.isCancelled()) {
refreshActualsJob = scheduler.schedule(this::refreshActualsRestore, REFRESH_ACTUAL_DURATION,
TimeUnit.SECONDS);
}
device.setActualTempLastUpdated(Calendar.getInstance().getTime());
}
}
logger.debug("Actual Refresh in progress for {} {} ({}) id: {}", device.getType(), device.getName(),
device.getSerialNumber(), getThing().getUID());
} else {
if (logger.isTraceEnabled()) {
final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
logger.trace("Actual date for {} {} ({}) : {}", device.getType(), device.getName(),
device.getSerialNumber(), dateFormat.format(device.getActualTempLastUpdated().getTime()));
}
}
}
/**
* Send the commands to restore the original settings for mode & temperature
* to end the automatic update cycle
*/
private synchronized void refreshActualsRestore() {
try {
refreshingActuals = false;
if (originalMode == ThermostatModeType.AUTOMATIC || originalMode == ThermostatModeType.MANUAL) {
logger.debug("Finished Actuals Refresh: Restoring Temp {}", originalSetTemp);
handleCommand(new ChannelUID(getThing().getUID(), CHANNEL_SETTEMP),
new QuantityType<>(originalSetTemp, CELSIUS));
}
if (refreshActualsJob != null && !refreshActualsJob.isCancelled()) {
refreshActualsJob.cancel(true);
refreshActualsJob = null;
}
} catch (Exception e) {
logger.debug("Exception occurred during Actuals Refresh : {}", e.getMessage(), e);
}
}
@Override
public void onDeviceRemoved(MaxCubeBridgeHandler bridge, Device device) {
if (device.getSerialNumber().equals(maxDeviceSerial)) {
bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void onDeviceAdded(Bridge bridge, Device device) {
}
/**
* Set the properties for this device
*/
private void setProperties(Device device) {
try {
logger.debug("MAX! {} {} properties update", device.getType(), device.getSerialNumber());
Map<String, String> properties = editProperties();
properties.put(Thing.PROPERTY_MODEL_ID, device.getType().toString());
properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber());
properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
updateProperties(properties);
logger.debug("properties updated");
propertiesSet = true;
} catch (Exception e) {
logger.debug("Exception occurred during property edit: {}", e.getMessage(), e);
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("Bridge Status updated to {} for device: {}", bridgeStatusInfo.getStatus(), getThing().getUID());
if (!bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
}
/**
* Set the Configurable properties for this device
*/
private void setDeviceConfiguration(Device device) {
try {
boolean config_changed = false;
logger.debug("MAX! {} {} configuration update", device.getType(), device.getSerialNumber());
Configuration configuration = editConfiguration();
if (!device.getRoomName().equalsIgnoreCase((String) getConfig().get(PROPERTY_ROOMNAME))) {
configuration.put(PROPERTY_ROOMNAME, device.getRoomName());
config_changed = true;
}
if (getConfig().get(PROPERTY_ROOMID) == null || !(new BigDecimal(device.getRoomId())
.compareTo((BigDecimal) getConfig().get(PROPERTY_ROOMID)) == 0)) {
configuration.put(PROPERTY_ROOMID, new BigDecimal(device.getRoomId()));
config_changed = true;
}
if (!device.getName().equalsIgnoreCase((String) getConfig().get(PROPERTY_DEVICENAME))) {
configuration.put(PROPERTY_DEVICENAME, device.getName());
config_changed = true;
}
if (!device.getRFAddress().equalsIgnoreCase((String) getConfig().get(PROPERTY_RFADDRESS))) {
configuration.put(PROPERTY_RFADDRESS, device.getRFAddress());
config_changed = true;
}
for (Map.Entry<String, Object> entry : device.getProperties().entrySet()) {
configuration.put(entry.getKey(), entry.getValue());
}
if (config_changed) {
updateConfiguration(configuration);
logger.debug("Config updated: {}", configuration.getProperties());
} else {
logger.debug("MAX! {} {} no updated required.", device.getType(), device.getSerialNumber());
}
configSet = true;
} catch (Exception e) {
logger.debug("Exception occurred during configuration edit: {}", e.getMessage(), e);
}
}
@Override
public void onDeviceConfigUpdate(Bridge bridge, Device device) {
if (device.getSerialNumber().equals(maxDeviceSerial)) {
setDeviceConfiguration(device);
}
}
}

View File

@@ -0,0 +1,136 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.command.CubeCommand;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
/**
* Class for sending a command.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public final class SendCommand {
private int id;
private static int commandId = -1;
private @Nullable ChannelUID channelUID;
private @Nullable Command command;
private @Nullable CubeCommand cubeCommand;
private String serialNumber;
private String key;
private String commandText;
public SendCommand(String serialNumber, ChannelUID channelUID, Command command) {
commandId++;
id = commandId;
this.serialNumber = serialNumber;
this.channelUID = channelUID;
this.command = command;
key = getKey(serialNumber, channelUID);
this.commandText = command.toString();
}
public SendCommand(String serialNumber, CubeCommand cubeCommand, String commandText) {
commandId++;
id = commandId;
this.serialNumber = serialNumber;
this.cubeCommand = cubeCommand;
key = getKey(serialNumber, cubeCommand);
this.commandText = commandText;
}
/**
* Get the key based on the serial and channel
* This is can be used to find duplicated commands in the queue
*/
private static String getKey(String serialNumber, ChannelUID channelUID) {
return serialNumber + "-" + channelUID.getId();
}
/**
* Get the key based on the serial and channel
* This is can be used to find duplicated commands in the queue
*/
private static String getKey(String serialNumber, CubeCommand cubeCommand) {
String key = serialNumber + "-" + cubeCommand.getClass().getSimpleName();
return key;
}
/**
* @return the key based on the serial and channel
* This is can be used to find duplicated commands in the queue
*/
public String getKey() {
return key;
}
public int getId() {
return id;
}
public @Nullable ChannelUID getChannelUID() {
return channelUID;
}
public void setChannelUID(ChannelUID channelUID) {
this.channelUID = channelUID;
key = getKey(serialNumber, channelUID);
}
public @Nullable Command getCommand() {
return command;
}
public void setCommand(Command command) {
this.command = command;
}
public @Nullable CubeCommand getCubeCommand() {
return cubeCommand;
}
public String getDeviceSerial() {
return serialNumber;
}
public void setDeviceSerial(String device) {
this.serialNumber = device;
final ChannelUID channelUID = this.channelUID;
if (channelUID != null) {
key = getKey(serialNumber, channelUID);
}
}
public String getCommandText() {
return commandText;
}
public void setCommandText(String commandText) {
this.commandText = commandText;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
return sb.append("id: ").append(id).append(", channelUID: ").append(channelUID).append(", command: ")
.append(command).append(", cubeCommand: ").append(cubeCommand).append(", serialNumber: ")
.append(serialNumber).append(", key: ").append(key).append(", commandText: ").append(commandText)
.toString();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
/**
* The {@link AMessage} Acknowledge the execution of a command
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public final class AMessage extends Message {
public AMessage(String raw) {
super(raw);
}
@Override
public void debug(Logger logger) {
logger.trace("=== A Message === ");
logger.trace("\tRAW : {}", this.getPayload());
logger.debug("Cube Command Acknowledged");
}
@Override
public MessageType getType() {
return MessageType.A;
}
}

View File

@@ -0,0 +1,291 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.openhab.binding.max.internal.MaxBindingConstants.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.DeviceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CMessage} contains configuration about a MAX! device.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - Detailed parsing, OH2 Update
*/
@NonNullByDefault
public final class CMessage extends Message {
private final Logger logger = LoggerFactory.getLogger(CMessage.class);
private String rfAddress;
private int length;
private DeviceType deviceType;
private int roomId = -1;
private String serialNumber;
private BigDecimal tempComfort = BigDecimal.ZERO;
private BigDecimal tempEco = BigDecimal.ZERO;
private BigDecimal tempSetpointMax = BigDecimal.ZERO;
private BigDecimal tempSetpointMin = BigDecimal.ZERO;
private BigDecimal tempOffset = BigDecimal.ZERO;
private BigDecimal tempOpenWindow = BigDecimal.ZERO;
private BigDecimal durationOpenWindow = BigDecimal.ZERO;
private BigDecimal decalcification = BigDecimal.ZERO;
private BigDecimal valveMaximum = BigDecimal.ZERO;
private BigDecimal valveOffset = BigDecimal.ZERO;
private BigDecimal boostDuration = BigDecimal.ZERO;
private BigDecimal boostValve = BigDecimal.ZERO;
private String programData = "";
private Map<String, Object> properties = new HashMap<>();
public CMessage(String raw) {
super(raw);
String[] tokens = this.getPayload().split(Message.DELIMETER);
rfAddress = tokens[0];
byte[] bytes = Base64.getDecoder().decode(tokens[1].getBytes(StandardCharsets.UTF_8));
int[] data = new int[bytes.length];
for (int i = 0; i < bytes.length; i++) {
data[i] = bytes[i] & 0xFF;
}
length = data[0];
if (length != data.length - 1) {
logger.debug("C Message malformed: wrong data length. Expected bytes {}, actual bytes {}", length,
data.length - 1);
}
String rfAddress2 = Utils.toHex(data[1], data[2], data[3]);
if (!rfAddress.toUpperCase().equals(rfAddress2.toUpperCase())) {
logger.debug("C Message malformed: wrong RF address. Expected address {}, actual address {}",
rfAddress.toUpperCase(), rfAddress2.toUpperCase());
}
deviceType = DeviceType.create(data[4]);
roomId = data[5] & 0xFF;
serialNumber = getSerialNumber(bytes);
if (deviceType == DeviceType.HeatingThermostatPlus || deviceType == DeviceType.HeatingThermostat
|| deviceType == DeviceType.WallMountedThermostat) {
parseHeatingThermostatData(bytes);
}
if (deviceType == DeviceType.Cube) {
parseCubeData(bytes);
}
if (deviceType == DeviceType.EcoSwitch || deviceType == DeviceType.ShutterContact) {
logger.trace("Device {} type {} Data: '{}'", rfAddress, deviceType, parseData(bytes));
}
}
private String getSerialNumber(byte[] bytes) {
byte[] sn = new byte[10];
for (int i = 0; i < 10; i++) {
sn[i] = bytes[i + 8];
}
return new String(sn, StandardCharsets.UTF_8);
}
private String parseData(byte[] bytes) {
if (bytes.length <= 18) {
return "";
}
try {
int dataStart = 18;
byte[] sn = new byte[bytes.length - dataStart];
for (int i = 0; i < sn.length; i++) {
sn[i] = bytes[i + dataStart];
}
logger.trace("DataBytes: {}", Utils.getHex(sn));
return new String(sn, StandardCharsets.UTF_8);
} catch (Exception e) {
logger.debug("Exception occurred during parsing: {}", e.getMessage(), e);
}
return "";
}
private void parseCubeData(byte[] bytes) {
if (bytes.length != 238) {
logger.debug("Unexpected lenght for Cube message {}, expected: 238", bytes.length);
}
try {
properties.put("Portal Enabled", Integer.toString(bytes[0x18] & 0xFF));
properties.put("Portal URL",
new String(Arrays.copyOfRange(bytes, 0x55, 0xD5), StandardCharsets.UTF_8).trim());
properties.put("TimeZone (Winter)",
new String(Arrays.copyOfRange(bytes, 0xD6, 0xDA), StandardCharsets.UTF_8).trim());
properties.put("TimeZone (Daylight)",
new String(Arrays.copyOfRange(bytes, 0x00E2, 0x00E6), StandardCharsets.UTF_8).trim());
properties.put("Unknown1", Utils.getHex(Arrays.copyOfRange(bytes, 0x13, 0x33))); // Pushbutton Up config
// 0=auto, 1=eco, 2=comfort
properties.put("Unknown2", Utils.getHex(Arrays.copyOfRange(bytes, 0x34, 0x54))); // Pushbutton down config
// 0=auto, 1=eco, 2=comfort
properties.put("Winter Time", parseTimeInfo(Arrays.copyOfRange(bytes, 0xDB, 0xE2), "Winter").toString()); // Date
// of
// wintertime
properties.put("Summer Time", parseTimeInfo(Arrays.copyOfRange(bytes, 0xE7, 0xEF), "Summer").toString()); // Date
// of
// summertime
} catch (Exception e) {
logger.debug("Exception occurred during parsing: {}", e.getMessage(), e);
}
}
private Date parseTimeInfo(byte[] bytes, String suffix) {
int month = bytes[0] & 0xFF;
int weekDay = bytes[1] & 0xFF;
int hour = bytes[2] & 0xFF;
int utcOffset = new BigInteger(Arrays.copyOfRange(bytes, 0x03, 0x07)).intValue();
properties.put("Utc Offset" + " (" + suffix + ")", utcOffset);
Calendar pCal = Calendar.getInstance();
pCal.set(Calendar.getInstance().get(Calendar.YEAR), month - 1, 15, hour, 0, 0);
pCal.set(Calendar.DAY_OF_WEEK, weekDay + 1);
pCal.set(Calendar.DAY_OF_WEEK_IN_MONTH, -1);
return pCal.getTime();
}
private void parseHeatingThermostatData(byte[] bytes) {
try {
int plusDataStart = 18;
int programDataStart = 11;
tempComfort = new BigDecimal((bytes[plusDataStart] & 0xFF) / 2D);
tempEco = new BigDecimal((bytes[plusDataStart + 1] & 0xFF) / 2D);
tempSetpointMax = new BigDecimal((bytes[plusDataStart + 2] & 0xFF) / 2D);
tempSetpointMin = new BigDecimal((bytes[plusDataStart + 3] & 0xFF) / 2D);
properties.put(PROPERTY_THERMO_COMFORT_TEMP, tempComfort.setScale(1, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_ECO_TEMP, tempEco.setScale(1, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_MAX_TEMP_SETPOINT, tempSetpointMax.setScale(1, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_MIN_TEMP_SETPOINT, tempSetpointMin.setScale(1, RoundingMode.HALF_DOWN));
if (bytes.length < 211) {
// Device is a WallMountedThermostat
programDataStart = 4;
logger.trace("WallThermostat byte {}: {}", bytes.length - 3,
Float.toString(bytes[bytes.length - 3] & 0xFF));
logger.trace("WallThermostat byte {}: {}", bytes.length - 2,
Float.toString(bytes[bytes.length - 2] & 0xFF));
logger.trace("WallThermostat byte {}: {}", bytes.length - 1,
Float.toString(bytes[bytes.length - 1] & 0xFF));
} else {
// Device is a HeatingThermostat(+)
tempOffset = new BigDecimal((bytes[plusDataStart + 4] & 0xFF) / 2D - 3.5);
tempOpenWindow = new BigDecimal((bytes[plusDataStart + 5] & 0xFF) / 2D);
durationOpenWindow = new BigDecimal((bytes[plusDataStart + 6] & 0xFF) * 5);
boostDuration = new BigDecimal(bytes[plusDataStart + 7] & 0xFF >> 5);
boostValve = new BigDecimal((bytes[plusDataStart + 7] & 0x1F) * 5);
decalcification = new BigDecimal(bytes[plusDataStart + 8]);
valveMaximum = new BigDecimal((bytes[plusDataStart + 9] & 0xFF) * 100 / 255);
valveOffset = new BigDecimal((bytes[plusDataStart + 10] & 0xFF) * 100 / 255);
properties.put(PROPERTY_THERMO_OFFSET_TEMP, tempOffset.setScale(1, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_WINDOW_OPEN_TEMP, tempOpenWindow.setScale(1, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_WINDOW_OPEN_DURATION,
durationOpenWindow.setScale(0, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_BOOST_DURATION, boostDuration.setScale(0, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_BOOST_VALVEPOS, boostValve.setScale(0, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_DECALCIFICATION, decalcification.setScale(0, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_VALVE_MAX, valveMaximum.setScale(0, RoundingMode.HALF_DOWN));
properties.put(PROPERTY_THERMO_VALVE_OFFSET, valveOffset.setScale(0, RoundingMode.HALF_DOWN));
}
programData = "";
int ln = 13 * 6; // first day = Sat
String startTime = "00:00h";
for (int charIdx = plusDataStart + programDataStart; charIdx < (plusDataStart + programDataStart
+ 26 * 7); charIdx++) {
if (ln % 13 == 0) {
programData += "\r\n Day " + Integer.toString((ln / 13) % 7) + ": ";
startTime = "00:00h";
}
int progTime = (bytes[charIdx + 1] & 0xFF) * 5 + (bytes[charIdx] & 0x01) * 1280;
int progMinutes = progTime % 60;
int progHours = (progTime - progMinutes) / 60;
String endTime = Integer.toString(progHours) + ":" + String.format("%02d", progMinutes) + "h";
programData += startTime + "-" + endTime + " " + Double.toString(bytes[charIdx] / 4) + "C ";
startTime = endTime;
charIdx++;
ln++;
}
} catch (Exception e) {
logger.debug("Exception occurred during heater data: {}", e.getMessage(), e);
}
return;
}
public String getSerialNumber() {
return serialNumber;
}
@Override
public MessageType getType() {
return MessageType.C;
}
public String getRFAddress() {
return rfAddress;
}
public DeviceType getDeviceType() {
return deviceType;
}
public Map<String, Object> getProperties() {
return properties;
}
public int getRoomID() {
return roomId;
}
@Override
public void debug(Logger logger) {
logger.debug("=== C Message === ");
logger.trace("\tRAW: {}", this.getPayload());
logger.debug("DeviceType: {}", deviceType);
logger.debug("SerialNumber: {}", serialNumber);
logger.debug("RFAddress: {}", rfAddress);
logger.debug("RoomID: {}", roomId);
for (String key : properties.keySet()) {
if (!key.startsWith("Unknown")) {
String propertyName = StringUtils.join(StringUtils.splitByCharacterTypeCamelCase(key), ' ');
logger.debug("{}: {}", propertyName, properties.get(key));
} else {
logger.debug("{}: {}", key, properties.get(key));
}
}
if (programData != null) {
logger.trace("ProgramData: {}", programData);
}
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
/**
* The {@link: FMessage} contains information about the Cube NTP Configuration
* This is the response to a f: command
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public final class FMessage extends Message {
private String ntpServer1 = "";
private String ntpServer2 = "";
/**
* The {@link: FMessage} contains information about the Cube NTP Configuration
*
* @param raw String with raw message
*/
public FMessage(String raw) {
super(raw);
final String[] servers = this.getPayload().split(",");
if (servers.length > 0) {
ntpServer1 = servers[0];
}
if (servers.length > 1) {
ntpServer2 = servers[1];
}
}
/**
* @return the NTP Server1 name
*/
public String getNtpServer1() {
return ntpServer1;
}
/**
* @return the NTP Server2 name
*/
public String getNtpServer2() {
return ntpServer2;
}
@Override
public void debug(Logger logger) {
logger.debug("=== F Message === ");
logger.trace("\tRAW : {}", this.getPayload());
logger.debug("\tNTP Server1 : {}", this.ntpServer1);
logger.debug("\tNTP Server2 : {}", this.ntpServer2);
}
@Override
public MessageType getType() {
return MessageType.F;
}
}

View File

@@ -0,0 +1,143 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.slf4j.Logger;
/**
* The H message contains information about the MAX! Cube.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - Details parsing, OH2 version
*/
@NonNullByDefault
public final class HMessage extends Message {
private ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.systemDefault());
public Map<String, Object> properties = new HashMap<>();
private String rawSerialNumber;
private String rawRFAddress;
private String rawFirmwareVersion;
private String rawConnectionId;
private String rawDutyCycle;
private String rawFreeMemorySlots;
private String rawCubeTimeState;
private String rawNTPCounter;
// yet unknown fields
private String rawUnknownfield4;
public HMessage(String raw) {
super(raw);
String[] tokens = this.getPayload().split(Message.DELIMETER);
if (tokens.length < 11) {
throw new ArrayIndexOutOfBoundsException("MAX!Cube raw H Message corrupt");
}
rawSerialNumber = tokens[0];
rawRFAddress = tokens[1];
rawFirmwareVersion = tokens[2].substring(0, 2) + "." + tokens[2].substring(2, 4);
rawUnknownfield4 = tokens[3];
rawConnectionId = tokens[4];
rawDutyCycle = Integer.toString(Utils.fromHex(tokens[5]));
rawFreeMemorySlots = Integer.toString(Utils.fromHex(tokens[6]));
setDateTime(tokens[7], tokens[8]);
rawCubeTimeState = tokens[9];
rawNTPCounter = Integer.toString(Utils.fromHex(tokens[10]));
properties.put("Serial number", rawSerialNumber);
properties.put("RF address (HEX)", rawRFAddress);
properties.put("Firmware version", rawFirmwareVersion);
properties.put("Connection ID", rawConnectionId);
properties.put("Unknown", rawUnknownfield4);
properties.put("Duty Cycle", rawDutyCycle);
properties.put("FreeMemorySlots", rawFreeMemorySlots);
properties.put("CubeTimeState", rawCubeTimeState);
properties.put("NTPCounter", rawNTPCounter);
}
public String getSerialNumber() {
return rawSerialNumber;
}
public String getRFAddress() {
return rawRFAddress;
}
public String getFirmwareVersion() {
return rawFirmwareVersion;
}
public String getConnectionId() {
return rawConnectionId;
}
public int getDutyCycle() {
return Integer.parseInt(rawDutyCycle);
}
public int getFreeMemorySlots() {
return Integer.parseInt(rawFreeMemorySlots);
}
public String getCubeTimeState() {
return rawCubeTimeState;
}
public String getNTPCounter() {
return rawNTPCounter;
}
private final void setDateTime(String hexDate, String hexTime) {
// we have to add 2000, otherwise we get a wrong timestamp
int year = 2000 + Utils.fromHex(hexDate.substring(0, 2));
int month = Utils.fromHex(hexDate.substring(2, 4));
int dayOfMonth = Utils.fromHex(hexDate.substring(4, 6));
int hours = Utils.fromHex(hexTime.substring(0, 2));
int minutes = Utils.fromHex(hexTime.substring(2, 4));
zonedDateTime = ZonedDateTime.of(year, month, dayOfMonth, hours, minutes, 0, 0, ZoneId.systemDefault());
}
public Date getDateTime() {
return Date.from(zonedDateTime.toInstant());
}
@Override
public void debug(Logger logger) {
logger.debug("=== H Message === ");
logger.trace("\tRAW: : {}", getPayload());
logger.trace("\tReading Time : {}", getDateTime());
for (String key : properties.keySet()) {
logger.debug("\t{}: {}", key, properties.get(key));
}
}
@Override
public MessageType getType() {
return MessageType.H;
}
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.device.DeviceConfiguration;
import org.slf4j.Logger;
/**
* The L message contains real time information about all MAX! devices.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 update
*
*/
@NonNullByDefault
public final class LMessage extends Message {
public LMessage(String raw) {
super(raw);
}
public Collection<? extends Device> getDevices(List<DeviceConfiguration> configurations) {
final List<Device> devices = new ArrayList<>();
final byte[] decodedRawMessage = Base64.getDecoder()
.decode(getPayload().trim().getBytes(StandardCharsets.UTF_8));
final MaxTokenizer tokenizer = new MaxTokenizer(decodedRawMessage);
while (tokenizer.hasMoreElements()) {
byte[] token = tokenizer.nextElement();
final Device tempDevice = Device.create(token, configurations);
if (tempDevice != null) {
devices.add(tempDevice);
}
}
return devices;
}
public Collection<? extends Device> updateDevices(List<Device> devices, List<DeviceConfiguration> configurations) {
byte[] decodedRawMessage = Base64.getDecoder().decode(getPayload().trim().getBytes(StandardCharsets.UTF_8));
MaxTokenizer tokenizer = new MaxTokenizer(decodedRawMessage);
while (tokenizer.hasMoreElements()) {
byte[] token = tokenizer.nextElement();
String rfAddress = Utils.toHex(token[0] & 0xFF, token[1] & 0xFF, token[2] & 0xFF);
// logger.debug("token: "+token+" rfaddress: "+rfAddress);
Device foundDevice = null;
for (Device device : devices) {
// logger.debug(device.getRFAddress().toUpperCase()+ " vs "+rfAddress);
if (device.getRFAddress().toUpperCase().equals(rfAddress)) {
// logger.debug("Updating device..."+rfAddress);
foundDevice = device;
}
}
if (foundDevice != null) {
foundDevice = Device.update(token, configurations, foundDevice);
// devices.remove(token);
// devices.add(foundDevice);
} else {
Device tempDevice = Device.create(token, configurations);
if (tempDevice != null) {
devices.add(tempDevice);
}
}
}
return devices;
}
@Override
public void debug(Logger logger) {
logger.trace("=== L Message === ");
logger.trace("\tRAW: {}", this.getPayload());
}
@Override
public MessageType getType() {
return MessageType.L;
}
}

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.DeviceInformation;
import org.openhab.binding.max.internal.device.DeviceType;
import org.openhab.binding.max.internal.device.RoomInformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The M message contains metadata about the MAX! Cube setup.
*
* @author Andreas Heil (info@aheil.de) - Initial Contribution
* @author Marcel Verpaalen - Room details parse
*/
@NonNullByDefault
public final class MMessage extends Message {
private final Logger logger = LoggerFactory.getLogger(MMessage.class);
public List<RoomInformation> rooms = new ArrayList<>();
public List<DeviceInformation> devices = new ArrayList<>();
private Boolean hasConfiguration;
public MMessage(String raw) {
super(raw);
hasConfiguration = false;
String[] tokens = this.getPayload().split(Message.DELIMETER);
if (tokens.length <= 1) {
logger.debug("No rooms defined. Configure your Max! Cube");
hasConfiguration = false;
return;
}
try {
byte[] bytes = Base64.getDecoder().decode(tokens[2].trim().getBytes(StandardCharsets.UTF_8));
hasConfiguration = true;
logger.trace("*** M Message trace**** ");
logger.trace("\tMagic? (expect 86) : {}", (int) bytes[0]);
logger.trace("\tVersion? (expect 2): {}", (int) bytes[1]);
logger.trace("\t#defined rooms in M: {}", (int) bytes[2]);
rooms = new ArrayList<>();
devices = new ArrayList<>();
int roomCount = bytes[2];
int byteOffset = 3; // start of rooms
/* process room */
for (int i = 0; i < roomCount; i++) {
int position = bytes[byteOffset++];
int nameLength = bytes[byteOffset++] & 0xff;
byte[] data = new byte[nameLength];
System.arraycopy(bytes, byteOffset, data, 0, nameLength);
byteOffset += nameLength;
String name = new String(data, StandardCharsets.UTF_8);
String rfAddress = Utils.toHex((bytes[byteOffset] & 0xff), (bytes[byteOffset + 1] & 0xff),
(bytes[byteOffset + 2] & 0xff));
byteOffset += 3;
rooms.add(new RoomInformation(position, name, rfAddress));
}
/* process devices */
int deviceCount = bytes[byteOffset++];
for (int deviceId = 0; deviceId < deviceCount; deviceId++) {
DeviceType deviceType = DeviceType.create(bytes[byteOffset++]);
String rfAddress = Utils.toHex((bytes[byteOffset] & 0xff), (bytes[byteOffset + 1] & 0xff),
(bytes[byteOffset + 2] & 0xff));
byteOffset += 3;
final StringBuilder serialNumberBuilder = new StringBuilder(10);
for (int i = 0; i < 10; i++) {
serialNumberBuilder.append((char) bytes[byteOffset++]);
}
int nameLength = bytes[byteOffset++] & 0xff;
byte[] data = new byte[nameLength];
System.arraycopy(bytes, byteOffset, data, 0, nameLength);
byteOffset += nameLength;
String deviceName = new String(data, StandardCharsets.UTF_8);
int roomId = bytes[byteOffset++] & 0xff;
devices.add(new DeviceInformation(deviceType, serialNumberBuilder.toString(), rfAddress, deviceName,
roomId));
}
} catch (Exception e) {
logger.debug("Unknown error parsing the M Message: {}", e.getMessage(), e);
logger.debug("\tRAW : {}", this.getPayload());
}
}
@Override
public void debug(Logger logger) {
logger.debug("=== M Message === ");
if (hasConfiguration) {
logger.trace("\tRAW : {}", this.getPayload());
for (RoomInformation room : rooms) {
logger.debug("\t=== Rooms ===");
logger.debug("\tRoom Pos : {}", room.getPosition());
logger.debug("\tRoom Name : {}", room.getName());
logger.debug("\tRoom RF Adr: {}", room.getRFAddress());
for (DeviceInformation device : devices) {
if (room.getPosition() == device.getRoomId()) {
logger.debug("\t=== Devices ===");
logger.debug("\tDevice Type : {}", device.getDeviceType());
logger.debug("\tDevice Name : {}", device.getName());
logger.debug("\tDevice Serialnr: {}", device.getSerialNumber());
logger.debug("\tDevice RF Adr : {}", device.getRFAddress());
logger.debug("\tRoom Id : {}", device.getRoomId());
}
}
}
} else {
logger.debug("M Message empty. No Configuration");
}
}
@Override
public MessageType getType() {
return MessageType.M;
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.util.Enumeration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The MaxTokenizer parses a L message into the MAX! devices encoded within. The L message contains
* real time information for multiple devices. Each device starts with the length n bytes.
* The MaxTokenzier starts with the first device and chops off one device after another from the byte stream.
*
* The tokens returned consist of the payload solely, and do not contain the first byte holding the
* tokens length.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
@NonNullByDefault
public final class MaxTokenizer implements Enumeration<byte[]> {
private int offset;
private byte[] decodedRawMessage;
/**
* Creates a new MaxTokenizer.
*
* @param decodedRawMessage
* The Base64 decoded MAX! Cube protocol L message as byte array
*/
public MaxTokenizer(byte[] decodedRawMessage) {
this.decodedRawMessage = decodedRawMessage;
}
@Override
public boolean hasMoreElements() {
return offset < decodedRawMessage.length;
}
@Override
public byte[] nextElement() {
byte length = decodedRawMessage[offset++];
// make sure to get the correct length in case > 127
byte[] token = new byte[length & 0xFF];
for (int i = 0; i < (length & 0xFF); i++) {
token[i] = decodedRawMessage[offset++];
}
return token;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
/**
* Base message for the messages received from the MAX! Cube.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
@NonNullByDefault
public abstract class Message {
public static final String DELIMETER = ",";
private final String raw;
public Message(String raw) {
this.raw = raw;
}
public abstract void debug(Logger logger);
public abstract MessageType getType();
protected final String getPayload() {
return raw.substring(2, raw.length());
}
}

View File

@@ -0,0 +1,242 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.exceptions.IncompleteMessageException;
import org.openhab.binding.max.internal.exceptions.IncorrectMultilineIndexException;
import org.openhab.binding.max.internal.exceptions.MessageIsWaitingException;
import org.openhab.binding.max.internal.exceptions.NoMessageAvailableException;
import org.openhab.binding.max.internal.exceptions.UnprocessableMessageException;
import org.openhab.binding.max.internal.exceptions.UnsupportedMessageTypeException;
/**
* The message processor was introduced to combine multiple received lines to
* one single message. There are cases, when the MAX! Cube sends multiple
* messages (M-Message for example). The message processor acts as stack for
* received messages. Every received line should be added to the processor.
* After every added line, the message processor analyses the line. It is not
* possible to add additional lines when there is a message ready to be
* processed.
*
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
@NonNullByDefault
public class MessageProcessor {
public static final String SEPARATOR = ":";
/**
* The message that was created from last line received. (Null if no message
* available yet)
*/
private @Nullable Message currentMessage;
/**
* <pre>
* If more that one single line is required to create a message
* numberOfRequiredLines holds the number of required messages to complete
* receivedLines holds the lines received so far
* currentMessageType indicates which message type is currently on stack
* </pre>
*/
private @Nullable Integer numberOfRequiredLines;
private List<String> receivedLines = new ArrayList<>();
private @Nullable MessageType currentMessageType;
/**
* Resets the current status and processed lines. Should be used after
* processing a message
*/
public void reset() {
this.currentMessage = null;
receivedLines.clear();
currentMessageType = null;
numberOfRequiredLines = null;
}
/**
* Analyses the line and creates a message when possible. If the line
* indicates, that additional lines are required to create a complete
* message, the message processor keeps the line in memory and awaits
* additional lines. If the new line does not fit into current state
* (incomplete M: message on stack but L: message line received) a
* IncompleteMessageException is thrown.
*
* @param line
* is the new line received
* @return true if a message could be created by this line, false in any
* other cases (line was stacked, error, ...)
* @throws MessageIsWaitingException
* when a line was added without pulling the previous message
* @throws IncompleteMessageException
* when a line was added that does not belong to current message
* stack
* @throws UnsupportedMessageTypeException
* in case the line starts with an unknown message indicator
* @throws UnprocessableMessageException
* is thrown when there was a known message indicator found, but
* message could not be parsed correctly.
* @throws IncorrectMultilineIndexException
*/
public Boolean addReceivedLine(String line) throws IncompleteMessageException, MessageIsWaitingException,
UnsupportedMessageTypeException, UnprocessableMessageException, IncorrectMultilineIndexException {
if (this.currentMessage != null) {
throw new MessageIsWaitingException();
}
MessageType messageType = getMessageType(line);
if (messageType == null) {
throw new UnsupportedMessageTypeException();
}
if ((this.currentMessageType != null) && (!messageType.equals(this.currentMessageType))) {
throw new IncompleteMessageException();
}
Boolean result = true;
switch (messageType) {
case H:
this.currentMessage = new HMessage(line);
break;
case C:
this.currentMessage = new CMessage(line);
break;
case L:
this.currentMessage = new LMessage(line);
break;
case S:
this.currentMessage = new SMessage(line);
break;
case M:
result = handleMMessageLine(line);
break;
case N:
this.currentMessage = new NMessage(line);
break;
case F:
this.currentMessage = new FMessage(line);
break;
case A:
this.currentMessage = new AMessage(line);
break;
default:
}
return result;
}
private Boolean handleMMessageLine(String line)
throws UnprocessableMessageException, IncompleteMessageException, IncorrectMultilineIndexException {
Boolean result = false;
String[] tokens = line.split(Message.DELIMETER); // M:00,01,xyz.....
try {
Integer index = Integer.valueOf(tokens[0].replaceFirst("M:", "")); // M:00
Integer counter = Integer.valueOf(tokens[1]); // 01
if (this.numberOfRequiredLines == null) {
switch (counter) {
case 0:
throw new UnprocessableMessageException();
case 1:
this.currentMessage = new MMessage(line);
result = true;
break;
default:
this.numberOfRequiredLines = counter;
this.currentMessageType = MessageType.M;
if (index == 0) {
this.receivedLines.add(line);
} else {
throw new IncorrectMultilineIndexException();
}
}
} else {
if ((!counter.equals(this.numberOfRequiredLines)) || (!(index == this.receivedLines.size()))) {
throw new IncorrectMultilineIndexException();
}
receivedLines.add(tokens[2]);
if (index + 1 == receivedLines.size()) {
String newLine = "";
for (String curLine : receivedLines) {
newLine += curLine;
}
this.currentMessage = new MMessage(newLine);
result = true;
}
}
} catch (IncorrectMultilineIndexException ex) {
throw ex;
} catch (Exception ex) {
throw new UnprocessableMessageException();
}
return result;
}
/**
* @return true if there is a message waiting to be pulled
*/
public boolean isMessageAvailable() {
return this.currentMessage != null;
}
/**
* Pulls the message from the stack when there is one available. This needs
* to be done before next line can be added into message processor. When
* message is pulled, the message processor is reseted and ready to process
* next line.
*
* @return Message
* @throws NoMessageAvailableException
* when there was no message on the stack
*/
@Nullable
public Message pull() throws NoMessageAvailableException {
final Message result = this.currentMessage;
if (this.currentMessage == null) {
throw new NoMessageAvailableException();
}
reset();
return result;
}
/**
* Processes the raw TCP data read from the MAX protocol, returning the
* corresponding MessageType.
*
* @param line
* the raw data provided read from the MAX protocol
* @return MessageType of the line added
*/
@Nullable
private static MessageType getMessageType(String line) {
for (MessageType msgType : MessageType.values()) {
if (line.startsWith(msgType.name() + SEPARATOR)) {
return msgType;
}
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This enumeration represents the different message types provided by the MAX! Cube protocol.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
*/
@NonNullByDefault
public enum MessageType {
H,
M,
C,
L,
S,
N,
F,
A
}

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.DeviceType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link: NMessage} contains information about a newly discovered Device
* This is the response to a n: command
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public final class NMessage extends Message {
private final Logger logger = LoggerFactory.getLogger(NMessage.class);
private String decodedPayload = "";
private @Nullable DeviceType deviceType;
private String rfAddress = "";
private String serialnr = "";
/**
* The {@link: NMessage} contains information about a newly discovered Device
*
* @param raw String with raw message
*/
public NMessage(String raw) {
super(raw);
String msgPayload = this.getPayload();
if (msgPayload.length() > 0) {
try {
byte[] bytes = Base64.getDecoder().decode(msgPayload.trim());
decodedPayload = new String(bytes, StandardCharsets.UTF_8);
deviceType = DeviceType.create(bytes[0] & 0xFF);
rfAddress = Utils.toHex(bytes[1] & 0xFF, bytes[2] & 0xFF, bytes[3] & 0xFF);
byte[] data = new byte[10];
System.arraycopy(bytes, 4, data, 0, 10);
serialnr = new String(data, StandardCharsets.UTF_8);
} catch (Exception e) {
logger.debug("Exception occurred during parsing of N message: {}", e.getMessage(), e);
}
} else {
logger.debug("No device found during inclusion");
}
}
public @Nullable DeviceType getDeviceType() {
return deviceType;
}
public String getRfAddress() {
return rfAddress;
}
public String getSerialNumber() {
return serialnr;
}
@Override
public void debug(Logger logger) {
if (!this.rfAddress.isEmpty()) {
logger.debug("=== N Message === ");
logger.trace("\tRAW : {}", this.decodedPayload);
logger.debug("\tDevice Type : {}", this.deviceType);
logger.debug("\tRF Address : {}", this.rfAddress);
logger.debug("\tSerial : {}", this.serialnr);
} else {
logger.trace("=== N Message === ");
logger.trace("\tRAW : {}", this.decodedPayload);
}
}
@Override
public MessageType getType() {
return MessageType.N;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SMessage} contains information about Command execution results
*
* @author Bernd Michael Helm (bernd.helm at helmundwalter.de) - Initial contribution
* @author Marcel Verpaalen - OH2 version + parsing of the message
*/
@NonNullByDefault
public final class SMessage extends Message {
private final Logger logger = LoggerFactory.getLogger(SMessage.class);
private int dutyCycle;
private int freeMemorySlots;
private boolean commandDiscarded = false;
public SMessage(String raw) {
super(raw);
String[] tokens = this.getPayload().split(Message.DELIMETER);
if (tokens.length == 3) {
try {
dutyCycle = Integer.parseInt(tokens[0], 16);
commandDiscarded = tokens[1].contentEquals("1");
freeMemorySlots = Integer.parseInt(tokens[2], 16);
} catch (Exception e) {
logger.debug("Exception occurred during parsing of S message: {}", e.getMessage(), e);
}
} else {
logger.debug("Unexpected # of tolkens ({}) received in S message: {}", tokens.length, this.getPayload());
}
}
public int getDutyCycle() {
return dutyCycle;
}
public int getFreeMemorySlots() {
return freeMemorySlots;
}
public boolean isCommandDiscarded() {
return commandDiscarded;
}
@Override
public void debug(Logger logger) {
logger.trace("=== S Message === ");
logger.trace("\tRAW : {}", this.getPayload());
logger.trace("\tDutyCycle : {}", this.dutyCycle);
logger.trace("\tCommand Discarded : {}", this.commandDiscarded);
logger.trace("\tFreeMemorySlots : {}", this.freeMemorySlots);
}
@Override
public MessageType getType() {
return MessageType.S;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="max" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>MAX! Binding</name>
<description>This is the binding for the eQ-3 MAX! Home Solution.</description>
<author>Marcel Verpaalen</author>
</binding:binding>

View File

@@ -0,0 +1,278 @@
# binding
binding.max.name = MAX! Heizungssteuerung Binding
binding.max.description = Dieses Binding integriert die MAX! Heizungssteuerung (z.B. Heizkörperthermostat, Heizkörperthermostat+, Wandthermostat+, Eco Taster, Fensterkontakt).
# bridge types
thing-type.max.bridge.description = MAX! Cube LAN Gateway.
# bridge types config groups
thing-type.config.max.bridge.group.identification.label = Identifizierung
thing-type.config.max.bridge.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.bridge.group.network.label = Netzwerk
thing-type.config.max.bridge.group.network.description = Einstellungen für das Netzwerk.
thing-type.config.max.bridge.group.device.label = Gerät
thing-type.config.max.bridge.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.bridge.group.actions.label = Aktionen
thing-type.config.max.bridge.group.actions.description = Einstellungen für Aktionen.
# bridge types config
thing-type.config.max.bridge.ipAddress.label = IP-Adresse
thing-type.config.max.bridge.ipAddress.description = Lokale IP-Adresse oder Hostname des MAX! Cube LAN Gateway.
thing-type.config.max.bridge.port.label = Port
thing-type.config.max.bridge.port.description = Port des MAX! Cube LAN Gateway.
thing-type.config.max.bridge.refreshInterval.label = Abfrageintervall
thing-type.config.max.bridge.refreshInterval.description = Intervall zur Abfrage des MAX! Cube LAN Gateway (in Sekunden).
thing-type.config.max.bridge.serialNumber.label = Seriennummer
thing-type.config.max.bridge.serialNumber.description = Seriennummer des MAX! Cube LAN Gateway.
thing-type.config.max.bridge.rfAddress.label = RF-Addresse
thing-type.config.max.bridge.rfAddress.description = RF-Addresse des MAX! Cube LAN Gateway.
thing-type.config.max.bridge.exclusive.label = Exklusivmodus
thing-type.config.max.bridge.exclusive.description = Setzt den Exklusivmodus zur Nutzung des MAX! Cube LAN Gateways.
thing-type.config.max.bridge.maxRequestsPerConnection.label = Verbindingsanzahl
thing-type.config.max.bridge.maxRequestsPerConnection.description = Maximale Anzahl an Verbindungen zum MAX! Cube LAN Gateways.
thing-type.config.max.bridge.ntpServer1.label = 1. NTP-Server
thing-type.config.max.bridge.ntpServer1.description = Erster NTP-Server zur Abfrage des Datums und der Zeit.
thing-type.config.max.bridge.ntpServer2.label = 2. NTP-Server
thing-type.config.max.bridge.ntpServer2.description = Zweiter NTP-Server zur Abfrage des Datums und der Zeit.
thing-type.config.max.bridge.action-cubeReset.label = Werkszustand
thing-type.config.max.bridge.action-cubeReset.description = Ermöglicht das MAX Cube LAN Gateway in den Werkszustand zurück zu versetzen.
thing-type.config.max.bridge.action-cubeReset.option.1234 = Zurücksetzen
thing-type.config.max.bridge.action-cubeReset.option.-1 = Keine Aktion
thing-type.config.max.bridge.action-cubeReboot.label = Neustart
thing-type.config.max.bridge.action-cubeReboot.description = Ermöglicht das MAX Cube LAN Gateway neu zu starten.
thing-type.config.max.bridge.action-cubeReboot.option.1234 = Neustarten
thing-type.config.max.bridge.action-cubeReboot.option.-1 = Keine Aktion
# thing types
thing-type.max.thermostat.label = MAX! Heizkörperthermostat
thing-type.max.thermostat.description = MAX! Heizkörperthermostat und MAX! Heizkörperthermostat basic. Dient zur Steuerung von Heizkörpern und liefert Daten wie z.B. Temperatur.
thing-type.max.thermostatplus.label = MAX! Heizkörperthermostat+
thing-type.max.thermostatplus.description = MAX! Heizkörperthermostat+. Dient zur Steuerung von Heizkörpern und liefert Daten wie z.B. Temperatur.
thing-type.max.wallthermostat.label = MAX! Wandthermostat+
thing-type.max.wallthermostat.description = MAX! Wandthermostat+. Dient zur Steuerung von Heizkörperthermostaten und liefert Daten wie z.B. Temperatur.
thing-type.max.ecoswitch.label = MAX! Eco Taster
thing-type.max.ecoswitch.description = MAX! Eco Taster. Dient zur Steuerung von Heizkörperthermostaten.
thing-type.max.shuttercontact.label = MAX! Fensterkontakt
thing-type.max.shuttercontact.description = MAX! Fensterkontakt. Liefert Daten wie z.B. Fenster-Zustand.
# thing types config groups
thing-type.config.max.thermostat.group.identification.label = Identifizierung
thing-type.config.max.thermostat.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.thermostat.group.device.label = Gerät
thing-type.config.max.thermostat.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.thermostat.group.binding.label = Binding
thing-type.config.max.thermostat.group.binding.description = Einstellungen für das Binding.
thing-type.config.max.thermostat.group.actions.label = Aktionen
thing-type.config.max.thermostat.group.actions.description = Einstellungen für Aktionen.
thing-type.config.max.thermostatplus.group.identification.label = Identifizierung
thing-type.config.max.thermostatplus.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.thermostatplus.group.device.label = Gerät
thing-type.config.max.thermostatplus.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.thermostatplus.group.actions.label = Aktionen
thing-type.config.max.thermostatplus.group.actions.description = Einstellungen für Aktionen.
thing-type.config.max.thermostatplus.group.binding.label = Binding
thing-type.config.max.thermostatplus.group.binding.description = Einstellungen für das Binding.
thing-type.config.max.wallthermostat.group.identification.label = Identifizierung
thing-type.config.max.wallthermostat.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.wallthermostat.group.device.label = Gerät
thing-type.config.max.wallthermostat.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.wallthermostat.group.actions.label = Aktionen
thing-type.config.max.wallthermostat.group.actions.description = Einstellungen für Aktionen.
thing-type.config.max.ecoswitch.group.identification.label = Identifizierung
thing-type.config.max.ecoswitch.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.ecoswitch.group.device.label = Gerät
thing-type.config.max.ecoswitch.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.shuttercontact.group.identification.label = Identifizierung
thing-type.config.max.shuttercontact.group.identification.description = Einstellungen für die Identifizierung des Gerätes.
thing-type.config.max.shuttercontact.group.device.label = Gerät
thing-type.config.max.shuttercontact.group.device.description = Einstellungen für das Gerät.
thing-type.config.max.shuttercontact.group.actions.label = Aktionen
thing-type.config.max.shuttercontact.group.actions.description = Einstellungen für Aktionen.
# thing types config
thing-type.config.max.thermostat.room.label = Raum
thing-type.config.max.thermostat.room.description = Benutzerspezifischer Namen des Raumes, dem das Gerät zugeordnet ist.
thing-type.config.max.thermostat.name.label = Name
thing-type.config.max.thermostat.name.description = Benutzerspezifischer Namen des Gerätes.
thing-type.config.max.thermostat.comfortTemp.label = Komforttemperatur
thing-type.config.max.thermostat.comfortTemp.description = Gibt die Komforttemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.ecoTemp.label = Absenktemperatur
thing-type.config.max.thermostat.ecoTemp.description = Gibt die Absenktemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.offsetTemp.label = Temperatur-Offset
thing-type.config.max.thermostat.offsetTemp.description = Gibt den Temperatur-Offset (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.maxTempSetpoint.label = Max. Solltemperatur
thing-type.config.max.thermostat.maxTempSetpoint.description = Gibt die maximal einstellbare Solltemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.minTempSetpoint.label = Min. Solltemperatur
thing-type.config.max.thermostat.minTempSetpoint.description = Gibt die minimal einstellbare Solltemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.windowOpenTemp.label = Fenster-Auf-Temperatur
thing-type.config.max.thermostat.windowOpenTemp.description = Gibt die Temperatur bei geöffnetem Fenster-Zustand (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostat.windowOpenDuration.label = Dauer Fenster-Auf-Temperatur
thing-type.config.max.thermostat.windowOpenDuration.description = Gibt die Dauer der Absenkung auf die Fenster-Auf-Temperatur (in Minuten) des Heizkörperthermostats an.
thing-type.config.max.thermostat.serialNumber.label = Seriennummer
thing-type.config.max.thermostat.serialNumber.description = Seriennummer des Gerätes.
thing-type.config.max.thermostat.rfAddress.label = RF-Addresse
thing-type.config.max.thermostat.rfAddress.description = RF-Addresse des Gerätes.
thing-type.config.max.thermostat.refreshActualRate.label = Aktualisierungsrate
thing-type.config.max.thermostat.action-deviceDelete.label = Gerät trennen
thing-type.config.max.thermostat.action-deviceDelete.description = Ermöglicht das Trennen des Gerätes vom MAX! Cube LAN Gateway.
thing-type.config.max.thermostat.action-deviceDelete.option.1234 = Löschen
thing-type.config.max.thermostat.action-deviceDelete.option.-1 = Keine Aktion
thing-type.config.max.thermostatplus.room.label = Raum
thing-type.config.max.thermostatplus.room.description = Benutzerspezifischer Namen des Raumes, dem das Gerät zugeordnet ist.
thing-type.config.max.thermostatplus.name.label = Name
thing-type.config.max.thermostatplus.name.description = Benutzerspezifischer Namen des Gerätes.
thing-type.config.max.thermostatplus.comfortTemp.label = Komforttemperatur
thing-type.config.max.thermostatplus.comfortTemp.description = Gibt die Komforttemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.ecoTemp.label = Absenktemperatur
thing-type.config.max.thermostatplus.ecoTemp.description = Gibt die Absenktemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.offsetTemp.label = Temperatur-Offset
thing-type.config.max.thermostatplus.offsetTemp.description = Gibt den Temperatur-Offset (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.maxTempSetpoint.label = Max. Solltemperatur
thing-type.config.max.thermostatplus.maxTempSetpoint.description = Gibt die maximal einstellbare Solltemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.minTempSetpoint.label = Min. Solltemperatur
thing-type.config.max.thermostatplus.minTempSetpoint.description = Gibt die minimal einstellbare Solltemperatur (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.windowOpenTemp.label = Fenster-Auf-Temperatur
thing-type.config.max.thermostatplus.windowOpenTemp.description = Gibt die Temperatur bei geöffnetem Fenster-Zustand (in °C) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.windowOpenDuration.label = Dauer Fenster-Auf-Temperatur
thing-type.config.max.thermostatplus.windowOpenDuration.description = Gibt die Dauer der Absenkung auf die Fenster-Auf-Temperatur (in Minuten) des Heizkörperthermostats an.
thing-type.config.max.thermostatplus.serialNumber.label = Seriennummer
thing-type.config.max.thermostatplus.serialNumber.description = Seriennummer des Gerätes.
thing-type.config.max.thermostatplus.rfAddress.label = RF-Addresse
thing-type.config.max.thermostatplus.rfAddress.description = RF-Addresse des Gerätes.
thing-type.config.max.thermostatplus.refreshActualRate.label = Aktualisierungsrate
thing-type.config.max.thermostatplus.action-deviceDelete.label = Gerät trennen
thing-type.config.max.thermostatplus.action-deviceDelete.description = Ermöglicht das Trennen des Gerätes vom MAX! Cube LAN Gateway.
thing-type.config.max.thermostatplus.action-deviceDelete.option.1234 = Löschen
thing-type.config.max.thermostatplus.action-deviceDelete.option.-1 = Keine Aktion
thing-type.config.max.wallthermostat.room.label = Raum
thing-type.config.max.wallthermostat.room.description = Benutzerspezifischer Namen des Raumes, dem das Gerät zugeordnet ist.
thing-type.config.max.wallthermostat.name.label = Name
thing-type.config.max.wallthermostat.name.description = Benutzerspezifischer Namen des Gerätes.
thing-type.config.max.wallthermostat.serialNumber.label = Seriennummer
thing-type.config.max.wallthermostat.serialNumber.description = Seriennummer des Gerätes.
thing-type.config.max.wallthermostat.rfAddress.label = RF-Addresse
thing-type.config.max.wallthermostat.rfAddress.description = RF-Addresse des Gerätes.
thing-type.config.max.wallthermostat.action-deviceDelete.label = Gerät trennen
thing-type.config.max.wallthermostat.action-deviceDelete.description = Ermöglicht das Trennen des Gerätes vom MAX! Cube LAN Gateway.
thing-type.config.max.wallthermostat.action-deviceDelete.option.1234 = Löschen
thing-type.config.max.wallthermostat.action-deviceDelete.option.-1 = Keine Aktion
thing-type.config.max.ecoswitch.room.label = Raum
thing-type.config.max.ecoswitch.room.description = Benutzerspezifischer Namen des Raumes, dem das Gerät zugeordnet ist.
thing-type.config.max.ecoswitch.name.label = Name
thing-type.config.max.ecoswitch.name.description = Benutzerspezifischer Namen des Gerätes.
thing-type.config.max.ecoswitch.serialNumber.label = Seriennummer
thing-type.config.max.ecoswitch.serialNumber.description = Seriennummer des Gerätes.
thing-type.config.max.ecoswitch.rfAddress.label = RF-Addresse
thing-type.config.max.ecoswitch.rfAddress.description = RF-Addresse des Gerätes.
thing-type.config.max.shuttercontact.room.label = Raum
thing-type.config.max.shuttercontact.room.description = Benutzerspezifischer Namen des Raumes, dem das Gerät zugeordnet ist.
thing-type.config.max.shuttercontact.name.label = Name
thing-type.config.max.shuttercontact.name.description = Benutzerspezifischer Namen des Gerätes.
thing-type.config.max.shuttercontact.serialNumber.label = Seriennummer
thing-type.config.max.shuttercontact.serialNumber.description = Seriennummer des Gerätes.
thing-type.config.max.shuttercontact.rfAddress.label = RF-Addresse
thing-type.config.max.shuttercontact.rfAddress.description = RF-Addresse des Gerätes.
thing-type.config.max.shuttercontact.action-deviceDelete.label = Gerät trennen
thing-type.config.max.shuttercontact.action-deviceDelete.description = Ermöglicht das Trennen des Gerätes vom MAX! Cube LAN Gateway.
thing-type.config.max.shuttercontact.action-deviceDelete.option.1234 = Löschen
thing-type.config.max.shuttercontact.action-deviceDelete.option.-1 = Keine Aktion
# channel types
channel-type.max.free_mem.label = Freie Speicherplätze
channel-type.max.free_mem.description = Gibt an, wie viele freie Speicherplätze im MAX! Cube LAN Gateway für Befehle zur Verfügung stehen.
channel-type.max.duty_cycle.label = Auslastungsgrad
channel-type.max.duty_cycle.description = Gibt den Auslastungsgrad für Befehle (in %) an.
channel-type.max.valve.label = Ventil
channel-type.max.valve.description = Gibt die Ventilöffnung des Heizkörperreglers (in %) an.
channel-type.max.mode.label = Modus des Gerätes
channel-type.max.mode.description = Gibt den aktuellen Modus des Gerätes an (MANUAL/AUTOMATIC/BOOST/VACATION).
channel-type.max.mode.state.option.AUTOMATIC = Automatisch
channel-type.max.mode.state.option.MANUAL = Manuell
channel-type.max.mode.state.option.BOOST = Boost
channel-type.max.mode.state.option.VACATION = Urlaubsmodus
channel-type.max.actual_temp.label = Temperatur
channel-type.max.actual_temp.description = Gibt die aktuell gemessene Temperatur des Thermostats an.
channel-type.max.set_temp.label = Solltemperatur
channel-type.max.set_temp.description = Gibt die aktuell eingestellte Solltemperatur des Thermostats an.
channel-type.max.locked.label = Tastensperre
channel-type.max.locked.description = Gibt an, ob die Tastensperre am Gerät aktiviert ist.
channel-type.max.contact_state.label = Fenster-Zustand
channel-type.max.contact_state.description = Gibt an, ob ein Fenster offen oder geschlossen ist.

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="max" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>MAX! Cube LAN Gateway</label>
<description>This bridge represents the MAX! Cube LAN Gateway.
</description>
<channels>
<channel id="free_mem" typeId="free_mem"/>
<channel id="duty_cycle" typeId="duty_cycle"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
<advanced>false</advanced>
</parameter-group>
<parameter-group name="network">
<label>Connection</label>
<description>Connection Settings</description>
<advanced>false</advanced>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
<advanced>true</advanced>
</parameter-group>
<!-- Trigger actions from habmin -->
<parameter-group name="actions">
<context></context>
<label>Actions</label>
<description>Action Buttons</description>
</parameter-group>
<parameter name="ipAddress" type="text" required="true" groupName="network">
<context>network-address</context>
<label>MAX! Cube LAN Gateway IP</label>
<description>The IP address of the MAX! Cube LAN gateway</description>
</parameter>
<parameter name="port" type="integer" required="false" min="1" max="65535" groupName="network">
<context>network-address</context>
<label>MAX! Cube LAN Gateway Port</label>
<description>Port of the LAN gateway</description>
<default>62910</default>
<advanced>true</advanced>
</parameter>
<parameter name="refreshInterval" type="integer" required="false" min="1" unit="s" groupName="network">
<label>Refresh Interval</label>
<description>The refresh interval in seconds which is used to poll given
MAX! Cube.</description>
<default>30</default>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
<parameter name="exclusive" type="boolean" required="false" groupName="network">
<label>Exclusive Mode</label>
<description>If set to true, the binding will leave the connection to the
Cube open.</description>
<default>true</default>
</parameter>
<parameter name="maxRequestsPerConnection" type="integer" required="false" groupName="network">
<label>Max Requests Per Connection</label>
<description>In exclusive mode, how many requests are allowed until
connection is closed and reopened.</description>
<default>1000</default>
<advanced>true</advanced>
</parameter>
<parameter name="ntpServer1" type="text" required="false" groupName="device">
<label>NTP Server 1</label>
<description>The hostname for NTP Server 1 used by the Cube to get the time.</description>
<default>ntp.homematic.com</default>
<advanced>true</advanced>
</parameter>
<parameter name="ntpServer2" type="text" required="false" groupName="device">
<label>NTP Server 2</label>
<description>The hostname for NTP Server 2 used by the Cube to get the time.</description>
<default>ntp.homematic.com</default>
<advanced>true</advanced>
</parameter>
<!-- Trigger reset action from habmin menu -->
<parameter name="action-cubeReset" type="integer" groupName="actions">
<label>Reset Cube Configuration</label>
<description>Resets the MAX! Cube room and device information. Devices will
need to be included again!</description>
<options>
<option value="1234">Reset</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
<advanced>true</advanced>
</parameter>
<!-- Trigger reboot action from habmin menu -->
<parameter name="action-cubeReboot" type="integer" groupName="actions">
<label>Restart Cube</label>
<description>Restarts the Cube.</description>
<options>
<option value="1234">Reboot</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
</parameter>
</config-description>
</bridge-type>
<channel-type id="free_mem" advanced="true">
<item-type>Number</item-type>
<label>Free Memory Slots</label>
<description>Free memory slots to store commands send to the devices</description>
<state pattern="%d" readOnly="true"/>
</channel-type>
<channel-type id="duty_cycle" advanced="true">
<item-type>Number</item-type>
<label>Duty Cycle</label>
<description>Duty Cycle for sending commands to the devices</description>
<state pattern="%d %%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,493 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="max" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>MAX! HeatingThermostat</label>
<description>This is a MAX! HeatingThermostat</description>
<channels>
<channel id="valve" typeId="valve"/>
<channel id="battery_low" typeId="system.low-battery"/>
<channel id="mode" typeId="mode"/>
<channel id="actual_temp" typeId="actual_temp"/>
<channel id="set_temp" typeId="set_temp"/>
<channel id="locked" typeId="locked"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
<advanced>true</advanced>
</parameter-group>
<parameter-group name="binding">
<label>Binding Settings</label>
<description>Binding settings for this device</description>
</parameter-group>
<!-- Trigger actions from habmin -->
<parameter-group name="actions">
<label>Actions</label>
<description>Action Buttons</description>
</parameter-group>
<!-- Experimental - Enable when also removing rooms is implemented <parameter
name="roomId" type="integer" required="false" groupName="device"> <label>Room Id</label>
<description>The room name Id.</description> <advanced>true</advanced> </parameter> -->
<parameter name="room" type="text" required="false" groupName="device">
<label>Room</label>
<description>The room name.</description>
<advanced>true</advanced>
</parameter>
<parameter name="name" type="text" required="false" groupName="device">
<label>Device Name</label>
<description>The device description.</description>
<advanced>true</advanced>
</parameter>
<parameter name="comfortTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Comfort Temperature</label>
<description>Set the Comfort Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="ecoTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Eco Temperature</label>
<description>Set the Eco Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="offsetTemp" type="decimal" min="-3.5" max="3.5" step="0.5" required="false"
groupName="device">
<label>OffSet Temperature</label>
<description>Set the Thermostat offset.</description>
<advanced>true</advanced>
</parameter>
<parameter name="maxTempSetpoint" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Max Temperature</label>
<description>Set the Thermostat maximum temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="minTempSetpoint" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Min Temperature</label>
<description>Set the Thermostat minimum temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="windowOpenTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Window Open Temp</label>
<description>Set the Window Open Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="windowOpenDuration" type="integer" min="0" max="60" step="5" required="false"
groupName="device">
<label>Window Open Duration</label>
<description>Set the Thermostat Window Open Duration.</description>
<advanced>true</advanced>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
<parameter name="refreshActualRate" type="integer" required="false" groupName="binding">
<label>Actual Temperature Refresh Rate</label>
<description>Experimental feature! Rate of the actual refresh in minutes. 0-9=refresh disabled. Minimum refresh rate
once/10 minutes, recommended 60min. The settings of the heating thermostats are changed to trigger an update of
temperature, as the actual temperature only is updated when the valves changes.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<!-- Trigger reset action from habmin menu -->
<parameter name="action-deviceDelete" type="integer" groupName="actions">
<label>Delete Device from Cube</label>
<description>Deletes the device from the MAX! Cube. Device will need to be
included again!</description>
<options>
<option value="1234">Delete</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<thing-type id="thermostatplus">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>MAX! HeatingThermostat+</label>
<description>This is a MAX! HeatingThermostat+</description>
<channels>
<channel id="valve" typeId="valve"/>
<channel id="battery_low" typeId="system.low-battery"/>
<channel id="mode" typeId="mode"/>
<channel id="actual_temp" typeId="actual_temp"/>
<channel id="set_temp" typeId="set_temp"/>
<channel id="locked" typeId="locked"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
<advanced>true</advanced>
</parameter-group>
<parameter-group name="binding">
<label>Binding Settings</label>
<description>Binding settings for this device</description>
</parameter-group>
<!-- Trigger actions from habmin -->
<parameter-group name="actions">
<context></context>
<label>Actions</label>
<description>Action Buttons</description>
</parameter-group>
<!-- Experimental - Enable when also removing rooms is implemented <parameter
name="roomId" type="integer" required="false" groupName="device"> <label>Room Id</label>
<description>The room name Id.</description> <advanced>true</advanced> </parameter> -->
<parameter name="room" type="text" required="false" groupName="device">
<label>Room</label>
<description>The room name.</description>
<advanced>true</advanced>
</parameter>
<parameter name="name" type="text" required="false" groupName="device">
<label>Device Name</label>
<description>The device description.</description>
<advanced>true</advanced>
</parameter>
<parameter name="comfortTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Comfort Temperature</label>
<description>Set the Comfort Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="ecoTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Eco Temperature</label>
<description>Set the Eco Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="offsetTemp" type="decimal" min="-3.5" max="3.5" step="0.5" required="false"
groupName="device">
<label>OffSet Temperature</label>
<description>Set the Thermostat offset.</description>
<advanced>true</advanced>
</parameter>
<parameter name="maxTempSetpoint" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Max Temperature</label>
<description>Set the Thermostat maximum temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="minTempSetpoint" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Min Temperature</label>
<description>Set the Thermostat minimum temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="windowOpenTemp" type="decimal" min="4.5" max="30.5" step="0.5" required="false"
groupName="device">
<label>Window Open Temp</label>
<description>Set the Window Open Temperature.</description>
<advanced>true</advanced>
</parameter>
<parameter name="windowOpenDuration" type="integer" min="0" max="60" step="5" required="false"
groupName="device">
<label>Window Open Duration</label>
<description>Set the Thermostat Window Open Duration.</description>
<advanced>true</advanced>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
<parameter name="refreshActualRate" type="integer" required="false" groupName="binding">
<label>Actual Temperature Refresh Rate</label>
<description>Experimental feature! Rate of the actual refresh in minutes. 0-9=refresh disabled. Minimum refresh rate
once/10 minutes, recommended 60min. The settings of the heating thermostats are changed to trigger an update of
temperature, as the actual temperature only is updated when the valves changes.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<!-- Trigger reset action from habmin menu -->
<parameter name="action-deviceDelete" type="integer" groupName="actions">
<label>Delete Device from Cube</label>
<description>Deletes the device from the MAX! Cube. Device will need to be
included again!</description>
<options>
<option value="1234">Delete</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<thing-type id="wallthermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>MAX! WallThermostat+</label>
<description>This is a MAX! WallThermostat+</description>
<channels>
<channel id="valve" typeId="valve"/>
<channel id="battery_low" typeId="system.low-battery"/>
<channel id="mode" typeId="mode"/>
<channel id="actual_temp" typeId="actual_temp"/>
<channel id="set_temp" typeId="set_temp"/>
<channel id="locked" typeId="locked"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
</parameter-group>
<!-- Trigger actions from habmin -->
<parameter-group name="actions">
<label>Actions</label>
<description>Action Buttons</description>
</parameter-group>
<parameter name="room" type="text" required="false" groupName="device">
<label>Room</label>
<description>The room name.</description>
</parameter>
<parameter name="name" type="text" required="false" groupName="device">
<label>Device Name</label>
<description>The device description.</description>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
<!-- Trigger reset action from habmin menu -->
<parameter name="action-deviceDelete" type="integer" groupName="actions">
<label>Delete Device from Cube</label>
<description>Deletes the device from the MAX! Cube. Device will need to be
included again!</description>
<options>
<option value="1234">Delete</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<thing-type id="ecoswitch">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>MAX! Ecoswitch</label>
<description>This is a MAX! EcoSwitch</description>
<channels>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
</parameter-group>
<parameter name="room" type="text" required="false" groupName="device">
<label>Room</label>
<description>The room name.</description>
</parameter>
<parameter name="name" type="text" required="false" groupName="device">
<label>Device Name</label>
<description>The device description.</description>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="shuttercontact">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>MAX! Shutter Contact</label>
<description>This is a MAX! Shutter Contact</description>
<channels>
<channel id="contact_state" typeId="contact_state"/>
<channel id="battery_low" typeId="system.low-battery"/>
</channels>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter-group name="identification">
<label>Identification</label>
<description>Hardware &amp; location identification</description>
</parameter-group>
<parameter-group name="device">
<label>Device Settings</label>
<description>Device parameter settings</description>
</parameter-group>
<!-- Trigger actions from habmin -->
<parameter-group name="actions">
<label>Actions</label>
<description>Action Buttons</description>
</parameter-group>
<parameter name="room" type="text" required="false" groupName="device">
<label>Room</label>
<description>The room name.</description>
</parameter>
<parameter name="name" type="text" required="false" groupName="device">
<label>Device Name</label>
<description>The device description.</description>
</parameter>
<parameter name="serialNumber" type="text" required="true" groupName="identification">
<label>Serial Number</label>
<description>The Serial Number identifies one specific device.</description>
</parameter>
<parameter name="rfAddress" type="text" required="false" groupName="identification">
<label>RF Address</label>
<description>The RF Address used for communication between the devices.</description>
</parameter>
<!-- Trigger reset action from habmin menu -->
<parameter name="action-deviceDelete" type="integer" groupName="actions">
<label>Delete Device from Cube</label>
<description>Deletes the device from the MAX! Cube. Device will need to be
included again!</description>
<options>
<option value="1234">Delete</option>
<option value="-1">No Action</option>
</options>
<default>-1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="valve" advanced="true">
<item-type>Number</item-type>
<label>Valve Position</label>
<description>Thermostat Valve Position</description>
<state pattern="%d %%" readOnly="true">
</state>
</channel-type>
<channel-type id="mode">
<item-type>String</item-type>
<label>Mode</label>
<description>Thermostat Mode Setting</description>
<tags>
<tag>heating</tag>
</tags>
<state pattern="%s" readOnly="false">
<options>
<option value="AUTOMATIC">AUTOMATIC</option>
<option value="MANUAL">MANUAL</option>
<option value="BOOST">BOOST</option>
<option value="VACATION">VACATION</option>
</options>
</state>
</channel-type>
<channel-type id="actual_temp">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<description>Current measured room temperature</description>
<category>Temperature</category>
<tags>
<tag>heating</tag>
</tags>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="set_temp">
<item-type>Number:Temperature</item-type>
<label>Setpoint Temperature</label>
<description>Thermostat Setpoint temperature</description>
<category>Temperature</category>
<tags>
<tag>heating</tag>
</tags>
<state min="4.5" max="30.5" step="0.5" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="locked" advanced="true">
<item-type>Contact</item-type>
<label>Thermostat Locked</label>
<description>Thermostat is locked for adjustments</description>
<category>Lock</category>
<state pattern="%s" readOnly="true"></state>
</channel-type>
<channel-type id="contact_state">
<item-type>Contact</item-type>
<label>Contact State</label>
<description>Contact state information</description>
<category>Contact</category>
<state pattern="%s" readOnly="true"></state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests cases for {@link FCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class FCommandTest {
@Test
public void prefixTest() {
FCommand scmd = new FCommand();
String commandStr = scmd.getCommandString();
String prefix = commandStr.substring(0, 2);
assertEquals("f:", prefix);
assertEquals("f:" + '\r' + '\n', commandStr);
}
@Test
public void baseCommandTest() {
FCommand scmd = new FCommand("ntp.homematic.com", "nl.ntp.pool.org");
String commandStr = scmd.getCommandString();
assertEquals("f:ntp.homematic.com,nl.ntp.pool.org" + '\r' + '\n', commandStr);
}
@Test
public void fCommandNullTest() {
FCommand scmd = new FCommand("ntp.homematic.com", null);
String commandStr = scmd.getCommandString();
assertEquals("f:ntp.homematic.com" + '\r' + '\n', commandStr);
scmd = new FCommand(null, "nl.ntp.pool.org");
commandStr = scmd.getCommandString();
assertEquals("f:nl.ntp.pool.org" + '\r' + '\n', commandStr);
scmd = new FCommand(null, null);
commandStr = scmd.getCommandString();
assertEquals("f:" + '\r' + '\n', commandStr);
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.device.DeviceConfiguration;
import org.openhab.binding.max.internal.device.RoomInformation;
import org.openhab.binding.max.internal.message.CMessage;
/**
* Tests cases for {@link MCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MCommandTest {
private List<DeviceConfiguration> configurations = new ArrayList<>();
private List<Device> devices = new ArrayList<>();
private List<RoomInformation> rooms = new ArrayList<>();
String deviceCMsg[] = {
"C:0b0da3,0gsNowIBEABLRVEwNTQ0MjQyLCQ9CQcYAzAM/wBIYViRSP1ZFE0gTSBNIEUgRSBFIEUgRSBFIEhhWJFQ/VkVUSBRIFEgRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIEhQWFpI/lkUTSBNIE0gRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIA==",
"C:08c1d6,0gjB1gEFGP9LRVEwNjQ5MzEyKyE9CQcYAzAM/wBEeFUgVSBVIFUgVSBVIEUgRSBFIEUgRSBFIER4VRZFIEUgRSBFIEUgRSBFIEUgRSBFIEUgRFFEYkTkTQ9FIEUgRSBFIEUgRSBFIEUgRSBEUURiRORND0UgRSBFIEUgRSBFIEUgRSBFIERRRGJE5E0PRSBFIEUgRSBFIEUgRSBFIEUgRFFEYkTkTQ9FIEUgRSBFIEUgRSBFIEUgRSBEUURiRORRGEUgRSBFIEUgRSBFIEUgRSBFIA==",
"C:0e75f6,EQ519gQCEABLRVExMTA0Mzgw",
"C:0f1d54,0g8dVAEAEKBMRVEwMTU1NTc4KiI9CQcYAzAM/wBESFUIRSBFIEUgRSBFIEUgRSBFIEUgRSBFIERIVQhFIEUgRSBFIEUgRSBFIEUgRSBFIEUgREhUbETMVRRFIEUgRSBFIEUgRSBFIEUgRSBESFRsRMxVFEUgRSBFIEUgRSBFIEUgRSBFIERIVGxEzFUURSBFIEUgRSBFIEUgRSBFIEUgREhUbETMVRRFIEUgRSBFIEUgRSBFIEUgRSBESFRsRMxVFEUgRSBFIEUgRSBFIEUgRSBFIA==" };
private void prepareDevices() {
// create a devices array
for (String cMsg : deviceCMsg) {
CMessage msg = new CMessage(cMsg);
// DeviceConfiguration c = null;
configurations.add(DeviceConfiguration.create(msg));
Device di = Device.create(msg.getRFAddress(), configurations);
devices.add(di);
}
}
@Test
public void prefixTest() {
prepareDevices();
MCommand mcmd = new MCommand(devices);
String commandStr = mcmd.getCommandString();
String prefix = commandStr.substring(0, 2);
assertEquals("m:", prefix);
}
@Test
public void baseCommandTest() {
prepareDevices();
MCommand mCmd = new MCommand(devices);
rooms = new ArrayList<>(mCmd.getRooms());
String commandStr = mCmd.getCommandString();
assertEquals(
"m:00,VgIDAQALDaMCAA519gUACMHWBAILDaNLRVEwNTQ0MjQyAAEBCMHWS0VRMDY0OTMxMgAFBA519ktFUTExMDQzODAAAgEPHVRMRVEwMTU1NTc4AAAB\r\n",
commandStr);
}
@Test
public void addRoomsTest() {
prepareDevices();
MCommand mCmd = new MCommand(devices);
rooms = new ArrayList<>(mCmd.getRooms());
RoomInformation room = new RoomInformation(3, "testroom", "0f1d54");
rooms.add(room);
mCmd = new MCommand(devices, rooms);
String commandStr = mCmd.getCommandString();
assertEquals(
"m:00,VgIEAQALDaMCAA519gMIdGVzdHJvb20PHVQFAAjB1gQCCw2jS0VRMDU0NDI0MgABAQjB1ktFUTA2NDkzMTIABQQOdfZLRVExMTA0MzgwAAIBDx1UTEVRMDE1NTU3OAAAAQ==\r\n",
commandStr);
devices.get(3).setRoomId(3);
devices.get(3).setName("Testroom");
mCmd = new MCommand(devices, rooms);
commandStr = mCmd.getCommandString();
assertEquals(
"m:00,VgIEAQALDaMCAA519gMIdGVzdHJvb20PHVQFAAjB1gQCCw2jS0VRMDU0NDI0MgABAQjB1ktFUTA2NDkzMTIABQQOdfZLRVExMTA0MzgwAAIBDx1UTEVRMDE1NTU3OAhUZXN0cm9vbQMB\r\n",
commandStr);
}
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.Utils;
import org.openhab.binding.max.internal.device.ThermostatModeType;
/**
* Tests cases for {@link SCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class SCommandTest {
@Test
public void prefixTest() {
SCommand scmd = new SCommand("0b0da3", 1, ThermostatModeType.MANUAL, 20.0);
String commandStr = scmd.getCommandString();
String prefix = commandStr.substring(0, 2);
assertEquals("s:", prefix);
}
@Test
public void baseCommandTest() {
SCommand scmd = new SCommand("0b0da3", 1, ThermostatModeType.MANUAL, 20.0);
String commandStr = scmd.getCommandString();
String base64Data = commandStr.substring(2).trim();
byte[] bytes = Base64.getDecoder().decode(base64Data.getBytes(StandardCharsets.UTF_8));
int[] data = new int[bytes.length];
for (int i = 0; i < bytes.length; i++) {
data[i] = bytes[i] & 0xFF;
}
String decodedString = Utils.toHex(data);
assertEquals("s:AARAAAAACw2jAWg=\r\n", commandStr);
assertEquals("0004400000000B0DA30168", decodedString);
}
@Test
public void boostModeTest() {
SCommand scmd = new SCommand("0b0da3", 1, ThermostatModeType.BOOST, 21.0);
String commandStr = scmd.getCommandString();
assertEquals("s:AARAAAAACw2jAeo=\r\n", commandStr);
}
@Test
public void autoModeTest() {
SCommand scmd = new SCommand("0b0da3", 1, ThermostatModeType.AUTOMATIC, 0);
String commandStr = scmd.getCommandString();
assertEquals("s:AARAAAAACw2jAQA=\r\n", commandStr);
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.command.SConfigCommand.ConfigCommandType;
/**
* Tests cases for {@link SConfigCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class SConfigCommandTest {
private static final String RF_TEST_ADDRESS = "0e15cc";
private static final int TEST_ROOM = 2;
@Test
public void setRoomTest() {
CubeCommand cubeCommand = new SConfigCommand(RF_TEST_ADDRESS, TEST_ROOM, ConfigCommandType.SetRoom);
String commandString = cubeCommand.getCommandString();
assertEquals("s:AAAiAAAADhXMAAI=\r\n", commandString);
}
@Test
public void removeRoomTest() {
CubeCommand cubeCommand = new SConfigCommand(RF_TEST_ADDRESS, 1, ConfigCommandType.RemoveRoom);
String commandString = cubeCommand.getCommandString();
assertEquals("s:AAAjAAAADhXMAAE=\r\n", commandString);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.Utils;
/**
* Tests cases for {@link TCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class TCommandTest {
@Test
public void prefixTest() {
TCommand scmd = new TCommand("0f1d54", false);
String commandStr = scmd.getCommandString();
String prefix = commandStr.substring(0, 2);
assertEquals("t:", prefix);
}
@Test
public void baseCommandTest() {
TCommand scmd = new TCommand("0f1d54", false);
String commandStr = scmd.getCommandString();
String base64Data = commandStr.split(",")[2];
byte[] bytes = Base64.getDecoder().decode(base64Data.trim().getBytes());
int[] data = new int[bytes.length];
for (int i = 0; i < bytes.length; i++) {
data[i] = bytes[i] & 0xFF;
}
String decodedString = Utils.toHex(data);
assertEquals("t:01,0,Dx1U\r\n", commandStr);
assertEquals("0F1D54", decodedString);
}
@Test
public void addRoomTest() {
TCommand scmd = new TCommand("0f1d54", false);
scmd.addRoom("0b0da3");
String commandStr = scmd.getCommandString();
String base64Data = commandStr.split(",")[2];
byte[] bytes = Base64.getDecoder().decode(base64Data.trim().getBytes());
int[] data = new int[bytes.length];
for (int i = 0; i < bytes.length; i++) {
data[i] = bytes[i] & 0xFF;
}
String decodedString = Utils.toHex(data);
assertEquals("t:02,0,Cw2jDx1U\r\n", commandStr);
assertEquals("0B0DA30F1D54", decodedString);
}
@Test
public void forceModeTest() {
TCommand scmd = new TCommand("0f1d54", true);
String commandStr = scmd.getCommandString();
assertEquals("t:01,1,Dx1U\r\n", commandStr);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.command;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests cases for {@link ZCommand}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class ZCommandTest {
@Test
public void prefixTest() {
ZCommand scmd = new ZCommand(ZCommand.WakeUpType.DEVICE, "0b0da3", 30);
String commandStr = scmd.getCommandString();
String prefix = commandStr.substring(0, 2);
assertEquals("z:", prefix);
}
@Test
public void baseCommandTest() {
ZCommand scmd = new ZCommand(ZCommand.WakeUpType.DEVICE, "0b0da3", 30);
String commandStr = scmd.getCommandString();
assertEquals("z:1E,D,0b0da3" + '\r' + '\n', commandStr);
}
@Test
public void wakeAllTest() {
ZCommand scmd = new ZCommand(ZCommand.WakeUpType.ALL, "0b0da3", 60);
String commandStr = scmd.getCommandString();
assertEquals("z:3C,A" + '\r' + '\n', commandStr);
scmd = ZCommand.wakeupAllDevices();
commandStr = scmd.getCommandString();
assertEquals("z:1E,A" + '\r' + '\n', commandStr);
scmd = ZCommand.wakeupAllDevices(60);
commandStr = scmd.getCommandString();
assertEquals("z:3C,A" + '\r' + '\n', commandStr);
}
@Test
public void wakeRoomTest() {
ZCommand scmd = new ZCommand(ZCommand.WakeUpType.ROOM, "01", 30);
String commandStr = scmd.getCommandString();
assertEquals("z:1E,G,01" + '\r' + '\n', commandStr);
scmd = ZCommand.wakeupRoom(1);
commandStr = scmd.getCommandString();
assertEquals("z:1E,G,01" + '\r' + '\n', commandStr);
scmd = ZCommand.wakeupRoom(2, 60);
commandStr = scmd.getCommandString();
assertEquals("z:3C,G,02" + '\r' + '\n', commandStr);
}
@Test
public void wakeDeviceTest() {
ZCommand scmd = ZCommand.wakeupDevice("0b0da3");
String commandStr = scmd.getCommandString();
assertEquals("z:1E,D,0b0da3" + '\r' + '\n', commandStr);
scmd = ZCommand.wakeupDevice("0b0da3", 60);
commandStr = scmd.getCommandString();
assertEquals("z:3C,D,0b0da3" + '\r' + '\n', commandStr);
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.device.DeviceType;
/**
* Tests cases for {@link CMessage}.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 Version and updates
*/
@NonNullByDefault
public class CMessageTest {
public static final String RAW_DATA = "C:0b0da3,0gsNowIBEABLRVEwNTQ0MjQyLCQ9CQcYAzAM/wBIYViRSP1ZFE0gTSBNIEUgRSBFIEUgRSBFIEhhWJFQ/VkVUSBRIFEgRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIEhQWFpI/lkUTSBNIE0gRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIA==";
private final CMessage message = new CMessage(RAW_DATA);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.C, messageType);
}
@Test
public void getRFAddressTest() {
String rfAddress = message.getRFAddress();
assertEquals("0b0da3", rfAddress);
}
@Test
public void getDeviceTypeTest() {
DeviceType deviceType = message.getDeviceType();
assertEquals(DeviceType.HeatingThermostatPlus, deviceType);
}
@Test
public void getSerialNumberTes() {
String serialNumber = message.getSerialNumber();
assertEquals("KEQ0544242", serialNumber);
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.max.internal.device.DeviceConfiguration;
import org.openhab.binding.max.internal.device.DeviceType;
/**
* Tests cases for {@link DeviceConfiguration}.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 Version and updates
*/
@NonNullByDefault
public class ConfigurationTest {
public static final String RAW_DATA = "C:0b0da3,0gsNowIBEABLRVEwNTQ0MjQyLCQ9CQcYAzAM/wBIYViRSP1ZFE0gTSBNIEUgRSBFIEUgRSBFIEhhWJFQ/VkVUSBRIFEgRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIEhQWFpI/lkUTSBNIE0gRSBFIEUgRSBFIEUgSFBYWkj+WRRNIE0gTSBFIEUgRSBFIEUgRSBIUFhaSP5ZFE0gTSBNIEUgRSBFIEUgRSBFIA==";
private final CMessage message = new CMessage(RAW_DATA);
private @Nullable DeviceConfiguration configuration;
@Before
public void before() {
configuration = DeviceConfiguration.create(message);
}
@Test
public void createTest() {
assertNotNull(configuration);
}
@Test
public void getRfAddressTest() {
final DeviceConfiguration configuration = this.configuration;
if (configuration != null) {
String rfAddress = configuration.getRFAddress();
assertEquals("0b0da3", rfAddress);
} else {
fail("Configuration missing");
}
}
@Test
public void getDeviceTypeTest() {
final DeviceConfiguration configuration = this.configuration;
if (configuration != null) {
DeviceType deviceType = configuration.getDeviceType();
assertEquals(DeviceType.HeatingThermostatPlus, deviceType);
} else {
fail("Configuration missing");
}
}
@Test
public void getSerialNumberTest() {
final DeviceConfiguration configuration = this.configuration;
if (configuration != null) {
String serialNumber = configuration.getSerialNumber();
assertEquals("KEQ0544242", serialNumber);
} else {
fail("Configuration missing");
}
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests cases for {@link FMessage}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class FMessageTest {
public static final String RAW_DATA = "F:nl.ntp.pool.org,ntp.homematic.com";
private final FMessage message = new FMessage(RAW_DATA);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.F, messageType);
}
@Test
public void getServer1Test() {
String ntpServer1 = message.getNtpServer1();
assertEquals("nl.ntp.pool.org", ntpServer1);
}
@Test
public void getServer2Test() {
String ntpServer1 = message.getNtpServer2();
assertEquals("ntp.homematic.com", ntpServer1);
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.Utils;
/**
* Tests cases for {@link HMessage}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class HMessageTest {
public static final String RAW_DATA = "H:KEQ0565026,0b5951,0113,00000000,4eed6795,01,32,12080a,070f,03,0000";
private final HMessage message = new HMessage(RAW_DATA);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.H, messageType);
}
@Test
public void getRFAddressTest() {
String rfAddress = message.getRFAddress();
assertEquals("0b5951", rfAddress);
}
@Test
public void getFirmwareTest() {
String firmware = message.getFirmwareVersion();
assertEquals("01.13", firmware);
}
@Test
public void getConnectionIdTest() {
String connectionId = message.getConnectionId();
assertEquals("4eed6795", connectionId);
}
@Test
public void getCubeTimeStateTest() {
String cubeTimeState = message.getCubeTimeState();
assertEquals("03", cubeTimeState);
}
@Test
public void testParseDateTime() {
String[] tokens = RAW_DATA.split(Message.DELIMETER);
String hexDate = tokens[7];
String hexTime = tokens[8];
int year = Utils.fromHex(hexDate.substring(0, 2));
int month = Utils.fromHex(hexDate.substring(2, 4));
int dayOfMonth = Utils.fromHex(hexDate.substring(4, 6));
assertEquals(18, year);
assertEquals(8, month);
assertEquals(10, dayOfMonth);
int hours = Utils.fromHex(hexTime.substring(0, 2));
int minutes = Utils.fromHex(hexTime.substring(2, 4));
assertEquals(7, hours);
assertEquals(15, minutes);
}
@Test
public void testGetDateTime() {
Date dateTime = message.getDateTime();
assertEquals(Date.from(ZonedDateTime.of(2018, 8, 10, 7, 15, 0, 0, ZoneId.systemDefault()).toInstant()),
dateTime);
}
@Test
public void getNTPCounterTest() {
String ntpCounter = message.getNTPCounter();
assertEquals("0", ntpCounter);
}
@Test
public void getSerialNumberTest() {
String serialNumber = message.getSerialNumber();
assertEquals("KEQ0565026", serialNumber);
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Before;
import org.junit.Test;
import org.openhab.binding.max.internal.device.Device;
import org.openhab.binding.max.internal.device.DeviceConfiguration;
import org.openhab.binding.max.internal.device.DeviceInformation;
import org.openhab.binding.max.internal.device.DeviceType;
import org.openhab.binding.max.internal.device.HeatingThermostat;
import org.openhab.binding.max.internal.device.ShutterContact;
/**
* Tests cases for {@link LMessage}.
*
* @author Dominic Lerbs - Initial contribution
* @author Christoph Weitkamp - OH2 Version and updates
*/
@NonNullByDefault
public class LMessageTest {
private static final String RAWDATA = "L:BgVPngkSEAsLhBkJEhkLJQDAAAsLhwwJEhkRJwDKAAYO8ZIJEhAGBU+kCRIQCwxuRPEaGQMmAMcACwxuQwkSGQgnAM8ACwQd5t0SGQ0oAMsA";
private final Map<String, Device> testDevices = new HashMap<>();
private final LMessage message = new LMessage(RAWDATA);
private final List<DeviceConfiguration> configurations = new ArrayList<>();
@Before
public void setUp() {
createTestDevices();
}
private void createTestDevices() {
addShutterContact("054f9e");
addShutterContact("0ef192");
addShutterContact("054fa4");
addHeatingThermostat("0b8419");
addHeatingThermostat("0b870c");
addHeatingThermostat("0c6e43");
addHeatingThermostat("041de6");
addHeatingThermostat("0c6e44").setError(true);
}
private ShutterContact addShutterContact(String rfAddress) {
ShutterContact device = new ShutterContact(createConfiguration(DeviceType.ShutterContact, rfAddress));
testDevices.put(rfAddress, device);
return device;
}
private HeatingThermostat addHeatingThermostat(String rfAddress) {
HeatingThermostat device = new HeatingThermostat(createConfiguration(DeviceType.HeatingThermostat, rfAddress));
testDevices.put(rfAddress, device);
return device;
}
private DeviceConfiguration createConfiguration(DeviceType type, String rfAddress) {
DeviceConfiguration configuration = DeviceConfiguration
.create(new DeviceInformation(type, "", rfAddress, "", 1));
configurations.add(configuration);
return configuration;
}
@Test
public void isCorrectMessageType() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.L, messageType);
}
@Test
public void allDevicesCreatedFromMessage() {
Collection<? extends Device> devices = message.getDevices(configurations);
assertEquals("Incorrect number of devices created", testDevices.size(), devices.size());
for (Device device : devices) {
assertTrue("Unexpected device created: " + device.getRFAddress(),
testDevices.containsKey(device.getRFAddress()));
}
}
@Test
public void isCorrectErrorState() {
for (Device device : message.getDevices(configurations)) {
Device testDevice = testDevices.get(device.getRFAddress());
assertEquals("Error set incorrectly in Device", testDevice.isError(), device.isError());
}
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.device.DeviceInformation;
import org.openhab.binding.max.internal.device.DeviceType;
import org.openhab.binding.max.internal.device.RoomInformation;
/**
* Tests cases for {@link MMessage}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class MMessageTest {
public static final String RAW_DATA = "M:00,01,VgIFAQhiYWRrYW1lcgsNowIMU3R1ZGVlcmthbWVyB7bnAwlXb29ua2FtZXIL6aIEDFN6b25qYSBLYW1lcgjDSQUGWm9sZGVyCMHWCAILDaNLRVEwNTQ0MjQyEUJhZGthbWVyIFJhZGlhdG9yAQEHtudLRVEwMTQ1MTcyFVJhZGlhdG9yIFN0dWRlZXJrYW1lcgIDDhXMTEVRMDAxNTM0MBlXYWxsIFRoZXJtb3N0YXQgV29vbmthbWVyAwEL6aJLRVE5MDE1NDMyG1BsdWcgQWRhcHRlciBNdXVydmVyd2FybWluZwMFBDNvSkVRMDM4MDg3OBdFY28gU3dpdGNoIFN0dWRlZXJrYW1lcgAEDnX2S0VRMTEwNDM4MBpXaW5kb3cgU2Vuc29yIFN0dWRlZXJrYW1lcgIBCMNJS0VRMDY0ODk0ORJUaGVybW9zdGFhdCBTem9uamEEAQjB1ktFUTA2NDkzMTIRU3R1ZGVlcmthbWVyIElybWEFAQ==";
private final MMessage message = new MMessage(RAW_DATA);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.M, messageType);
}
@Test
public void deviceInformationTest() {
List<DeviceInformation> allDevicesInformation = message.devices;
assertEquals(8, allDevicesInformation.size());
DeviceInformation deviceInformation = allDevicesInformation.get(0);
assertEquals("Badkamer Radiator", deviceInformation.getName());
assertEquals("0B0DA3", deviceInformation.getRFAddress());
assertEquals(1, deviceInformation.getRoomId());
assertEquals("KEQ0544242", deviceInformation.getSerialNumber());
assertEquals(DeviceType.HeatingThermostatPlus, deviceInformation.getDeviceType());
}
@Test
public void deviceInformationTypeTest1() {
List<DeviceInformation> allDevicesInformation = message.devices;
DeviceInformation deviceInformation = allDevicesInformation.get(1);
assertEquals(DeviceType.HeatingThermostat, deviceInformation.getDeviceType());
}
@Test
public void deviceInformationTypeTest2() {
List<DeviceInformation> allDevicesInformation = message.devices;
DeviceInformation deviceInformation = allDevicesInformation.get(2);
assertEquals(DeviceType.WallMountedThermostat, deviceInformation.getDeviceType());
}
@Test
public void deviceInformationTypeTest3() {
List<DeviceInformation> allDevicesInformation = message.devices;
DeviceInformation deviceInformation = allDevicesInformation.get(4);
assertEquals(DeviceType.EcoSwitch, deviceInformation.getDeviceType());
}
@Test
public void deviceInformationTypeTest4() {
List<DeviceInformation> allDevicesInformation = message.devices;
DeviceInformation deviceInformation = allDevicesInformation.get(5);
assertEquals(DeviceType.ShutterContact, deviceInformation.getDeviceType());
}
@Test
public void roomInformationTest() {
List<RoomInformation> roomInformation = message.rooms;
assertEquals(5, roomInformation.size());
assertEquals("badkamer", roomInformation.get(0).getName());
}
}

View File

@@ -0,0 +1,207 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Assert;
import org.junit.Test;
import org.openhab.binding.max.internal.exceptions.IncompleteMessageException;
import org.openhab.binding.max.internal.exceptions.IncorrectMultilineIndexException;
import org.openhab.binding.max.internal.exceptions.MessageIsWaitingException;
import org.openhab.binding.max.internal.exceptions.NoMessageAvailableException;
import org.openhab.binding.max.internal.exceptions.UnprocessableMessageException;
import org.openhab.binding.max.internal.exceptions.UnsupportedMessageTypeException;
/**
* @author Christian Rockrohr <christian@rockrohr.de> - Initial contribution
*/
@NonNullByDefault
public class MessageProcessorTest {
private final MessageProcessor processor = new MessageProcessor();
private void commonMessageTest(String line, Message expectedMessage) throws Exception {
Assert.assertTrue(this.processor.addReceivedLine(line));
Assert.assertTrue(this.processor.isMessageAvailable());
Message message = this.processor.pull();
Assert.assertNotNull(message);
Assert.assertEquals(message.getClass().getName(), expectedMessage.getClass().getName());
Assert.assertEquals(expectedMessage.getPayload(), message.getPayload());
}
@Test
public void testS_Message() throws Exception {
String rawData = "S:03,0,30";
SMessage expectedMessage = new SMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testN_Message() throws Exception {
String rawData = "N:Aw4VzExFUTAwMTUzNDD/";
NMessage expectedMessage = new NMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testA_Message() throws Exception {
String rawData = "A:";
AMessage expectedMessage = new AMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testC_Message() throws Exception {
String rawData = "C:0ff1bc,EQ/xvAQJEAJMRVEwNzk0MDA3";
CMessage expectedMessage = new CMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testL_Message() throws Exception {
String rawData = "L:Bg/xvAkAAA==";
LMessage expectedMessage = new LMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testH_Message() throws Exception {
String rawData = "H:KHA0007199,081dd4,0113,00000000,0d524351,10,30,0f0407,1130,03,0000";
HMessage expectedMessage = new HMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testSingleM_Message() throws Exception {
String rawData = "M:00,01,VgIBAQpXb2huemltbWVyAAAAAQMQV6lMRVEwOTgyMTU2DldhbmR0aGVybW9zdGF0AQE=";
MMessage expectedMessage = new MMessage(rawData);
commonMessageTest(rawData, expectedMessage);
}
@Test
public void testMultilineM_Message() throws Exception {
String line1_part1 = "M:00,02,";
String line1_part2 = "VgIMAQpXb2huemltbWVyCvMrAgtUb2lsZXR0ZSBFRwrenQMOVG9pbGV0dGUgMS4gT0cK3rgECkJhZGV6aW1tZXIK3qoFDFNjaGxhZnppbW1lcgresQYDSmFuD4lCBwlDaHJpc3RpbmEPiTYIBEZsdXIPiT0KEEJhZGV6aW1tZXIgMi4gT0cPiRwLBULDvHJvD4k/DAxHw6RzdGV6aW1tZXIPiRoJC1dhc2Noa8O8Y2hlD4lXNgQHOCtLRVEwMTg4NjczCFRlcnJhc3NlAQQHMblLRVEwMTg3MTkwCEZsdXJ0w7xyAQIK8ytLRVEwMzc5NTg3C1dhbmRoZWl6dW5nAQIK9P9LRVEwMzgwMDU1DkZlbnN0ZXJoZWl6dW5nAQQHMbtLRVEwMTg3MTg4CEZsdXJ0w7xyAgQHMuxLRVEwMTg2ODg0B0ZlbnN0ZXICAQrenUtFUTA0MDY5NjIHSGVpenVuZwIBCt64S0VRMDQwNjk4OQdIZWl6dW5nAwQIFGdLRVEwMTkwNTc3B0ZlbnN0ZXIDBAc2l0tFUTAxODU5NDUIRmx1cnTDvHIEAQreqktFUTA0MDY5NzUHSGVpenVuZwQBCt8JS0VRMDQwNzA3MA5IYW5kdHVjaGVpenVuZwQEBzhTS0VRMDE4ODcxMAdGZW5zdGVyBAQIFIxLRVEwMTkwNTQzFkZlbnN0ZXIgU3RyYcOfZSByZWNodHMFAQresUtFUTA0MDY5ODIHSGVpenVuZwUEBzHmS0VRMDE4NzE0NhVGZW5zdGVyIFN0cmHDn2UgbGlua3MFAxBXqUxFUTA5ODIxNTYOV2FuZHRoZXJtb3N0YXQBBA/u1ExFUTA3OTQ3NTIIRmx1cnTDvHIGBA/v6kxFUTA3OTQ0NzQNRmVuc3RlciBsaW5rcwYED/HnTEVRMDc5Mzk2NA5GZW5zdGVyIHJlY2h0cwYBD4lCTEVRMTAwNDYwMAdIZWl6dW5nBgQP9BVMRVEwNzkzNDA2CEZsdXJ0w7xyBwQP79FMRVEwNzk0NDk5B0ZlbnN0ZXIHAQ+JNkxFUTEwMDQ1ODgHSGVpenVuZwcBD4k9TEVRMTAwNDU5NQ1IZWl6dW5nIHVudGVuCAEPiRxMRVExMDA0NTYyB0hlaXp1bmcKBA/yTUxFUTA3OTM4NjIHRmVuc3RlcgoED/F+TEVRMDc5NDA2OQhGbHVydMO8cgoBD4k/TEVRMTAwNDU5NwdIZWl6dW5nCwQP8YdMRVEwNzk0MDYwB0ZlbnN0ZXILBA/xSExFUTA3OTQxMjQIRmx1cnTDvHILBA/yVkxFUTA3OTM4NTMURmVuc3RlciBHYXJ0ZW4gbGlua3MMBA/yI0xFUTA3OTM5MDQVRmVuc3RlciBHYXJ0ZW4gcmVjaHRzDAEPiRpMRVExMDA0NTYwB0hlaXp1bmcMBA/vj0xFUTA3OTQ1NjUPRmVuc3RlciBTdHJhw59lDAQP8CtMRVEwNzk0NDA5BFTDvHIDBAgUa0tFUTAxODcwNjkNRmVuc3RlciBTZWl0ZQUEBzagS0VRMDE4NTkzNhVGZW5zdGVyIFN0cmHDn2UgbGlua3MBBA/wI0xFUTA3OTQ0MTYORmVuc3RlciBLw7xjaGUBAxBV50xFUTA5ODI2NzYOV2FuZHRoZXJtb3N0YXQFAxBW2kxFUTA5ODIzNjgOV2FuZHRoZXJtb3N0YXQEAxBV4kxFUTA5ODI2NzEOV2FuZHRoZXJtb3N0YXQHAxBZWExFUTA5ODE3MjkOV2FuZHRoZXJtb3N0YXQMAxBV6ExFUTA5ODI2NzcOV2FuZHRoZXJtb3N0YXQGAxBV40xFUTA5ODI2NzIOV2FuZHRoZXJtb3N0YXQKBAcxoEtFUTAxODcyMTYLV2FzY2hrw7xjaGUF";
String line1 = line1_part1 + line1_part2;
String line2_part1 = "M:01,02,";
String line2_part2 = "AxBV8ExFUTA5ODI2ODUOV2FuZHRoZXJtb3N0YXQJBA/v50xFUTA3OTQ0NzcNQmFsa29uZmVuc3RlcgkBD4lXTEVRMTAwNDYyMRZIZWl6dW5nIHVudGVybSBGZW5zdGVyCQQP8llMRVEwNzkzODUwDkZlbnN0ZXIgcmVjaHRzCQQP8bxMRVEwNzk0MDA3DUZlbnN0ZXIgbGlua3MJAQ+JOExFUTEwMDQ1OTAOSGVpenVuZyBCYWxrb24JBA/yLExFUTA3OTM4OTUKQmFsa29udMO8cgkED++zTEVRMDc5NDUyOQhGbHVydMO8cgkB";
String line2 = line2_part1 + line2_part2;
String expectedString = line1 + line2_part2;
MMessage expectedMessage = new MMessage(expectedString);
Assert.assertFalse(this.processor.addReceivedLine(line1));
Assert.assertTrue(this.processor.addReceivedLine(line2));
Message message = this.processor.pull();
Assert.assertNotNull(message);
Assert.assertEquals(message.getClass().getName(), MMessage.class.getName());
Assert.assertEquals(expectedMessage.getPayload(), message.getPayload());
}
@Test
public void testWrongIndexOfMultilineM_Message() throws Exception {
String line1 = "M:00,02,VgIMAQpXb2huemltbWVyCvMrAgtUb2lsZXR0ZSBFRwrenQMOVG9pbGV0dGUgMS4gT0cK3rgECkJhZGV6aW1tZXIK3qoFDFNjaGxhZnppbW1lcgresQYDSmFuD4lCBwlDaHJpc3RpbmEPiTYIBEZsdXIPiT0KEEJhZGV6aW1tZXIgMi4gT0cPiRwLBULDvHJvD4k/DAxHw6RzdGV6aW1tZXIPiRoJC1dhc2Noa8O8Y2hlD4lXNgQHOCtLRVEwMTg4NjczCFRlcnJhc3NlAQQHMblLRVEwMTg3MTkwCEZsdXJ0w7xyAQIK8ytLRVEwMzc5NTg3C1dhbmRoZWl6dW5nAQIK9P9LRVEwMzgwMDU1DkZlbnN0ZXJoZWl6dW5nAQQHMbtLRVEwMTg3MTg4CEZsdXJ0w7xyAgQHMuxLRVEwMTg2ODg0B0ZlbnN0ZXICAQrenUtFUTA0MDY5NjIHSGVpenVuZwIBCt64S0VRMDQwNjk4OQdIZWl6dW5nAwQIFGdLRVEwMTkwNTc3B0ZlbnN0ZXIDBAc2l0tFUTAxODU5NDUIRmx1cnTDvHIEAQreqktFUTA0MDY5NzUHSGVpenVuZwQBCt8JS0VRMDQwNzA3MA5IYW5kdHVjaGVpenVuZwQEBzhTS0VRMDE4ODcxMAdGZW5zdGVyBAQIFIxLRVEwMTkwNTQzFkZlbnN0ZXIgU3RyYcOfZSByZWNodHMFAQresUtFUTA0MDY5ODIHSGVpenVuZwUEBzHmS0VRMDE4NzE0NhVGZW5zdGVyIFN0cmHDn2UgbGlua3MFAxBXqUxFUTA5ODIxNTYOV2FuZHRoZXJtb3N0YXQBBA/u1ExFUTA3OTQ3NTIIRmx1cnTDvHIGBA/v6kxFUTA3OTQ0NzQNRmVuc3RlciBsaW5rcwYED/HnTEVRMDc5Mzk2NA5GZW5zdGVyIHJlY2h0cwYBD4lCTEVRMTAwNDYwMAdIZWl6dW5nBgQP9BVMRVEwNzkzNDA2CEZsdXJ0w7xyBwQP79FMRVEwNzk0NDk5B0ZlbnN0ZXIHAQ+JNkxFUTEwMDQ1ODgHSGVpenVuZwcBD4k9TEVRMTAwNDU5NQ1IZWl6dW5nIHVudGVuCAEPiRxMRVExMDA0NTYyB0hlaXp1bmcKBA/yTUxFUTA3OTM4NjIHRmVuc3RlcgoED/F+TEVRMDc5NDA2OQhGbHVydMO8cgoBD4k/TEVRMTAwNDU5NwdIZWl6dW5nCwQP8YdMRVEwNzk0MDYwB0ZlbnN0ZXILBA/xSExFUTA3OTQxMjQIRmx1cnTDvHILBA/yVkxFUTA3OTM4NTMURmVuc3RlciBHYXJ0ZW4gbGlua3MMBA/yI0xFUTA3OTM5MDQVRmVuc3RlciBHYXJ0ZW4gcmVjaHRzDAEPiRpMRVExMDA0NTYwB0hlaXp1bmcMBA/vj0xFUTA3OTQ1NjUPRmVuc3RlciBTdHJhw59lDAQP8CtMRVEwNzk0NDA5BFTDvHIDBAgUa0tFUTAxODcwNjkNRmVuc3RlciBTZWl0ZQUEBzagS0VRMDE4NTkzNhVGZW5zdGVyIFN0cmHDn2UgbGlua3MBBA/wI0xFUTA3OTQ0MTYORmVuc3RlciBLw7xjaGUBAxBV50xFUTA5ODI2NzYOV2FuZHRoZXJtb3N0YXQFAxBW2kxFUTA5ODIzNjgOV2FuZHRoZXJtb3N0YXQEAxBV4kxFUTA5ODI2NzEOV2FuZHRoZXJtb3N0YXQHAxBZWExFUTA5ODE3MjkOV2FuZHRoZXJtb3N0YXQMAxBV6ExFUTA5ODI2NzcOV2FuZHRoZXJtb3N0YXQGAxBV40xFUTA5ODI2NzIOV2FuZHRoZXJtb3N0YXQKBAcxoEtFUTAxODcyMTYLV2FzY2hrw7xjaGUF";
String line2 = "M:02,02,AxBV8ExFUTA5ODI2ODUOV2FuZHRoZXJtb3N0YXQJBA/v50xFUTA3OTQ0NzcNQmFsa29uZmVuc3RlcgkBD4lXTEVRMTAwNDYyMRZIZWl6dW5nIHVudGVybSBGZW5zdGVyCQQP8llMRVEwNzkzODUwDkZlbnN0ZXIgcmVjaHRzCQQP8bxMRVEwNzk0MDA3DUZlbnN0ZXIgbGlua3MJAQ+JOExFUTEwMDQ1OTAOSGVpenVuZyBCYWxrb24JBA/yLExFUTA3OTM4OTUKQmFsa29udMO8cgkED++zTEVRMDc5NDUyOQhGbHVydMO8cgkB";
try {
this.processor.addReceivedLine(line2);
Assert.fail("Expected exception was not thrown.");
} catch (IncorrectMultilineIndexException e) {
// OK, correct Exception was thrown
}
try {
this.processor.reset();
Assert.assertFalse(this.processor.addReceivedLine(line1));
this.processor.addReceivedLine(line2);
Assert.fail("Expected exception was not thrown.");
} catch (IncorrectMultilineIndexException e) {
// OK, correct Exception was thrown
}
}
@Test
public void testPullBeforeNewLine() throws Exception {
String line1 = "H:KHA0007199,081dd4,0113,00000000,0d524351,10,30,0f0407,1130,03,0000";
String line2 = "M:00,01,VgIBAQpXb2huemltbWVyAAAAAQMQV6lMRVEwOTgyMTU2DldhbmR0aGVybW9zdGF0AQE=";
try {
Assert.assertTrue(this.processor.addReceivedLine(line1));
this.processor.addReceivedLine(line2);
Assert.fail("Expected exception was not thrown.");
} catch (MessageIsWaitingException e) {
// OK, correct Exception was thrown
}
Message message = this.processor.pull();
Assert.assertNotNull(message);
Assert.assertEquals(message.getClass().getName(), HMessage.class.getName());
this.processor.addReceivedLine(line2);
message = this.processor.pull();
Assert.assertNotNull(message);
Assert.assertEquals(message.getClass().getName(), MMessage.class.getName());
}
@Test
public void testWrongIndicatorWhileMultilineProcessing() throws Exception {
String line1 = "M:00,02,VgIMAQpXb2huemltbWVyCvMrAgtUb2lsZXR0ZSBFRwrenQMOVG9pbGV0dGUgMS4gT0cK3rgECkJhZGV6aW1tZXIK3qoFDFNjaGxhZnppbW1lcgresQYDSmFuD4lCBwlDaHJpc3RpbmEPiTYIBEZsdXIPiT0KEEJhZGV6aW1tZXIgMi4gT0cPiRwLBULDvHJvD4k/DAxHw6RzdGV6aW1tZXIPiRoJC1dhc2Noa8O8Y2hlD4lXNgQHOCtLRVEwMTg4NjczCFRlcnJhc3NlAQQHMblLRVEwMTg3MTkwCEZsdXJ0w7xyAQIK8ytLRVEwMzc5NTg3C1dhbmRoZWl6dW5nAQIK9P9LRVEwMzgwMDU1DkZlbnN0ZXJoZWl6dW5nAQQHMbtLRVEwMTg3MTg4CEZsdXJ0w7xyAgQHMuxLRVEwMTg2ODg0B0ZlbnN0ZXICAQrenUtFUTA0MDY5NjIHSGVpenVuZwIBCt64S0VRMDQwNjk4OQdIZWl6dW5nAwQIFGdLRVEwMTkwNTc3B0ZlbnN0ZXIDBAc2l0tFUTAxODU5NDUIRmx1cnTDvHIEAQreqktFUTA0MDY5NzUHSGVpenVuZwQBCt8JS0VRMDQwNzA3MA5IYW5kdHVjaGVpenVuZwQEBzhTS0VRMDE4ODcxMAdGZW5zdGVyBAQIFIxLRVEwMTkwNTQzFkZlbnN0ZXIgU3RyYcOfZSByZWNodHMFAQresUtFUTA0MDY5ODIHSGVpenVuZwUEBzHmS0VRMDE4NzE0NhVGZW5zdGVyIFN0cmHDn2UgbGlua3MFAxBXqUxFUTA5ODIxNTYOV2FuZHRoZXJtb3N0YXQBBA/u1ExFUTA3OTQ3NTIIRmx1cnTDvHIGBA/v6kxFUTA3OTQ0NzQNRmVuc3RlciBsaW5rcwYED/HnTEVRMDc5Mzk2NA5GZW5zdGVyIHJlY2h0cwYBD4lCTEVRMTAwNDYwMAdIZWl6dW5nBgQP9BVMRVEwNzkzNDA2CEZsdXJ0w7xyBwQP79FMRVEwNzk0NDk5B0ZlbnN0ZXIHAQ+JNkxFUTEwMDQ1ODgHSGVpenVuZwcBD4k9TEVRMTAwNDU5NQ1IZWl6dW5nIHVudGVuCAEPiRxMRVExMDA0NTYyB0hlaXp1bmcKBA/yTUxFUTA3OTM4NjIHRmVuc3RlcgoED/F+TEVRMDc5NDA2OQhGbHVydMO8cgoBD4k/TEVRMTAwNDU5NwdIZWl6dW5nCwQP8YdMRVEwNzk0MDYwB0ZlbnN0ZXILBA/xSExFUTA3OTQxMjQIRmx1cnTDvHILBA/yVkxFUTA3OTM4NTMURmVuc3RlciBHYXJ0ZW4gbGlua3MMBA/yI0xFUTA3OTM5MDQVRmVuc3RlciBHYXJ0ZW4gcmVjaHRzDAEPiRpMRVExMDA0NTYwB0hlaXp1bmcMBA/vj0xFUTA3OTQ1NjUPRmVuc3RlciBTdHJhw59lDAQP8CtMRVEwNzk0NDA5BFTDvHIDBAgUa0tFUTAxODcwNjkNRmVuc3RlciBTZWl0ZQUEBzagS0VRMDE4NTkzNhVGZW5zdGVyIFN0cmHDn2UgbGlua3MBBA/wI0xFUTA3OTQ0MTYORmVuc3RlciBLw7xjaGUBAxBV50xFUTA5ODI2NzYOV2FuZHRoZXJtb3N0YXQFAxBW2kxFUTA5ODIzNjgOV2FuZHRoZXJtb3N0YXQEAxBV4kxFUTA5ODI2NzEOV2FuZHRoZXJtb3N0YXQHAxBZWExFUTA5ODE3MjkOV2FuZHRoZXJtb3N0YXQMAxBV6ExFUTA5ODI2NzcOV2FuZHRoZXJtb3N0YXQGAxBV40xFUTA5ODI2NzIOV2FuZHRoZXJtb3N0YXQKBAcxoEtFUTAxODcyMTYLV2FzY2hrw7xjaGUF";
String line2 = "H:KHA0007199,081dd4,0113,00000000,0d524351,10,30,0f0407,1130,03,0000";
try {
Assert.assertFalse(this.processor.addReceivedLine(line1));
this.processor.addReceivedLine(line2);
Assert.fail("Expected exception was not thrown.");
} catch (IncompleteMessageException e) {
// OK, correct Exception was thrown
}
}
@Test
public void testUnknownMessageIndicator() throws Exception {
String rawData = "X:0ff1bc,EQ/xvAQJEAJMRVEwNzk0MDA3";
try {
Assert.assertFalse(this.processor.addReceivedLine(rawData));
Assert.fail("Expected exception was not thrown.");
} catch (UnsupportedMessageTypeException e) {
// OK, correct Exception was thrown
}
}
@Test
public void testNoMessageAvailable() throws Exception {
String rawData = "M:00,01,VgIBAQpXb2huemltbWVyAAAAAQMQV6lMRVEwOTgyMTU2DldhbmR0aGVybW9zdGF0AQE=";
MMessage expectedMessage = new MMessage(rawData);
commonMessageTest(rawData, expectedMessage);
try {
this.processor.pull();
Assert.fail("Expected exception was not thrown.");
} catch (NoMessageAvailableException e) {
// OK, correct Exception was thrown
}
}
@Test
public void testUnprocessableMessage() throws Exception {
String line1 = "M:00"; // Some information are missing.
try {
this.processor.addReceivedLine(line1);
Assert.fail("Expected exception was not thrown.");
} catch (UnprocessableMessageException e) {
// OK, correct Exception was thrown
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.device.DeviceType;
/**
* Tests cases for {@link NMessage}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class NMessageTest {
public static final String RAW_DATA = "N:Aw4VzExFUTAwMTUzNDD/";
// public final String rawData = "N:AQe250tFUTAxNDUxNzL/";
private NMessage message = new NMessage(RAW_DATA);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message).getType();
assertEquals(MessageType.N, messageType);
}
@Test
public void getRFAddressTest() {
String rfAddress = message.getRfAddress();
assertEquals("0E15CC", rfAddress);
}
@Test
public void getSerialNumberTest() {
String serialNumber = message.getSerialNumber();
assertEquals("LEQ0015340", serialNumber);
}
@Test
public void getDeviceTypeTest() {
DeviceType deviceType = message.getDeviceType();
assertEquals(DeviceType.WallMountedThermostat, deviceType);
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
/**
* Tests cases for {@link FMessage}.
*
* @author Marcel Verpaalen - Initial contribution
*/
@NonNullByDefault
public class SMessageTest {
private static final String RAW_DATA_1 = "S:01,0,31";
private static final String RAW_DATA_2 = "S:00,1,00";
private final SMessage message1 = new SMessage(RAW_DATA_1);
private final SMessage message2 = new SMessage(RAW_DATA_2);
@Test
public void getMessageTypeTest() {
MessageType messageType = ((Message) message1).getType();
assertEquals(MessageType.S, messageType);
}
@Test
public void getDutyCycleTest() {
int dutyCycle = message1.getDutyCycle();
assertEquals(1, dutyCycle);
dutyCycle = message2.getDutyCycle();
assertEquals(0, dutyCycle);
}
@Test
public void getCommandDiscardedTest() {
boolean commandDiscarded = message1.isCommandDiscarded();
assertEquals(false, commandDiscarded);
commandDiscarded = message2.isCommandDiscarded();
assertEquals(true, commandDiscarded);
}
@Test
public void getFreeMemTest() {
int freeMemory = message1.getFreeMemorySlots();
assertEquals(49, freeMemory);
freeMemory = message2.getDutyCycle();
assertEquals(0, freeMemory);
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.max.internal.message;
import static org.junit.Assert.assertEquals;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.Test;
import org.openhab.binding.max.internal.Utils;
/**
* Tests cases for {@link Utils}.
*
* @author Andreas Heil (info@aheil.de) - Initial contribution
* @author Marcel Verpaalen - OH2 Version and updates
*/
@NonNullByDefault
public class UtilsTest {
@Test
public void fromHexTest() {
assertEquals(0x00, Utils.fromHex("00"));
assertEquals(0x01, Utils.fromHex("01"));
assertEquals(0x1F, Utils.fromHex("1F"));
assertEquals(0xFF, Utils.fromHex("FF"));
}
@Test
public void fromByteTest() {
byte b0 = 0;
byte b127 = 127;
byte b128 = (byte) 128; // overflow due to
byte bn128 = -128; // signed bytes
byte bn1 = -1;
int ar0 = Utils.fromByte(b0);
int ar127 = Utils.fromByte(b127);
int ar128 = Utils.fromByte(b128);
int arn128 = Utils.fromByte(bn128);
int arn1 = Utils.fromByte(bn1);
assertEquals(0, ar0);
assertEquals(127, ar127);
assertEquals(128, ar128);
assertEquals(128, arn128);
assertEquals(255, arn1);
}
@Test
public void toHexNoArgTest() {
String actualResult = Utils.toHex();
assertEquals("", actualResult);
}
@Test
public void toHexOneArgTest() {
String actualResult = Utils.toHex(15);
assertEquals("0F", actualResult);
}
@Test
public void toHexMultipleArgTest() {
String actualResult = Utils.toHex(4863);
assertEquals("12FF", actualResult);
}
@SuppressWarnings("deprecation")
@Test
public void resolveDateTimeTest() {
int date = Utils.fromHex("858B"); // 05-09-2011
int time = Utils.fromHex("2E"); // 23:00
Date result = Utils.resolveDateTime(date, time);
assertEquals(5, result.getDate());
assertEquals(9, result.getMonth());
assertEquals(2011, result.getYear());
assertEquals(23, result.getHours());
assertEquals(00, result.getMinutes());
}
@Test
public void getBitsTest() {
boolean b1[] = Utils.getBits(0xFF);
assertEquals(b1.length, 8);
for (int i = 0; i < 8; i++) {
assertEquals(true, b1[i]);
}
boolean b2[] = Utils.getBits(0x5A);
assertEquals(b2.length, 8);
assertEquals(false, b2[0]);
assertEquals(true, b2[1]);
assertEquals(false, b2[2]);
assertEquals(true, b2[3]);
assertEquals(true, b2[4]);
assertEquals(false, b2[5]);
assertEquals(true, b2[6]);
assertEquals(false, b2[7]);
}
@Test
public void hexStringToByteArrayTest() {
String s = "000102030AFF";
byte[] result = Utils.hexStringToByteArray(s);
assertEquals(0, result[0] & 0xFF);
assertEquals(1, result[1] & 0xFF);
assertEquals(2, result[2] & 0xFF);
assertEquals(3, result[3] & 0xFF);
assertEquals(10, result[4] & 0xFF);
assertEquals(255, result[5] & 0xFF);
}
}