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,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.max</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,163 @@
# MAX! Binding
This is the binding for the [eQ-3 MAX! Home Solution](http://www.eq-3.de/).
This binding allows you to integrate, view and control the MAX! Thermostats in the openHAB environment
## Supported Things
This binding support 6 different things types
| Thing | Type | Description |
|----------------|--------|--------------------------------------------------------------------------------------------------------------------|
| bridge | Bridge | This is the MAX! Cube LAN gateway. |
| thermostat | Thing | This is for the MAX! Heating Thermostat. This is also used for the powerplug switch "Zwischenstecker-Schaltaktor". |
| thermostatplus | Thing | This is for the MAX! Heating Thermostat+. This is the type that can hold the program by itself. |
| wallthermostat | Thing | MAX! Wall Thermostat. |
| ecoswitch | Thing | MAX! Ecoswitch. |
| shuttercontact | Thing | MAX! Shuttercontact / Window Contact. |
Generally one does not have to worry about the thing types as they are automatically defined.
If for any reason you need to manually define the Things and you are not exactly sure what type of thermostat you have, you can choose `thermostat` for both the thermostat and thermostat+, this will not affect their working.
## Discovery
The discovery process for the MAX! binding works in 2 steps.
When the binding is started or when manually triggered, the network is queried for the existence of a MAX! Cube LAN gateway.
When the Cube is found, it will become available in the discovery inbox. Periodically the network is queried again for a Cube.
Once the Cube is available in openHAB, all the devices connected to it are discovered and added to the discovery inbox.
No scan is needed to trigger this.
## Binding Configuration
In the openHAB2 version of this binding there are no binding wide settings.
All the configuration settings are now per MAX! Cube, hence in case you have multiple Cubes, they can run with alternative settings.
## Thing Configuration
All the things are identified by their serial number, hence this is mandatory.
The Cube (`bridge` thing) also requires the IP address to be defined.
All other configuration is optional.
Note that several configuration options are automatically populated.
Later versions of the binding may allow you to update this information.
These properties can be found in the `Device Settings` section of parameters.
## Channels
Depending on the thing it supports different Channels
| Channel Type ID | Item Type | Description | Available on thing |
|-----------------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------|
| mode | String | This channel indicates the mode of a thermostat (AUTOMATIC/MANUAL/BOOST/VACATION). | thermostat, thermostatplus, wallthermostat |
| battery_low | Switch | This channel indicates if the device battery is low (ON/OFF). | thermostat, thermostatplus, wallthermostat, ecoswitch, shuttercontact |
| set_temp | Number:Temperature | This channel indicates the sets temperature of a thermostat. | thermostat, thermostatplus, wallthermostat |
| actual_temp | Number:Temperature | This channel indicates the measured temperature of a thermostat (see below for more details). | thermostat, thermostatplus, wallthermostat |
| valve | Number | This channel indicates the valve opening in %. Note this is an advanced setting, normally not visible. | thermostat, thermostatplus, wallthermostat |
| locked | Contact | This channel indicates if the thermostat is locked for adjustments (OPEN/CLOSED). Note this is an advanced setting, normally not visible. | thermostat, thermostatplus, wallthermostat |
| contact_state | Contact | This channel indicates the contact state for a shutterswitch (OPEN/CLOSED). | shuttercontact |
| free_mem | Number | This channel indicates the free available memory on the cube to hold send commands. Note this is an advanced setting, normally not visible. | bridge |
| duty_cycle | Number | This channel indicates the duty cycle. Due to regulatory compliance reasons in Europe, the cube is allowed to send at no more than 1% of the time, which sums up to 36 seconds per hour. Once the threshold has been reached, the cube stops sending commands for the remaining time of the hour. The value in this field seems to represent a percentage ranging from 0 to 100, with a value of 100 meaning that the threshold has been reached. Note that this is an advanced setting, normally not visible. | bridge |
## Full Example
In most cases no Things need to be defined manually.
In case your Cube can't be discovered you need a `max:bridge` definition including the right IP address of the Cube.
Only in exceptional cases you would need to define the thermostats etc.
max.things:
```java
Bridge max:bridge:KEQ0565026 [ ipAddress="192.168.3.9", serialNumber="KEQ0565026" ] {
Thing thermostat KEQ0565123 [ serialNumber="KEQ0565123", refreshActualRate=60 ]
Thing shuttercontact NEQ1150510 [ serialNumber="NEQ1150510" ]
}
```
max.items:
```java
Group gMAX "MAX Heating" <temperature> [ "home-group" ]
Switch maxBattery "Battery Low" (gMAX) {channel="max:thermostat:KEQ0565026:KEQ0648949:battery_low"}
String maxMode "Thermostat Mode Setting" (gMAX) {channel="max:thermostat:KEQ0565026:KEQ0648949:mode"}
Number:Temperature maxActual "Actual measured room temperature [%.1f %unit%]" (gMAX) {channel="max:thermostat:KEQ0565026:KEQ0648949:actual_temp"}
Number:Temperature maxSetTemp "Thermostat temperature setpoint [%.1f %unit%]" (gMAX) {channel="max:thermostat:KEQ0565026:KEQ0648949:set_temp"}
Contact maxShuttercontactState "Contact State" (gMAX) {channel="max:shuttercontact:KEQ0565026:NEQ1150510:contact_state"}
Switch maxShuttercontactBattery "Contact Battery Low" <battery> (gMAX) {channel="max:shuttercontact:KEQ0565026:NEQ1150510:battery_low"}
```
demo.sitemap:
```perl
sitemap demo label="Main Menu" {
Frame label="MAX Heating System" {
Switch item=maxMode icon="climate" mappings=[AUTOMATIC=AUTOMATIC, MANUAL=MANUAL, BOOST=BOOST]
Setpoint item=maxSetTemp minValue=4.5 maxValue=32 step=0.5 icon="temperature"
Text item=maxActual icon="temperature"
Switch item=maxBattery
}
}
```
## Actual Temperature Update
Please be aware that the actual temperature measure for thermostats is only updated after the valve moved position or the thermostats mode has changed.
Hence the temperature you see may be hours old.
In that case you can update the temperature by changing the mode, wait approximately 2 minutes and change the mode back.
There is an experimental mode that does this automatically.
This can be enabled by showing the "advanced settings" (e.g. in HABmin UI).
Then the "Actual Temperature Refresh Rate" can be set.
Minimum refresh rate once/10 minutes, recommended 60min to avoid excessive battery drain.
## New Device Inclusion
When clicking the discovery button for MAX! devices manually in the UI, you will start New Device Inclusion mode for 60seconds.
During this time, holding the _boost_ button on your device will link it to the Cube.
## Device Configuration
In the _Configuration Parameters_ section of the device Things you can update some of the device configuration parameters.
Currently the following parameters can be updated:
* _name_ Name of the thermostat stored in the Cube (also used by the eQ-3 software).
_Cube device configurable parameters_
* _ntpServer1_ The hostname for NTP Server 1 used by the Cube to get the time
* _ntpServer2_ The hostname for NTP Server 2 used by the Cube to get the time
## Action Buttons (visible in HABmin)
The Action Buttons are deprecated and will be removed in a future version.
## Thing Actions
Several Thing Actions are available to trigger special actions on the MAX! Cube
* `reset()`: _Reset Cube Configuration_ resets the MAX! Cube room and device information. Devices will need to be included again!
* `reboot()`: _Restart Cube_ triggers the reboot of a Cube. This can be used if a Cube became unresponsive to commands or no connection can be made. (e.g. if you tried to connect to the Cube with multiple applications at the same time)
On the MAX! devices you can trigger the following action
* `deleteFromCube()`: _Delete Device from Cube_ deletes the device from the MAX! Cube. Device will need to be included again!
Note: In Paper UI there are no action buttons.
You can trigger these actions changing from 'No Action' in the dropdown to the action value.
### Example Rule
demo.rules:
```java
rule "Reboot MAX! Cube"
when
...
then
val maxCubeActions = getActions("max-cube", "max:bridge:KEQ0565026")
val Boolean success = maxCubeActions.reboot()
logInfo("max", "Action 'reboot' returned '{}'", success)
end
```

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.max</artifactId>
<name>openHAB Add-ons :: Bundles :: MaxCube Binding</name>
</project>

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);
}
}