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="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="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="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.plugwise</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,248 @@
# Plugwise Binding
The Plugwise binding adds support to openHAB for [Plugwise](https://www.plugwise.com) ZigBee devices using the Stick.
Users should use the Plugwise [Source](https://www.plugwise.com/source) software to define the network, reset devices or perform firmware upgrades.
Currently only "V2" of the Plugwise protocol is supported.
It is advised that users of the binding upgrade their devices to the latest firmware using the Plugwise Source software.
## Supported Things
The binding supports the following Plugwise devices:
| Device Type | Description | Thing Type |
|---------------------------------------------------------------|------------------------------------------------------------------------------------------|------------|
| [Circle](https://www.plugwise.com/en_US/products/circle) | A power outlet plug that provides energy measurement and switching control of appliances | circle |
| [Circle+](https://www.plugwise.com/en_US/products/circle) | A special Circle that coordinates the ZigBee network and acts as network gateway | circleplus |
| [Scan](https://www.plugwise.com/en_US/products/scan) | A wireless motion (PIR) and light sensor | scan |
| [Sense](https://www.plugwise.com/en_US/products/sense) | A wireless temperature and humidity sensor | sense |
| [Stealth](https://www.plugwise.com/en_US/products/stealth) | A Circle with a more compact form factor that can be built-in | stealth |
| [Stick](https://www.plugwise.com/en_US/products/start-source) | A ZigBee USB controller that openHAB uses to communicate with the Circle+ | stick |
| [Switch](https://www.plugwise.com/en_US/products/switch) | A wireless wall switch | switch |
## Discovery
Automatic device discovery runs every 3 minutes which can be sped up by starting a manual discovery.
All Circle, Circle+ and Stealth devices are discovered immediately after adding the Stick. Battery powered devices like the Scan, Sense and Switch are discovered when they are awake.
The Scan and Sense can be woken by pressing the "Wake" button.
The Switch is detected when it is awake after switching the left or right button.
## Thing Configuration
### MAC addresses
The MAC addresses are stickered to the back of Plugwise devices.
The binding uses full MAC addresses i.e. also the fine print on the sticker.
If you don't want to get off your chair, climb up ladders and unplug devices all across your home, causing all sorts of havoc; you can also find them in Source. Open `Settings > Appliances`. Then double click on an appliance.
Click on the little Circle icon to the right of the short Address to see the details of a module and the full MAC address.
Similarly the MAC addresses of the Scan, Sense and Switch can also be obtained from the Appliances screen by double clicking them in the `Sensors and other modules` list.
### Stick
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|--------------|-----------------------------------------------------------------------------------|
| serialPort | X | /dev/ttyUSB0 | The serial port of the Stick, e.g. "/dev/ttyUSB0" for Linux or "COM1" for Windows |
| messageWaitTime | | 150 | The time to wait between messages sent on the ZigBee network (in ms) |
To determine the serial port in Linux, insert the Stick, then execute the `dmesg` command.
The last few lines of the output will contain the USB port of the Stick (e.g. `/dev/ttyUSB0`).
In Windows the Device Manager lists it in the `Ports (COM & LPT)` section.
On some Linux distributions (e.g. Raspbian) an OS restart may be required before the Stick is properly configured.
To access the serial port of the Stick on Linux, the user running openHAB needs to be part of the 'dialout' group. E.g. for the user 'openhab' issue the following command: `sudo adduser openhab dialout`.
### Circle(+), Stealth
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|------------------|------------------------------------------------------------------------------------------------------------------------|
| macAddress | X | | The full device MAC address e.g. "000D6F0000A1B2C3" |
| powerStateChanging | | commandSwitching | Controls if the power state can be changed with commands or is always on/off (commandSwitching, alwaysOn or alwaysOff) |
| suppliesPower | | false | Enables power production measurements (true or false) |
| measurementInterval | | 60 | The energy measurement interval (in minutes) (5 to 60) |
| temporarilyNotInNetwork | | false | Stops searching for an unplugged device on the ZigBee network traffic (true or false) |
### Scan
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|---------|------------------------------------------------------------------------------------------------------------------|
| macAddress | X | | The full device MAC address e.g. "000D6F0000A1B2C3" |
| sensitivity | | medium | The sensitivity of movement detection (off, medium or high) |
| switchOffDelay | | 5 | The delay the Scan waits before sending an off command when motion is no longer detected (in minutes) (1 to 240) |
| daylightOverride | | false | Disables movement detection when there is daylight (true or false) |
| wakeupInterval | | 1440 | The interval in which the Scan wakes up at least once (in minutes) (5 to 1440) |
| wakeupDuration | | 10 | The number of seconds the Scan stays awake after it woke up (10 to 120) |
### Sense
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|-----------------|----------------------------------------------------------------------------------------------------------------------------|
| macAddress | X | | The full device MAC address e.g. "000D6F0000A1B2C3" |
| measurementInterval | | 15 | The interval in which the Sense measures the temperature and humidity (in minutes) (5 to 60) |
| boundaryType | | none | The boundary type that is used for switching (none, temperature or humidity) |
| boundaryAction | | offBelowOnAbove | The boundary switch action when the value is below/above the boundary minimum/maximum (offBelowOnAbove or onBelowOffAbove) |
| temperatureBoundaryMin | | 15 | The minimum boundary for the temperature boundary action (0 to 60) |
| temperatureBoundaryMax | | 25 | The maximum boundary for the temperature boundary action (0 to 60) |
| humidityBoundaryMin | | 45 | The minimum boundary for the humidity boundary action (5 to 95) |
| humidityBoundaryMax | | 65 | The maximum boundary for the humidity boundary action (5 to 95) |
| wakeupInterval | | 1440 | The interval in which the Sense wakes up at least once (in minutes) (5 to 1440) |
| wakeupDuration | | 10 | The number of seconds the Sense stays awake after it woke up (10 to 120) |
### Switch
| Configuration Parameter | Required | Default | Description |
|-------------------------|----------|---------|----------------------------------------------------------------------------------|
| macAddress | X | | The full device MAC address e.g. "000D6F0000A1B2C3" |
| wakeupInterval | | 1440 | The interval in which the Switch wakes up at least once (in minutes) (5 to 1440) |
| wakeupDuration | | 10 | The number of seconds the Switch stays awake after it woke up (10 to 120) |
## Channels
| Channel Type ID | Item Type | Description | Thing Types |
|------------------|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|
| clock | String | Time as indicated by the internal clock of the device | circle, circleplus, stealth |
| energy | Number:Energy | Energy consumption/production during the last measurement interval | circle, circleplus, stealth |
| energystamp | DateTime | Timestamp of the start of the last energy measurement interval | circle, circleplus, stealth |
| humidity | Number:Dimensionless | Current relative humidity | sense |
| lastseen | DateTime | Timestamp of the last received message. Because there is no battery level indication this is a helpful value to determine if a battery powered device is still operating properly even when no state changes occur | circle, circleplus, scan, sense, stealth, switch |
| leftbuttonstate | Switch | Current state of the left button | switch |
| power | Number:Power | Current power consumption, measured over 1 second interval | circle, circleplus, stealth |
| realtimeclock | DateTime | Time as indicated by the internal clock of the Circle+ | circleplus |
| rightbuttonstate | Switch | Current state of the right button | switch |
| state | Switch | Switches the power state on/off | circle, circleplus, stealth |
| temperature | Number:Temperature | Current temperature | sense |
| triggered | Switch | Most recent switch action initiated by the device. When daylight override is disabled on a Scan this corresponds one to one with motion detection | scan, sense |
## Example
demo.things
```
Bridge plugwise:stick:demostick [ serialPort="/dev/ttyUSB0", messageWaitTime=150 ]
Thing plugwise:circle:fan (plugwise:stick:demostick) [ macAddress="000D6F0000A1A1A1", measurementInterval=15 ]
Thing plugwise:circleplus:lamp (plugwise:stick:demostick) [ macAddress="000D6F0000B2B2B2" ] {
Channels:
Type clock : clock [ updateInterval=30 ]
Type energy : energy [ updateInterval=600 ]
Type power : power [ updateInterval=10 ]
Type realtimeclock : realtimeclock [ updateInterval=30 ]
Type state : state [ updateInterval=10 ]
}
Thing plugwise:scan:motionsensor (plugwise:stick:demostick) [ macAddress="000D6F0000C3C3C3", sensitivity="high", switchOffDelay=10 ]
Thing plugwise:sense:climatesensor (plugwise:stick:demostick) [ macAddress="000D6F0000D4D4D4", measurementInterval=10, boundaryType="temperature", boundaryAction="onBelowOffAbove", temperatureBoundaryMin=15, temperatureBoundaryMax=20 ]
Thing plugwise:stealth:fridge (plugwise:stick:demostick) [ macAddress="000D6F0000E5E5E5", powerStateChanging="alwaysOn" ] {
Channels:
Type power : power [ updateInterval=10 ]
Type state : state [ updateInterval=10 ]
}
Thing plugwise:switch:lightswitches (plugwise:stick:demostick) [ macAddress="000D6F0000F6F6F6", wakeupInterval=240, wakeupDuration=20 ]
```
demo.items
```
/* Circle */
Switch Fan_Switch "Switch" <switch> { channel="plugwise:circle:fan:state" }
String Fan_Clock "Clock [%s]" <clock> { channel="plugwise:circle:fan:clock" }
Number:Power Fan_Power "Power [%.1f %unit%]" <energy> { channel="plugwise:circle:fan:power" }
Number:Energy Fan_Energy "Energy [%.3f %unit%]" <chart> { channel="plugwise:circle:fan:energy" }
DateTime Fan_Energy_Stamp "Energy stamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:circle:fan:energystamp" }
DateTime Fan_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:circle:fan:lastseen" }
/* Circle+ */
Switch Lamp_Switch "Switch" <switch> { channel="plugwise:circleplus:lamp:state" }
String Lamp_Clock "Clock [%s]" <clock> { channel="plugwise:circleplus:lamp:clock" }
Number:Power Lamp_Power "Power [%.1f %unit%]" <energy> { channel="plugwise:circleplus:lamp:power" }
Number:Energy Lamp_Energy "Energy [%.3f %unit%]" <chart> { channel="plugwise:circleplus:lamp:energy" }
DateTime Lamp_Energy_Stamp "Energy stamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:circleplus:lamp:energystamp" }
DateTime Lamp_Real_Time_Clock "Real time clock [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <clock> { channel="plugwise:circleplus:lamp:realtimeclock" }
DateTime Lamp_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:circleplus:lamp:lastseen" }
/* Scan */
Switch Motion_Sensor_Switch "Triggered [%s]" <switch> { channel="plugwise:scan:motionsensor:triggered" }
DateTime Motion_Sensor_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <clock> { channel="plugwise:scan:motionsensor:lastseen" }
/* Sense */
Switch Climate_Sensor_Switch "Triggered [%s]" <switch> { channel="plugwise:sense:climatesensor:triggered" }
Number:Dimensionless Climate_Sensor_Humidity "Humidity [%.1f %unit%]" <humidity> { channel="plugwise:sense:climatesensor:humidity" }
Number:Temperature Climate_Sensor_Temperature "Temperature [%.1f %unit%]" <temperature> { channel="plugwise:sense:climatesensor:temperature" }
DateTime Climate_Sensor_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <clock> { channel="plugwise:sense:climatesensor:lastseen" }
/* Stealth */
Switch Fridge_Switch "Switch" <switch> { channel="plugwise:stealth:fridge:state" }
String Fridge_Clock "Clock [%s]" <clock> { channel="plugwise:stealth:fridge:clock" }
Number:Power Fridge_Power "Power [%.1f %unit%]" <energy> { channel="plugwise:stealth:fridge:power" }
Number:Energy Fridge_Energy "Energy [%.3f %unit%]" <chart> { channel="plugwise:stealth:fridge:energy" }
DateTime Fridge_Energy_Stamp "Energy stamp [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:stealth:fridge:energystamp" }
DateTime Fridge_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <calendar> { channel="plugwise:stealth:fridge:lastseen" }
/* Switch */
Switch Light_Switches_Left_Button_State "Left button [%s]" <switch> { channel="plugwise:switch:lightswitches:leftbuttonstate" }
Switch Light_Switches_Right_Button_State "Right button [%s]" <switch> { channel="plugwise:switch:lightswitches:rightbuttonstate" }
DateTime Light_Switches_Last_Seen "Last seen [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <clock> { channel="plugwise:switch:lightswitches:lastseen" }
```
demo.sitemap
```
sitemap demo label="Main Menu"
{
Frame label="Fan (Circle)" {
Switch item=Fan_Switch
Text item=Fan_Clock
Text item=Fan_Power
Text item=Fan_Energy
Text item=Fan_Energy_Stamp
Text item=Fan_Last_Seen
}
Frame label="Lamp (Circle+)" {
Switch item=Lamp_Switch
Text item=Lamp_Clock
Text item=Lamp_Power
Text item=Lamp_Energy
Text item=Lamp_Energy_Stamp
Text item=Lamp_Real_Time_Clock
Text item=Lamp_Last_Seen
}
Frame label="Motion Sensor (Scan)" {
Text item=Motion_Sensor_Switch
Text item=Motion_Sensor_Last_Seen
}
Frame label="Climate Sensor (Sense)" {
Text item=Climate_Sensor_Switch
Text item=Climate_Sensor_Humidity
Text item=Climate_Sensor_Temperature
Text item=Climate_Sensor_Last_Seen
}
Frame label="Fridge (Stealth)" {
Switch item=Fridge_Switch
Text item=Fridge_Clock
Text item=Fridge_Power
Text item=Fridge_Energy
Text item=Fridge_Energy_Stamp
Text item=Fridge_Last_Seen
}
Frame label="Light Switches (Switch)" {
Text item=Light_Switches_Left_Button_State
Text item=Light_Switches_Right_Button_State
Text item=Light_Switches_Last_Seen
}
}
```

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.plugwise</artifactId>
<name>openHAB Add-ons :: Bundles :: Plugwise Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.plugwise-${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-plugwise" description="Plugwise Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.plugwise/${project.version}</bundle>
</feature>
</features>

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.plugwise.internal;
import static java.util.stream.Collectors.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link PlugwiseBinding} class defines common constants, which are used across the whole binding.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseBindingConstants {
public static final String BINDING_ID = "plugwise";
// List of all Channel IDs
public static final String CHANNEL_CLOCK = "clock";
public static final String CHANNEL_ENERGY = "energy";
public static final String CHANNEL_ENERGY_STAMP = "energystamp";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_LAST_SEEN = "lastseen";
public static final String CHANNEL_LEFT_BUTTON_STATE = "leftbuttonstate";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_REAL_TIME_CLOCK = "realtimeclock";
public static final String CHANNEL_RIGHT_BUTTON_STATE = "rightbuttonstate";
public static final String CHANNEL_STATE = "state";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_TRIGGERED = "triggered";
// List of all configuration properties
public static final String CONFIG_PROPERTY_MAC_ADDRESS = "macAddress";
public static final String CONFIG_PROPERTY_RECALIBRATE = "recalibrate";
public static final String CONFIG_PROPERTY_SERIAL_PORT = "serialPort";
public static final String CONFIG_PROPERTY_UPDATE_CONFIGURATION = "updateConfiguration";
public static final String CONFIG_PROPERTY_UPDATE_INTERVAL = "updateInterval";
// List of all property IDs
public static final String PROPERTY_HERTZ = "hertz";
public static final String PROPERTY_MAC_ADDRESS = "macAddress";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CIRCLE = new ThingTypeUID(BINDING_ID, "circle");
public static final ThingTypeUID THING_TYPE_CIRCLE_PLUS = new ThingTypeUID(BINDING_ID, "circleplus");
public static final ThingTypeUID THING_TYPE_SCAN = new ThingTypeUID(BINDING_ID, "scan");
public static final ThingTypeUID THING_TYPE_SENSE = new ThingTypeUID(BINDING_ID, "sense");
public static final ThingTypeUID THING_TYPE_STEALTH = new ThingTypeUID(BINDING_ID, "stealth");
public static final ThingTypeUID THING_TYPE_STICK = new ThingTypeUID(BINDING_ID, "stick");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(THING_TYPE_CIRCLE, THING_TYPE_CIRCLE_PLUS, THING_TYPE_SCAN, THING_TYPE_SENSE, THING_TYPE_STEALTH,
THING_TYPE_STICK, THING_TYPE_SWITCH)
.collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
}

View File

@@ -0,0 +1,206 @@
/**
* 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.plugwise.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Comparator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.config.PlugwiseStickConfig;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The communication context used by the {@link PlugwiseMessageSender} and {@link PlugwiseMessageProcessor} for sending
* and receiving messages.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseCommunicationContext {
/** Plugwise protocol header code (hex) */
public static final String PROTOCOL_HEADER = "\u0005\u0005\u0003\u0003";
/** Carriage return */
public static final char CR = '\r';
/** Line feed */
public static final char LF = '\n';
/** Plugwise protocol trailer code (hex) */
public static final String PROTOCOL_TRAILER = new String(new char[] { CR, LF });
public static final int MAX_BUFFER_SIZE = 1024;
private static final Comparator<? super @Nullable PlugwiseQueuedMessage> QUEUED_MESSAGE_COMPERATOR = new Comparator<@Nullable PlugwiseQueuedMessage>() {
@Override
public int compare(@Nullable PlugwiseQueuedMessage o1, @Nullable PlugwiseQueuedMessage o2) {
if (o1 == null || o2 == null) {
return -1;
}
int result = o1.getPriority().compareTo(o2.getPriority());
if (result == 0) {
result = o1.getDateTime().compareTo(o2.getDateTime());
}
return result;
}
};
private final Logger logger = LoggerFactory.getLogger(PlugwiseCommunicationContext.class);
private final BlockingQueue<@Nullable AcknowledgementMessage> acknowledgedQueue = new ArrayBlockingQueue<>(
MAX_BUFFER_SIZE, true);
private final BlockingQueue<@Nullable Message> receivedQueue = new ArrayBlockingQueue<>(MAX_BUFFER_SIZE, true);
private final PriorityBlockingQueue<@Nullable PlugwiseQueuedMessage> sendQueue = new PriorityBlockingQueue<>(
MAX_BUFFER_SIZE, QUEUED_MESSAGE_COMPERATOR);
private final BlockingQueue<@Nullable PlugwiseQueuedMessage> sentQueue = new ArrayBlockingQueue<>(MAX_BUFFER_SIZE,
true);
private final ReentrantLock sentQueueLock = new ReentrantLock();
private final PlugwiseFilteredMessageListenerList filteredListeners = new PlugwiseFilteredMessageListenerList();
private final ThingUID bridgeUID;
private final Supplier<PlugwiseStickConfig> configurationSupplier;
private final SerialPortManager serialPortManager;
private @Nullable SerialPort serialPort;
public PlugwiseCommunicationContext(ThingUID bridgeUID, Supplier<PlugwiseStickConfig> configurationSupplier,
SerialPortManager serialPortManager) {
this.bridgeUID = bridgeUID;
this.configurationSupplier = configurationSupplier;
this.serialPortManager = serialPortManager;
}
public void clearQueues() {
acknowledgedQueue.clear();
receivedQueue.clear();
sendQueue.clear();
sentQueue.clear();
}
public void closeSerialPort() {
SerialPort localSerialPort = serialPort;
if (localSerialPort != null) {
try {
InputStream inputStream = localSerialPort.getInputStream();
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the input stream: {}", e.getMessage());
}
}
OutputStream outputStream = localSerialPort.getOutputStream();
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
logger.debug("Error while closing the output stream: {}", e.getMessage());
}
}
localSerialPort.close();
serialPort = null;
} catch (IOException e) {
logger.warn("An exception occurred while closing the serial port {} ({})", localSerialPort,
e.getMessage());
}
}
}
private SerialPortIdentifier findSerialPortIdentifier() throws PlugwiseInitializationException {
SerialPortIdentifier identifier = serialPortManager.getIdentifier(getConfiguration().getSerialPort());
if (identifier != null) {
logger.debug("Serial port '{}' has been found", getConfiguration().getSerialPort());
return identifier;
}
// Build exception message when port not found
String availablePorts = serialPortManager.getIdentifiers().map(id -> id.getName())
.collect(Collectors.joining(System.lineSeparator()));
throw new PlugwiseInitializationException(
String.format("Serial port '%s' could not be found. Available ports are:%n%s",
getConfiguration().getSerialPort(), availablePorts));
}
public BlockingQueue<@Nullable AcknowledgementMessage> getAcknowledgedQueue() {
return acknowledgedQueue;
}
public ThingUID getBridgeUID() {
return bridgeUID;
}
public PlugwiseStickConfig getConfiguration() {
return configurationSupplier.get();
}
public PlugwiseFilteredMessageListenerList getFilteredListeners() {
return filteredListeners;
}
public BlockingQueue<@Nullable Message> getReceivedQueue() {
return receivedQueue;
}
public PriorityBlockingQueue<@Nullable PlugwiseQueuedMessage> getSendQueue() {
return sendQueue;
}
public BlockingQueue<@Nullable PlugwiseQueuedMessage> getSentQueue() {
return sentQueue;
}
public ReentrantLock getSentQueueLock() {
return sentQueueLock;
}
public @Nullable SerialPort getSerialPort() {
return serialPort;
}
/**
* Initialize this device and open the serial port
*
* @throws PlugwiseInitializationException if port can not be found or opened
*/
public void initializeSerialPort() throws PlugwiseInitializationException {
try {
SerialPort localSerialPort = findSerialPortIdentifier().open(getClass().getName(), 2000);
localSerialPort.notifyOnDataAvailable(true);
localSerialPort.setSerialPortParams(115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);
serialPort = localSerialPort;
} catch (PortInUseException e) {
throw new PlugwiseInitializationException("Serial port already in use", e);
} catch (UnsupportedCommOperationException e) {
throw new PlugwiseInitializationException("Failed to set serial port parameters", e);
}
}
}

View File

@@ -0,0 +1,84 @@
/**
* 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.plugwise.internal;
import java.io.IOException;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.config.PlugwiseStickConfig;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.ThingUID;
/**
* The {@link PlugwiseCommunicationHandler} handles all serial communication with the Plugwise Stick.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseCommunicationHandler {
private final PlugwiseCommunicationContext context;
private final PlugwiseMessageProcessor messageProcessor;
private final PlugwiseMessageSender messageSender;
private boolean initialized = false;
public PlugwiseCommunicationHandler(ThingUID bridgeUID, Supplier<PlugwiseStickConfig> configurationSupplier,
SerialPortManager serialPortManager) {
context = new PlugwiseCommunicationContext(bridgeUID, configurationSupplier, serialPortManager);
messageProcessor = new PlugwiseMessageProcessor(context);
messageSender = new PlugwiseMessageSender(context);
}
public void addMessageListener(PlugwiseMessageListener listener) {
context.getFilteredListeners().addListener(listener);
}
public void addMessageListener(PlugwiseMessageListener listener, MACAddress macAddress) {
context.getFilteredListeners().addListener(listener, macAddress);
}
public void removeMessageListener(PlugwiseMessageListener listener) {
context.getFilteredListeners().removeListener(listener);
}
public void sendMessage(Message message, PlugwiseMessagePriority priority) throws IOException {
if (initialized) {
messageSender.sendMessage(message, priority);
}
}
public void start() throws PlugwiseInitializationException {
try {
context.clearQueues();
context.initializeSerialPort();
messageSender.start();
messageProcessor.start();
initialized = true;
} catch (PlugwiseInitializationException e) {
initialized = false;
throw e;
}
}
public void stop() {
messageSender.stop();
messageProcessor.stop();
context.closeSerialPort();
initialized = false;
}
}

View File

@@ -0,0 +1,122 @@
/**
* 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.plugwise.internal;
import java.time.Duration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A recurring Plugwise device task that can for instance be extended for updating a channel or setting the clock.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class PlugwiseDeviceTask {
private final Logger logger = LoggerFactory.getLogger(PlugwiseDeviceTask.class);
private final ReentrantLock lock = new ReentrantLock();
private final String name;
private final ScheduledExecutorService scheduler;
private @Nullable DeviceType deviceType;
private @Nullable Duration interval;
private @Nullable MACAddress macAddress;
private @Nullable ScheduledFuture<?> future;
private Runnable scheduledRunnable = new Runnable() {
@Override
public void run() {
try {
lock.lock();
logger.debug("Running '{}' Plugwise task for {} ({})", name, deviceType, macAddress);
runTask();
} catch (Exception e) {
logger.warn("Error while running '{}' Plugwise task for {} ({})", name, deviceType, macAddress, e);
} finally {
lock.unlock();
}
}
};
public PlugwiseDeviceTask(String name, ScheduledExecutorService scheduler) {
this.name = name;
this.scheduler = scheduler;
}
public abstract Duration getConfiguredInterval();
public @Nullable Duration getInterval() {
return interval;
}
public String getName() {
return name;
}
public boolean isScheduled() {
return future != null && !future.isCancelled();
}
public abstract void runTask();
public abstract boolean shouldBeScheduled();
public void start() {
try {
lock.lock();
if (!isScheduled()) {
Duration configuredInterval = getConfiguredInterval();
future = scheduler.scheduleWithFixedDelay(scheduledRunnable, 0, configuredInterval.getSeconds(),
TimeUnit.SECONDS);
interval = configuredInterval;
logger.debug("Scheduled '{}' Plugwise task for {} ({}) with {} seconds interval", name, deviceType,
macAddress, configuredInterval.getSeconds());
}
} finally {
lock.unlock();
}
}
public void stop() {
try {
lock.lock();
if (isScheduled()) {
ScheduledFuture<?> localFuture = future;
if (localFuture != null) {
localFuture.cancel(true);
}
future = null;
logger.debug("Stopped '{}' Plugwise task for {} ({})", name, deviceType, macAddress);
}
} finally {
lock.unlock();
}
}
public void update(DeviceType deviceType, @Nullable MACAddress macAddress) {
this.deviceType = deviceType;
this.macAddress = macAddress;
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A filtered message listener listens to either all messages or only those of a device that has a certain MAC address.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseFilteredMessageListener {
private final PlugwiseMessageListener listener;
private final @Nullable MACAddress macAddress;
public PlugwiseFilteredMessageListener(PlugwiseMessageListener listener) {
this(listener, null);
}
public PlugwiseFilteredMessageListener(PlugwiseMessageListener listener, @Nullable MACAddress macAddress) {
this.listener = listener;
this.macAddress = macAddress;
}
public PlugwiseMessageListener getListener() {
return listener;
}
public @Nullable MACAddress getMACAddress() {
return macAddress;
}
public boolean matches(Message message) {
MACAddress localMACAddress = macAddress;
return localMACAddress == null || localMACAddress.equals(message.getMACAddress());
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.plugwise.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PlugwiseFilteredMessageListenerList} keeps track of a list of {@link PlugwiseFilteredMessageListener}s and
* facilitates listener operations such as message notification.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseFilteredMessageListenerList {
private final Logger logger = LoggerFactory.getLogger(PlugwiseFilteredMessageListenerList.class);
private final List<PlugwiseFilteredMessageListener> filteredListeners = new CopyOnWriteArrayList<>();
public void addListener(PlugwiseMessageListener listener) {
if (!isExistingListener(listener)) {
filteredListeners.add(new PlugwiseFilteredMessageListener(listener));
}
}
public void addListener(PlugwiseMessageListener listener, MACAddress macAddress) {
if (!isExistingListener(listener, macAddress)) {
filteredListeners.add(new PlugwiseFilteredMessageListener(listener, macAddress));
}
}
public boolean isExistingListener(PlugwiseMessageListener listener) {
return filteredListeners.stream().anyMatch(filteredListener -> filteredListener.getListener().equals(listener));
}
public boolean isExistingListener(PlugwiseMessageListener listener, MACAddress macAddress) {
return filteredListeners.stream().anyMatch(filteredListener -> filteredListener.getListener().equals(listener)
&& macAddress.equals(filteredListener.getMACAddress()));
}
public void notifyListeners(Message message) {
for (PlugwiseFilteredMessageListener filteredListener : filteredListeners) {
if (filteredListener.matches(message)) {
try {
filteredListener.getListener().handleReponseMessage(message);
} catch (Exception e) {
logger.warn("Listener failed to handle message: {}", message, e);
}
}
}
}
public void removeListener(PlugwiseMessageListener listener) {
List<PlugwiseFilteredMessageListener> removedListeners = new ArrayList<>();
for (PlugwiseFilteredMessageListener filteredListener : filteredListeners) {
if (filteredListener.getListener().equals(listener)) {
removedListeners.add(filteredListener);
}
}
filteredListeners.removeAll(removedListeners);
}
}

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.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.handler.PlugwiseRelayDeviceHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseScanHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseSenseHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseSwitchHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.serial.SerialPortManager;
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.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link PlugwiseHandlerFactory} is responsible for creating Plugwise things and thing handlers.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.plugwise")
public class PlugwiseHandlerFactory extends BaseThingHandlerFactory {
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegistrations = new HashMap<>();
private final SerialPortManager serialPortManager;
@Activate
public PlugwiseHandlerFactory(final @Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_STICK)) {
PlugwiseStickHandler handler = new PlugwiseStickHandler((Bridge) thing, serialPortManager);
registerDiscoveryService(handler);
return handler;
} else if (thingTypeUID.equals(THING_TYPE_CIRCLE) || thingTypeUID.equals(THING_TYPE_CIRCLE_PLUS)
|| thingTypeUID.equals(THING_TYPE_STEALTH)) {
return new PlugwiseRelayDeviceHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SCAN)) {
return new PlugwiseScanHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SENSE)) {
return new PlugwiseSenseHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
return new PlugwiseSwitchHandler(thing);
}
return null;
}
private void registerDiscoveryService(PlugwiseStickHandler handler) {
PlugwiseThingDiscoveryService discoveryService = new PlugwiseThingDiscoveryService(handler);
discoveryService.activate();
this.discoveryServiceRegistrations.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
ServiceRegistration<?> registration = this.discoveryServiceRegistrations.get(thingHandler.getThing().getUID());
if (registration != null) {
PlugwiseThingDiscoveryService discoveryService = (PlugwiseThingDiscoveryService) bundleContext
.getService(registration.getReference());
discoveryService.deactivate();
registration.unregister();
discoveryServiceRegistrations.remove(thingHandler.getThing().getUID());
}
}
}

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.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception used during Stick initialization.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseInitializationException extends Exception {
private static final long serialVersionUID = 2095258016390913221L;
public PlugwiseInitializationException(String msg) {
super(msg);
}
public PlugwiseInitializationException(String msg, Throwable cause) {
super(msg, cause);
}
}

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.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* When there are multiple queued messages, the message priority and date/time determine which message is sent first.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum PlugwiseMessagePriority {
/**
* Messages caused by Thing channel commands have the highest priority, e.g. to switch power on/off
*/
COMMAND,
/**
* Messages that update the state of Thing channels immediately after a command has been sent.
*/
FAST_UPDATE,
/**
* Messages for normal state updates and Thing discovery. E.g. scheduled tasks that update the state of a
* channel.
*/
UPDATE_AND_DISCOVERY
}

View File

@@ -0,0 +1,246 @@
/**
* 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.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseCommunicationContext.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.TooManyListenersException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.MessageFactory;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortEvent;
import org.openhab.core.io.transport.serial.SerialPortEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Processes messages received from the Plugwise Stick using a serial connection.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseMessageProcessor implements SerialPortEventListener {
private class MessageProcessorThread extends Thread {
public MessageProcessorThread() {
super("OH-binding-" + context.getBridgeUID() + "-message-processor");
setDaemon(true);
}
@Override
public void run() {
while (!interrupted()) {
try {
Message message = context.getReceivedQueue().take();
if (message != null) {
logger.debug("Took message from receivedQueue (length={})", context.getReceivedQueue().size());
processMessage(message);
} else {
logger.debug("Skipping null message from receivedQueue (length={})",
context.getReceivedQueue().size());
}
} catch (InterruptedException e) {
// That's our signal to stop
break;
} catch (Exception e) {
logger.warn("Error while taking message from receivedQueue", e);
}
}
}
}
/** Matches Plugwise responses into the following groups: protocolHeader command sequence payload CRC */
private static final Pattern RESPONSE_PATTERN = Pattern.compile("(.{4})(\\w{4})(\\w{4})(\\w*?)(\\w{4})");
private final Logger logger = LoggerFactory.getLogger(PlugwiseMessageProcessor.class);
private final PlugwiseCommunicationContext context;
private final MessageFactory messageFactory = new MessageFactory();
private final ByteBuffer readBuffer = ByteBuffer.allocate(PlugwiseCommunicationContext.MAX_BUFFER_SIZE);
private int previousByte = -1;
private @Nullable MessageProcessorThread thread;
public PlugwiseMessageProcessor(PlugwiseCommunicationContext context) {
this.context = context;
}
/**
* Parse a buffer into a Message and put it in the appropriate queue for further processing
*
* @param readBuffer - the string to parse
*/
private void parseAndQueue(ByteBuffer readBuffer) {
String response = new String(readBuffer.array(), 0, readBuffer.limit());
response = StringUtils.chomp(response);
Matcher matcher = RESPONSE_PATTERN.matcher(response);
if (matcher.matches()) {
String protocolHeader = matcher.group(1);
String messageTypeHex = matcher.group(2);
String sequence = matcher.group(3);
String payload = matcher.group(4);
String crc = matcher.group(5);
if (protocolHeader.equals(PROTOCOL_HEADER)) {
String calculatedCRC = Message.getCRC(messageTypeHex + sequence + payload);
if (calculatedCRC.equals(crc)) {
MessageType messageType = MessageType.forValue(Integer.parseInt(messageTypeHex, 16));
int sequenceNumber = Integer.parseInt(sequence, 16);
if (messageType == null) {
logger.debug("Received unrecognized message: messageTypeHex=0x{}, sequence={}, payload={}",
messageTypeHex, sequenceNumber, payload);
return;
}
logger.debug("Received message: messageType={}, sequenceNumber={}, payload={}", messageType,
sequenceNumber, payload);
try {
Message message = messageFactory.createMessage(messageType, sequenceNumber, payload);
if (message instanceof AcknowledgementMessage
&& !((AcknowledgementMessage) message).isExtended()) {
logger.debug("Adding to acknowledgedQueue: {}", message);
context.getAcknowledgedQueue().put((AcknowledgementMessage) message);
} else {
logger.debug("Adding to receivedQueue: {}", message);
context.getReceivedQueue().put(message);
}
} catch (IllegalArgumentException e) {
logger.warn("Failed to create message", e);
} catch (InterruptedException e) {
Thread.interrupted();
}
} else {
logger.warn("Plugwise protocol CRC error: {} does not match {} in message", calculatedCRC, crc);
}
} else {
logger.debug("Plugwise protocol header error: {} in message {}", protocolHeader, response);
}
} else if (!response.contains("APSRequestNodeInfo") && !response.contains("APSSetSleepBehaviour")
&& !response.startsWith("# ")) {
logger.warn("Plugwise protocol message error: {}", response);
}
}
private void processMessage(Message message) {
context.getFilteredListeners().notifyListeners(message);
// After processing the response to a message, we remove any reference to the original request
// stored in the sentQueue
// WARNING: We assume that each request sent out can only be followed bye EXACTLY ONE response - so
// far it seems that the Plugwise protocol is operating in that way
try {
context.getSentQueueLock().lock();
Iterator<@Nullable PlugwiseQueuedMessage> messageIterator = context.getSentQueue().iterator();
while (messageIterator.hasNext()) {
PlugwiseQueuedMessage queuedSentMessage = messageIterator.next();
if (queuedSentMessage != null
&& queuedSentMessage.getMessage().getSequenceNumber() == message.getSequenceNumber()) {
logger.debug("Removing from sentQueue: {}", queuedSentMessage.getMessage());
context.getSentQueue().remove(queuedSentMessage);
break;
}
}
} finally {
context.getSentQueueLock().unlock();
}
}
@SuppressWarnings("resource")
@Override
public void serialEvent(@Nullable SerialPortEvent event) {
if (event != null && event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
// We get here if data has been received
SerialPort serialPort = context.getSerialPort();
if (serialPort == null) {
logger.debug("Failed to read available data from null serialPort");
return;
}
try {
InputStream inputStream = serialPort.getInputStream();
if (inputStream == null) {
logger.debug("Failed to read available data from null inputStream");
return;
}
// Read data from serial device
while (inputStream.available() > 0) {
int currentByte = inputStream.read();
// Plugwise sends ASCII data, but for some unknown reason we sometimes get data with unsigned
// byte value >127 which in itself is very strange. We filter these out for the time being
if (currentByte < 128) {
readBuffer.put((byte) currentByte);
if (previousByte == CR && currentByte == LF) {
readBuffer.flip();
parseAndQueue(readBuffer);
readBuffer.clear();
previousByte = -1;
} else {
previousByte = currentByte;
}
}
}
} catch (IOException e) {
logger.debug("Error receiving data on serial port {}: {}", context.getConfiguration().getSerialPort(),
e.getMessage());
}
}
}
@SuppressWarnings("resource")
public void start() throws PlugwiseInitializationException {
SerialPort serialPort = context.getSerialPort();
if (serialPort == null) {
throw new PlugwiseInitializationException("Failed to add serial port listener because port is null");
}
try {
serialPort.addEventListener(this);
} catch (TooManyListenersException e) {
throw new PlugwiseInitializationException("Failed to add serial port listener", e);
}
thread = new MessageProcessorThread();
thread.start();
}
@SuppressWarnings("resource")
public void stop() {
PlugwiseUtils.stopBackgroundThread(thread);
SerialPort serialPort = context.getSerialPort();
if (serialPort != null) {
serialPort.removeEventListener();
}
}
}

View File

@@ -0,0 +1,201 @@
/**
* 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.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseCommunicationContext.*;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_STATUS_REQUEST;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.core.io.transport.serial.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Sends messages to the Plugwise Stick using a serial connection.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseMessageSender {
private class MessageSenderThread extends Thread {
private int messageWaitTime;
public MessageSenderThread(int messageWaitTime) {
super("OH-binding-" + context.getBridgeUID() + "-message-sender");
this.messageWaitTime = messageWaitTime;
setDaemon(true);
}
@Override
public void run() {
while (!interrupted()) {
try {
PlugwiseQueuedMessage queuedMessage = context.getSendQueue().take();
logger.debug("Took message from sendQueue (length={})", context.getSendQueue().size());
if (queuedMessage == null) {
continue;
}
sendMessage(queuedMessage);
sleep(messageWaitTime);
} catch (InterruptedException e) {
// That's our signal to stop
break;
} catch (Exception e) {
logger.warn("Error while polling/sending message", e);
}
}
}
}
/** Default maximum number of attempts to send a message */
private static final int MAX_RETRIES = 1;
/** After exceeding this threshold the Stick is set offline */
private static final int MAX_SEQUENTIAL_WRITE_ERRORS = 15;
private final Logger logger = LoggerFactory.getLogger(PlugwiseMessageSender.class);
private final PlugwiseCommunicationContext context;
private int sequentialWriteErrors;
private @Nullable WritableByteChannel outputChannel;
private @Nullable MessageSenderThread thread;
public PlugwiseMessageSender(PlugwiseCommunicationContext context) {
this.context = context;
}
public void sendMessage(Message message, PlugwiseMessagePriority priority) throws IOException {
if (sequentialWriteErrors > MAX_SEQUENTIAL_WRITE_ERRORS) {
throw new IOException("Error writing to serial port " + context.getConfiguration().getSerialPort() + " ("
+ sequentialWriteErrors + " times)");
}
logger.debug("Adding {} message to sendQueue: {}", priority, message);
context.getSendQueue().put(new PlugwiseQueuedMessage(message, priority));
}
private void sendMessage(PlugwiseQueuedMessage queuedMessage) throws InterruptedException {
if (queuedMessage.getAttempts() < MAX_RETRIES) {
queuedMessage.increaseAttempts();
Message message = queuedMessage.getMessage();
String messageHexString = message.toHexString();
WritableByteChannel localOutputChannel = outputChannel;
if (localOutputChannel == null) {
logger.warn("Error writing '{}' to serial port {}: outputChannel is null", messageHexString,
context.getConfiguration().getSerialPort());
sequentialWriteErrors++;
return;
}
String packetString = PROTOCOL_HEADER + messageHexString + PROTOCOL_TRAILER;
ByteBuffer bytebuffer = ByteBuffer.allocate(packetString.length());
bytebuffer.put(packetString.getBytes());
bytebuffer.rewind();
try {
logger.debug("Sending: {} as {}", message, messageHexString);
localOutputChannel.write(bytebuffer);
sequentialWriteErrors = 0;
} catch (IOException e) {
logger.warn("Error writing '{}' to serial port {}: {}", messageHexString,
context.getConfiguration().getSerialPort(), e.getMessage());
sequentialWriteErrors++;
return;
}
// Poll the acknowledgement message for at most 1 second, normally it is received within 75ms
AcknowledgementMessage ack = context.getAcknowledgedQueue().poll(1, TimeUnit.SECONDS);
logger.debug("Removing from acknowledgedQueue: {}", ack);
if (ack == null) {
String logMsg = "Error sending: No ACK received after 1 second: {}";
if (NETWORK_STATUS_REQUEST.equals(message.getType())) {
// Log on debug because the Stick will be set offline anyhow
logger.debug(logMsg, messageHexString);
} else {
logger.warn(logMsg, messageHexString);
}
} else if (!ack.isSuccess()) {
if (ack.isError()) {
logger.warn("Error sending: Negative ACK: {}", messageHexString);
}
} else {
// Update the sent message with the new sequence number
message.setSequenceNumber(ack.getSequenceNumber());
// Place the sent message in the sent queue
logger.debug("Adding to sentQueue: {}", message);
context.getSentQueueLock().lock();
try {
if (context.getSentQueue().size() == PlugwiseCommunicationContext.MAX_BUFFER_SIZE) {
// For some reason Plugwise devices, or the Stick, does not send responses to Requests.
// They clog the sent queue. Let's flush some part of the queue
PlugwiseQueuedMessage someMessage = context.getSentQueue().poll();
logger.debug("Flushing from sentQueue: {}", someMessage);
}
context.getSentQueue().put(queuedMessage);
} finally {
context.getSentQueueLock().unlock();
}
}
} else {
// Max attempts reached. We give up, and to a network reset
logger.warn("Giving up on Plugwise message after {} attempts: {}", queuedMessage.getAttempts(),
queuedMessage.getMessage());
}
}
@SuppressWarnings("resource")
public void start() throws PlugwiseInitializationException {
SerialPort serialPort = context.getSerialPort();
if (serialPort == null) {
throw new PlugwiseInitializationException("Failed to get serial port output stream because port is null");
}
try {
outputChannel = Channels.newChannel(serialPort.getOutputStream());
} catch (IOException e) {
throw new PlugwiseInitializationException("Failed to get serial port output stream", e);
}
sequentialWriteErrors = 0;
thread = new MessageSenderThread(context.getConfiguration().getMessageWaitTime());
thread.start();
}
public void stop() {
PlugwiseUtils.stopBackgroundThread(thread);
if (outputChannel != null) {
try {
outputChannel.close();
outputChannel = null;
} catch (IOException e) {
logger.warn("Failed to close output channel", e);
}
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.plugwise.internal;
import java.time.LocalDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.Message;
/**
* A queued message that is being sent or waiting to be sent to the Stick.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseQueuedMessage {
private final PlugwiseMessagePriority priority;
private final LocalDateTime dateTime = LocalDateTime.now();
private final Message message;
private int attempts;
public PlugwiseQueuedMessage(Message message, PlugwiseMessagePriority priority) {
this.message = message;
this.priority = priority;
}
public int getAttempts() {
return attempts;
}
public LocalDateTime getDateTime() {
return dateTime;
}
public Message getMessage() {
return message;
}
public PlugwiseMessagePriority getPriority() {
return priority;
}
public void increaseAttempts() {
attempts++;
}
}

View File

@@ -0,0 +1,377 @@
/**
* 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.plugwise.internal;
import static java.util.stream.Collectors.*;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.listener.PlugwiseStickStatusListener;
import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.RoleCallRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.RoleCallResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovers Plugwise devices by periodically reading the Circle+ node/MAC table with {@link RoleCallRequestMessage}s.
* Sleeping end devices are discovered when they announce being awake with a {@link AnnounceAwakeRequestMessage}. To
* reduce network traffic {@link InformationRequestMessage}s are only sent to undiscovered devices.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseThingDiscoveryService extends AbstractDiscoveryService
implements PlugwiseMessageListener, PlugwiseStickStatusListener {
private static class CurrentRoleCall {
private boolean isRoleCalling;
private int currentNodeID;
private int attempts;
private long lastRequestMillis;
}
private static class DiscoveredNode {
private final MACAddress macAddress;
private final Map<String, String> properties = new HashMap<>();
private DeviceType deviceType = DeviceType.UNKNOWN;
private int attempts;
private long lastRequestMillis;
public DiscoveredNode(MACAddress macAddress) {
this.macAddress = macAddress;
}
public boolean isDataComplete() {
return deviceType != DeviceType.UNKNOWN && !properties.isEmpty();
}
}
private static final Set<ThingTypeUID> DISCOVERED_THING_TYPES_UIDS = SUPPORTED_THING_TYPES_UIDS.stream()
.filter(thingTypeUID -> !thingTypeUID.equals(THING_TYPE_STICK))
.collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
private static final int MIN_NODE_ID = 0;
private static final int MAX_NODE_ID = 63;
private static final int DISCOVERY_INTERVAL = 180;
private static final int WATCH_INTERVAL = 1;
private static final int MESSAGE_TIMEOUT = 15;
private static final int MESSAGE_RETRY_ATTEMPTS = 5;
private final Logger logger = LoggerFactory.getLogger(PlugwiseThingDiscoveryService.class);
private final PlugwiseStickHandler stickHandler;
private @Nullable ScheduledFuture<?> discoveryJob;
private @Nullable ScheduledFuture<?> watchJob;
private CurrentRoleCall currentRoleCall = new CurrentRoleCall();
private final Map<MACAddress, @Nullable DiscoveredNode> discoveredNodes = new ConcurrentHashMap<>();
public PlugwiseThingDiscoveryService(PlugwiseStickHandler stickHandler) throws IllegalArgumentException {
super(DISCOVERED_THING_TYPES_UIDS, 1, true);
this.stickHandler = stickHandler;
this.stickHandler.addStickStatusListener(this);
}
@Override
public synchronized void abortScan() {
logger.debug("Aborting nodes discovery");
super.abortScan();
currentRoleCall.isRoleCalling = false;
stopDiscoveryWatchJob();
}
public void activate() {
super.activate(new HashMap<>());
}
private void createDiscoveryResult(DiscoveredNode node) {
String mac = node.macAddress.toString();
ThingUID bridgeUID = stickHandler.getThing().getUID();
ThingTypeUID thingTypeUID = PlugwiseUtils.getThingTypeUID(node.deviceType);
if (thingTypeUID != null) {
ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, mac);
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID)
.withLabel("Plugwise " + node.deviceType.toString())
.withProperty(PlugwiseBindingConstants.CONFIG_PROPERTY_MAC_ADDRESS, mac)
.withProperties(new HashMap<>(node.properties))
.withRepresentationProperty(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS).build());
}
}
@Override
protected void deactivate() {
super.deactivate();
stickHandler.removeMessageListener(this);
stickHandler.removeStickStatusListener(this);
}
private void discoverNewNodeDetails(MACAddress macAddress) {
if (!isAlreadyDiscovered(macAddress)) {
logger.debug("Discovered new node ({})", macAddress);
discoveredNodes.put(macAddress, new DiscoveredNode(macAddress));
updateInformation(macAddress);
} else {
logger.debug("Already discovered node ({})", macAddress);
}
}
protected void discoverNodes() {
MACAddress circlePlusMAC = getCirclePlusMAC();
if (getStickStatus() != ThingStatus.ONLINE) {
logger.debug("Discovery with role call not possible (Stick status is {})", getStickStatus());
} else if (circlePlusMAC == null) {
logger.debug("Discovery with role call not possible (Circle+ MAC address is null)");
} else if (currentRoleCall.isRoleCalling) {
logger.debug("Discovery with role call not possible (already role calling)");
} else {
stickHandler.addMessageListener(this);
discoveredNodes.clear();
currentRoleCall.isRoleCalling = true;
currentRoleCall.currentNodeID = Integer.MIN_VALUE;
discoverNewNodeDetails(circlePlusMAC);
logger.debug("Discovering nodes with role call on Circle+ ({})", circlePlusMAC);
roleCall(MIN_NODE_ID);
startDiscoveryWatchJob();
}
}
private @Nullable MACAddress getCirclePlusMAC() {
return stickHandler.getCirclePlusMAC();
}
private ThingStatus getStickStatus() {
return stickHandler.getThing().getStatus();
}
private void handleAnnounceAwakeRequest(AnnounceAwakeRequestMessage message) {
discoverNewNodeDetails(message.getMACAddress());
}
private void handleInformationResponse(InformationResponseMessage message) {
MACAddress mac = message.getMACAddress();
DiscoveredNode node = discoveredNodes.get(mac);
if (node != null) {
node.deviceType = message.getDeviceType();
PlugwiseUtils.updateProperties(node.properties, message);
if (node.isDataComplete()) {
createDiscoveryResult(node);
discoveredNodes.remove(mac);
logger.debug("Finished discovery of {} ({})", node.deviceType, mac);
}
} else {
logger.debug("Received information response for already discovered node ({})", mac);
}
}
@Override
public void handleReponseMessage(Message message) {
switch (message.getType()) {
case ANNOUNCE_AWAKE_REQUEST:
handleAnnounceAwakeRequest((AnnounceAwakeRequestMessage) message);
break;
case DEVICE_INFORMATION_RESPONSE:
handleInformationResponse((InformationResponseMessage) message);
break;
case DEVICE_ROLE_CALL_RESPONSE:
handleRoleCallResponse((RoleCallResponseMessage) message);
break;
default:
logger.trace("Received unhandled {} message from {}", message.getType(), message.getMACAddress());
break;
}
}
private void handleRoleCallResponse(RoleCallResponseMessage message) {
logger.debug("Node with ID {} has MAC address: {}", message.getNodeID(), message.getNodeMAC());
if (message.getNodeID() <= MAX_NODE_ID && (message.getNodeMAC() != null)) {
discoverNewNodeDetails(message.getNodeMAC());
// Check if there is any other on the network
int nextNodeID = message.getNodeID() + 1;
if (nextNodeID <= MAX_NODE_ID) {
roleCall(nextNodeID);
} else {
currentRoleCall.isRoleCalling = false;
}
} else {
currentRoleCall.isRoleCalling = false;
}
if (!currentRoleCall.isRoleCalling) {
logger.debug("Finished discovering devices with role call on Circle+ ({})", getCirclePlusMAC());
}
}
private boolean isAlreadyDiscovered(MACAddress macAddress) {
Thing thing = stickHandler.getThingByMAC(macAddress);
if (thing != null) {
logger.debug("Node ({}) has existing thing: {}", macAddress, thing.getUID());
}
return thing != null;
}
/**
* Role calling is basically asking the Circle+ to return all the devices known to it. Up to 64 devices
* are supported in a Plugwise network, and role calling is done by sequentially sending
* {@link RoleCallRequestMessage} for all possible IDs in the network (0 <= ID <= 63)
*
* @param nodeID of the device to role call
*/
private void roleCall(int nodeID) {
if (MIN_NODE_ID <= nodeID && nodeID <= MAX_NODE_ID) {
sendMessage(new RoleCallRequestMessage(getCirclePlusMAC(), nodeID));
if (nodeID != currentRoleCall.currentNodeID) {
currentRoleCall.attempts = 0;
} else {
currentRoleCall.attempts++;
}
currentRoleCall.currentNodeID = nodeID;
currentRoleCall.lastRequestMillis = System.currentTimeMillis();
} else {
logger.warn("Invalid node ID for role call: {}", nodeID);
}
}
private void sendMessage(Message message) {
stickHandler.sendMessage(message, PlugwiseMessagePriority.UPDATE_AND_DISCOVERY);
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("Starting Plugwise device background discovery");
Runnable discoveryRunnable = () -> {
logger.debug("Discover nodes (background discovery)");
discoverNodes();
};
ScheduledFuture<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob == null || localDiscoveryJob.isCancelled()) {
discoveryJob = scheduler.scheduleWithFixedDelay(discoveryRunnable, 0, DISCOVERY_INTERVAL, TimeUnit.SECONDS);
}
}
private void startDiscoveryWatchJob() {
logger.debug("Starting Plugwise discovery watch job");
Runnable watchRunnable = () -> {
if (currentRoleCall.isRoleCalling) {
if ((System.currentTimeMillis() - currentRoleCall.lastRequestMillis) > (MESSAGE_TIMEOUT * 1000)
&& currentRoleCall.attempts < MESSAGE_RETRY_ATTEMPTS) {
logger.debug("Resending timed out role call message for node with ID {} on Circle+ ({})",
currentRoleCall.currentNodeID, getCirclePlusMAC());
roleCall(currentRoleCall.currentNodeID);
} else if (currentRoleCall.attempts >= MESSAGE_RETRY_ATTEMPTS) {
logger.debug("Giving up on role call for node with ID {} on Circle+ ({})",
currentRoleCall.currentNodeID, getCirclePlusMAC());
currentRoleCall.isRoleCalling = false;
}
}
Iterator<Entry<MACAddress, @Nullable DiscoveredNode>> it = discoveredNodes.entrySet().iterator();
while (it.hasNext()) {
Entry<MACAddress, @Nullable DiscoveredNode> entry = it.next();
DiscoveredNode node = entry.getValue();
if (node != null && (System.currentTimeMillis() - node.lastRequestMillis) > (MESSAGE_TIMEOUT * 1000)
&& node.attempts < MESSAGE_RETRY_ATTEMPTS) {
logger.debug("Resending timed out information request message to node ({})", node.macAddress);
updateInformation(node.macAddress);
node.attempts++;
} else if (node != null && node.attempts >= MESSAGE_RETRY_ATTEMPTS) {
logger.debug("Giving up on information request for node ({})", node.macAddress);
it.remove();
}
}
if (!currentRoleCall.isRoleCalling && discoveredNodes.isEmpty()) {
logger.debug("Discovery no longer needs to be watched");
stopDiscoveryWatchJob();
}
};
ScheduledFuture<?> localWatchJob = watchJob;
if (localWatchJob == null || localWatchJob.isCancelled()) {
watchJob = scheduler.scheduleWithFixedDelay(watchRunnable, WATCH_INTERVAL, WATCH_INTERVAL,
TimeUnit.SECONDS);
}
}
@Override
protected void startScan() {
logger.debug("Discover nodes (manual discovery)");
discoverNodes();
}
@Override
public void stickStatusChanged(ThingStatus status) {
if (status.equals(ThingStatus.ONLINE)) {
logger.debug("Discover nodes (Stick online)");
discoverNodes();
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("Stopping Plugwise device background discovery");
ScheduledFuture<?> localDiscoveryJob = discoveryJob;
if (localDiscoveryJob != null && !localDiscoveryJob.isCancelled()) {
localDiscoveryJob.cancel(true);
discoveryJob = null;
}
stopDiscoveryWatchJob();
}
private void stopDiscoveryWatchJob() {
logger.debug("Stopping Plugwise discovery watch job");
ScheduledFuture<?> localWatchJob = watchJob;
if (localWatchJob != null && !localWatchJob.isCancelled()) {
localWatchJob.cancel(true);
watchJob = null;
}
}
private void updateInformation(MACAddress macAddress) {
sendMessage(new InformationRequestMessage(macAddress));
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import static org.openhab.binding.plugwise.internal.protocol.field.DeviceType.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
/**
* Utility class for sharing utility methods between objects.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public final class PlugwiseUtils {
private PlugwiseUtils() {
// Hidden utility class constructor
}
public static DeviceType getDeviceType(ThingTypeUID uid) {
if (uid.equals(THING_TYPE_CIRCLE)) {
return CIRCLE;
} else if (uid.equals(THING_TYPE_CIRCLE_PLUS)) {
return CIRCLE_PLUS;
} else if (uid.equals(THING_TYPE_SCAN)) {
return SCAN;
} else if (uid.equals(THING_TYPE_SENSE)) {
return SENSE;
} else if (uid.equals(THING_TYPE_STEALTH)) {
return STEALTH;
} else if (uid.equals(THING_TYPE_SWITCH)) {
return SWITCH;
} else {
return UNKNOWN;
}
}
public static @Nullable ThingTypeUID getThingTypeUID(DeviceType deviceType) {
if (deviceType == CIRCLE) {
return THING_TYPE_CIRCLE;
} else if (deviceType == CIRCLE_PLUS) {
return THING_TYPE_CIRCLE_PLUS;
} else if (deviceType == SCAN) {
return THING_TYPE_SCAN;
} else if (deviceType == SENSE) {
return THING_TYPE_SENSE;
} else if (deviceType == STEALTH) {
return THING_TYPE_STEALTH;
} else if (deviceType == SWITCH) {
return THING_TYPE_SWITCH;
} else {
return null;
}
}
public static String lowerCamelToUpperUnderscore(String text) {
return text.replaceAll("([a-z])([A-Z]+)", "$1_$2").toUpperCase();
}
public static <T extends Comparable<T>> T minComparable(T first, T second) {
return first.compareTo(second) <= 0 ? first : second;
}
public static DateTimeType newDateTimeType(LocalDateTime localDateTime) {
return new DateTimeType(localDateTime.atZone(ZoneId.systemDefault()));
}
public static void stopBackgroundThread(@Nullable Thread thread) {
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}
public static String upperUnderscoreToLowerCamel(String text) {
String upperCamel = StringUtils.remove(WordUtils.capitalizeFully(text, new char[] { '_' }), "_");
return upperCamel.substring(0, 1).toLowerCase() + upperCamel.substring(1);
}
@SuppressWarnings("null")
public static boolean updateProperties(Map<String, String> properties, InformationResponseMessage message) {
boolean update = false;
// Update firmware version property
String oldFirmware = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
String newFirmware = DateTimeFormatter.ISO_LOCAL_DATE.format(message.getFirmwareVersion());
if (oldFirmware == null || !oldFirmware.equals(newFirmware)) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, newFirmware);
update = true;
}
// Update hardware version property
String oldHardware = properties.get(Thing.PROPERTY_HARDWARE_VERSION);
String newHardware = message.getHardwareVersion();
if (oldHardware == null || !oldHardware.equals(newHardware)) {
properties.put(Thing.PROPERTY_HARDWARE_VERSION, newHardware);
update = true;
}
// Update hertz property for devices with a relay
if (message.getDeviceType().isRelayDevice()) {
String oldHertz = properties.get(PlugwiseBindingConstants.PROPERTY_HERTZ);
String newHertz = Integer.toString(message.getHertz());
if (oldHertz == null || !oldHertz.equals(newHertz)) {
properties.put(PlugwiseBindingConstants.PROPERTY_HERTZ, newHertz);
update = true;
}
}
// Update MAC address property
String oldMACAddress = properties.get(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS);
String newMACAddress = message.getMACAddress().toString();
if (oldMACAddress == null || !oldMACAddress.equals(newMACAddress)) {
properties.put(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS, newMACAddress);
update = true;
}
return update;
}
}

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.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.config.PlugwiseRelayConfig.PowerStateChanging.COMMAND_SWITCHING;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* The {@link PlugwiseRelayConfig} class represents the configuration for a Plugwise relay device (Circle, Circle+,
* Stealth).
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseRelayConfig {
public enum PowerStateChanging {
COMMAND_SWITCHING,
ALWAYS_ON,
ALWAYS_OFF
}
private String macAddress = "";
private String powerStateChanging = upperUnderscoreToLowerCamel(COMMAND_SWITCHING.name());
private boolean suppliesPower = false;
private int measurementInterval = 60; // minutes
private boolean temporarilyNotInNetwork = false;
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public PowerStateChanging getPowerStateChanging() {
return PowerStateChanging.valueOf(lowerCamelToUpperUnderscore(powerStateChanging));
}
public boolean isSuppliesPower() {
return suppliesPower;
}
public Duration getMeasurementInterval() {
return Duration.ofMinutes(measurementInterval);
}
public boolean isTemporarilyNotInNetwork() {
return temporarilyNotInNetwork;
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
@Override
public String toString() {
return "PlugwiseRelayConfig [macAddress=" + macAddress + ", powerStateChanging=" + powerStateChanging
+ ", suppliesPower=" + suppliesPower + ", measurementInterval=" + measurementInterval
+ ", temporarilyNotInNetwork=" + temporarilyNotInNetwork + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

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.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.protocol.field.Sensitivity.MEDIUM;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Sensitivity;
/**
* The {@link PlugwiseScanConfig} class represents the configuration for a Plugwise Scan.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseScanConfig {
private String macAddress = "";
private String sensitivity = upperUnderscoreToLowerCamel(MEDIUM.name());
private int switchOffDelay = 5; // minutes
private boolean daylightOverride = false;
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean recalibrate = false;
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Sensitivity getSensitivity() {
return Sensitivity.valueOf(lowerCamelToUpperUnderscore(sensitivity));
}
public Duration getSwitchOffDelay() {
return Duration.ofMinutes(switchOffDelay);
}
public boolean isDaylightOverride() {
return daylightOverride;
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isRecalibrate() {
return recalibrate;
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalScanParameters(PlugwiseScanConfig other) {
return this.sensitivity.equals(other.sensitivity) && this.switchOffDelay == other.switchOffDelay
&& this.daylightOverride == other.daylightOverride;
}
public boolean equalSleepParameters(PlugwiseScanConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseScanConfig [macAddress=" + macAddress + ", sensitivity=" + sensitivity + ", switchOffDelay="
+ switchOffDelay + ", daylightOverride=" + daylightOverride + ", wakeupInterval=" + wakeupInterval
+ ", wakeupDuration=" + wakeupDuration + ", recalibrate=" + recalibrate + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,112 @@
/**
* 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.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction.OFF_BELOW_ON_ABOVE;
import static org.openhab.binding.plugwise.internal.protocol.field.BoundaryType.NONE;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryType;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* The {@link PlugwiseScanConfig} class represents the configuration for a Plugwise Sense.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSenseConfig {
private String macAddress = "";
private int measurementInterval = 15; // minutes
private String boundaryType = upperUnderscoreToLowerCamel(NONE.name());
private String boundaryAction = upperUnderscoreToLowerCamel(OFF_BELOW_ON_ABOVE.name());
private int temperatureBoundaryMin = 15; // degrees Celsius
private int temperatureBoundaryMax = 25; // degrees Celsius
private int humidityBoundaryMin = 45; // relative humidity (RH)
private int humidityBoundaryMax = 65; // relative humidity (RH)
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Duration getMeasurementInterval() {
return Duration.ofMinutes(measurementInterval);
}
public BoundaryType getBoundaryType() {
return BoundaryType.valueOf(lowerCamelToUpperUnderscore(boundaryType));
}
public BoundaryAction getBoundaryAction() {
return BoundaryAction.valueOf(lowerCamelToUpperUnderscore(boundaryAction));
}
public Temperature getTemperatureBoundaryMin() {
return new Temperature(temperatureBoundaryMin);
}
public Temperature getTemperatureBoundaryMax() {
return new Temperature(temperatureBoundaryMax);
}
public Humidity getHumidityBoundaryMin() {
return new Humidity(humidityBoundaryMin);
}
public Humidity getHumidityBoundaryMax() {
return new Humidity(humidityBoundaryMax);
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalBoundaryParameters(PlugwiseSenseConfig other) {
return boundaryType.equals(other.boundaryType) && boundaryAction.equals(other.boundaryAction)
&& temperatureBoundaryMin == other.temperatureBoundaryMin
&& temperatureBoundaryMax == other.temperatureBoundaryMax
&& humidityBoundaryMin == other.humidityBoundaryMin && humidityBoundaryMax == other.humidityBoundaryMax;
}
public boolean equalSleepParameters(PlugwiseSenseConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseSenseConfig [macAddress=" + macAddress + ", measurementInterval=" + measurementInterval
+ ", boundaryType=" + boundaryType + ", boundaryAction=" + boundaryAction + ", temperatureBoundaryMin="
+ temperatureBoundaryMin + ", temperatureBoundaryMax=" + temperatureBoundaryMax
+ ", humidityBoundaryMin=" + humidityBoundaryMin + ", humidityBoundaryMax=" + humidityBoundaryMax
+ ", wakeupInterval=" + wakeupInterval + ", wakeupDuration=" + wakeupDuration + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.plugwise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PlugwiseStickConfig} class represents the configuration for a Plugwise Stick.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseStickConfig {
private String serialPort = "";
private int messageWaitTime = 150; // milliseconds
public String getSerialPort() {
return serialPort;
}
public int getMessageWaitTime() {
return messageWaitTime;
}
public void setSerialPort(String serialPort) {
this.serialPort = serialPort;
}
public void setMessageWaitTime(int messageWaitTime) {
this.messageWaitTime = messageWaitTime;
}
@Override
public String toString() {
return "PlugwiseStickConfig [serialPort=" + serialPort + ", messageWaitTime=" + messageWaitTime + "]";
}
}

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.plugwise.internal.config;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* The {@link PlugwiseSwitchConfig} class represents the configuration for a Plugwise Switch.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSwitchConfig {
private String macAddress = "";
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalSleepParameters(PlugwiseSwitchConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseSwitchConfig [macAddress=" + macAddress + ", wakeupInterval=" + wakeupInterval
+ ", wakeupDuration=" + wakeupDuration + ", updateConfiguration=" + updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,306 @@
/**
* 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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.CHANNEL_LAST_SEEN;
import static org.openhab.core.thing.ThingStatus.*;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.PlugwiseBindingConstants;
import org.openhab.binding.plugwise.internal.PlugwiseDeviceTask;
import org.openhab.binding.plugwise.internal.PlugwiseMessagePriority;
import org.openhab.binding.plugwise.internal.PlugwiseUtils;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.InformationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.PingRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
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.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractPlugwiseThingHandler} handles common Plugwise device channel updates and commands.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractPlugwiseThingHandler extends BaseThingHandler implements PlugwiseMessageListener {
private static final Duration DEFAULT_UPDATE_INTERVAL = Duration.ofMinutes(1);
private static final Duration MESSAGE_TIMEOUT = Duration.ofSeconds(15);
private static final int MAX_UNANSWERED_PINGS = 2;
private final PlugwiseDeviceTask onlineStateUpdateTask = new PlugwiseDeviceTask("Online state update", scheduler) {
@Override
public Duration getConfiguredInterval() {
return MESSAGE_TIMEOUT;
}
@Override
public void runTask() {
updateOnlineState();
}
@Override
public boolean shouldBeScheduled() {
return shouldOnlineTaskBeScheduled();
}
@Override
public void start() {
unansweredPings = 0;
super.start();
}
};
private final Logger logger = LoggerFactory.getLogger(AbstractPlugwiseThingHandler.class);
private LocalDateTime lastSeen = LocalDateTime.MIN;
private @Nullable PlugwiseStickHandler stickHandler;
private @Nullable LocalDateTime lastConfigurationUpdateSend;
private int unansweredPings;
public AbstractPlugwiseThingHandler(Thing thing) {
super(thing);
}
protected void addMessageListener() {
if (stickHandler != null) {
stickHandler.addMessageListener(this, getMACAddress());
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
updateBridgeStatus();
}
@Override
public void dispose() {
removeMessageListener();
onlineStateUpdateTask.stop();
}
protected Duration durationSinceLastSeen() {
return Duration.between(lastSeen, LocalDateTime.now());
}
protected Duration getChannelUpdateInterval(String channelId) {
Channel channel = thing.getChannel(channelId);
if (channel == null) {
return DEFAULT_UPDATE_INTERVAL;
}
BigDecimal interval = (BigDecimal) channel.getConfiguration()
.get(PlugwiseBindingConstants.CONFIG_PROPERTY_UPDATE_INTERVAL);
return interval != null ? Duration.ofSeconds(interval.intValue()) : DEFAULT_UPDATE_INTERVAL;
}
protected DeviceType getDeviceType() {
return PlugwiseUtils.getDeviceType(thing.getThingTypeUID());
}
protected abstract MACAddress getMACAddress();
protected ThingStatusDetail getThingStatusDetail() {
return isConfigurationPending() ? ThingStatusDetail.CONFIGURATION_PENDING : ThingStatusDetail.NONE;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling command '{}' for {} ({}) channel '{}'", command, getDeviceType(), getMACAddress(),
channelUID.getId());
}
@Override
public void initialize() {
updateBridgeStatus();
updateTask(onlineStateUpdateTask);
// Add the message listener after dispose/initialize due to configuration update
if (isInitialized()) {
addMessageListener();
}
// Send configuration update commands after configuration update
if (thing.getStatus() == ONLINE) {
sendConfigurationUpdateCommands();
}
}
protected boolean isConfigurationPending() {
return false;
}
protected void ping() {
sendMessage(new PingRequestMessage(getMACAddress()));
}
protected boolean recentlySendConfigurationUpdate() {
return lastConfigurationUpdateSend != null
&& LocalDateTime.now().minus(Duration.ofMillis(500)).isBefore(lastConfigurationUpdateSend);
}
protected void removeMessageListener() {
if (stickHandler != null) {
stickHandler.removeMessageListener(this);
}
}
protected abstract boolean shouldOnlineTaskBeScheduled();
protected void sendCommandMessage(Message message) {
if (stickHandler != null) {
stickHandler.sendMessage(message, PlugwiseMessagePriority.COMMAND);
}
}
protected void sendConfigurationUpdateCommands() {
lastConfigurationUpdateSend = LocalDateTime.now();
if (getThingStatusDetail() != thing.getStatusInfo().getStatusDetail()) {
updateStatus(thing.getStatus(), getThingStatusDetail());
}
}
protected void sendFastUpdateMessage(Message message) {
if (stickHandler != null) {
stickHandler.sendMessage(message, PlugwiseMessagePriority.FAST_UPDATE);
}
}
protected void sendMessage(Message message) {
if (stickHandler != null) {
stickHandler.sendMessage(message, PlugwiseMessagePriority.UPDATE_AND_DISCOVERY);
}
}
protected void stopTasks(List<PlugwiseDeviceTask> tasks) {
for (PlugwiseDeviceTask task : tasks) {
task.stop();
}
}
/**
* Updates the thing state based on that of the Stick
*/
protected void updateBridgeStatus() {
Bridge bridge = getBridge();
ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
if (bridge == null) {
removeMessageListener();
updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge configured");
} else if (bridgeStatus == ONLINE && thing.getStatus() != ONLINE) {
stickHandler = (PlugwiseStickHandler) bridge.getHandler();
addMessageListener();
updateStatus(OFFLINE, getThingStatusDetail());
} else if (bridgeStatus == OFFLINE) {
removeMessageListener();
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
} else if (bridgeStatus == UNKNOWN) {
removeMessageListener();
updateStatus(UNKNOWN);
}
}
protected void updateInformation() {
sendMessage(new InformationRequestMessage(getMACAddress()));
}
protected void updateLastSeen() {
unansweredPings = 0;
lastSeen = LocalDateTime.now();
if (isLinked(CHANNEL_LAST_SEEN)) {
updateState(CHANNEL_LAST_SEEN, PlugwiseUtils.newDateTimeType(lastSeen));
}
if (thing.getStatus() == OFFLINE) {
updateStatus(ONLINE, getThingStatusDetail());
}
}
protected void updateOnlineState() {
ThingStatus status = thing.getStatus();
if (status == ONLINE && unansweredPings < MAX_UNANSWERED_PINGS
&& MESSAGE_TIMEOUT.minus(durationSinceLastSeen()).isNegative()) {
ping();
unansweredPings++;
} else if (status == ONLINE && unansweredPings >= MAX_UNANSWERED_PINGS) {
updateStatus(OFFLINE, getThingStatusDetail());
unansweredPings = 0;
} else if (status == OFFLINE) {
ping();
}
}
protected void updateProperties(InformationResponseMessage message) {
Map<String, String> properties = editProperties();
boolean update = PlugwiseUtils.updateProperties(properties, message);
if (update) {
updateProperties(properties);
}
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
ThingStatus oldStatus = thing.getStatus();
super.updateStatus(status, statusDetail, description);
updateTask(onlineStateUpdateTask);
if (oldStatus != ONLINE && status == ONLINE && isConfigurationPending()) {
sendConfigurationUpdateCommands();
}
}
protected void updateStatusOnDetailChange() {
if (thing.getStatusInfo().getStatusDetail() != getThingStatusDetail()) {
updateStatus(thing.getStatus(), getThingStatusDetail());
}
}
protected void updateTask(PlugwiseDeviceTask task) {
if (task.shouldBeScheduled()) {
if (!task.isScheduled() || task.getConfiguredInterval() != task.getInterval()) {
if (task.isScheduled()) {
task.stop();
}
task.update(getDeviceType(), getMACAddress());
task.start();
}
} else if (!task.shouldBeScheduled() && task.isScheduled()) {
task.stop();
}
}
protected void updateTasks(List<PlugwiseDeviceTask> tasks) {
for (PlugwiseDeviceTask task : tasks) {
updateTask(task);
}
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.CHANNEL_TRIGGERED;
import static org.openhab.core.thing.ThingStatus.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage.AwakeReason;
import org.openhab.binding.plugwise.internal.protocol.BroadcastGroupSwitchResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractPlugwiseThingHandler} handles common Plugwise sleeping end device (SED) channel updates and
* commands.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractSleepingEndDeviceHandler extends AbstractPlugwiseThingHandler {
private static final int SED_PROPERTIES_COUNT = 3;
private final Logger logger = LoggerFactory.getLogger(AbstractSleepingEndDeviceHandler.class);
public AbstractSleepingEndDeviceHandler(Thing thing) {
super(thing);
}
protected abstract Duration getWakeupDuration();
protected void handleAcknowledgement(AcknowledgementMessage message) {
updateStatusOnDetailChange();
}
protected void handleAnnounceAwakeRequest(AnnounceAwakeRequestMessage message) {
AwakeReason awakeReason = message.getAwakeReason();
if (awakeReason == AwakeReason.MAINTENANCE || awakeReason == AwakeReason.WAKEUP_BUTTON
|| editProperties().size() < SED_PROPERTIES_COUNT) {
updateInformation();
if (isConfigurationPending() && !recentlySendConfigurationUpdate()) {
sendConfigurationUpdateCommands();
}
}
}
protected void handleBroadcastGroupSwitchResponseMessage(BroadcastGroupSwitchResponseMessage message) {
updateState(CHANNEL_TRIGGERED, message.getPowerState() ? OnOffType.ON : OnOffType.OFF);
}
protected void handleInformationResponse(InformationResponseMessage message) {
updateProperties(message);
}
@Override
public void handleReponseMessage(Message message) {
updateLastSeen();
switch (message.getType()) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
handleAcknowledgement((AcknowledgementMessage) message);
break;
case ANNOUNCE_AWAKE_REQUEST:
handleAnnounceAwakeRequest((AnnounceAwakeRequestMessage) message);
break;
case BROADCAST_GROUP_SWITCH_RESPONSE:
handleBroadcastGroupSwitchResponseMessage((BroadcastGroupSwitchResponseMessage) message);
break;
case DEVICE_INFORMATION_RESPONSE:
handleInformationResponse((InformationResponseMessage) message);
break;
default:
logger.trace("Received unhandled {} message from {} ({})", message.getType(), getDeviceType(),
getMACAddress());
break;
}
}
@Override
protected boolean shouldOnlineTaskBeScheduled() {
return thing.getStatus() == ONLINE;
}
@Override
protected void updateOnlineState() {
if (thing.getStatus() == ONLINE && getWakeupDuration().minus(durationSinceLastSeen()).isNegative()) {
updateStatus(OFFLINE, getThingStatusDetail());
}
}
}

View File

@@ -0,0 +1,608 @@
/**
* 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.plugwise.internal.handler;
import static java.util.stream.Collectors.*;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import static org.openhab.core.thing.ThingStatus.*;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.PlugwiseDeviceTask;
import org.openhab.binding.plugwise.internal.PlugwiseUtils;
import org.openhab.binding.plugwise.internal.config.PlugwiseRelayConfig;
import org.openhab.binding.plugwise.internal.config.PlugwiseRelayConfig.PowerStateChanging;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage.ExtensionCode;
import org.openhab.binding.plugwise.internal.protocol.ClockGetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.ClockGetResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.ClockSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.PowerBufferRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerBufferResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerCalibrationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerCalibrationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerChangeRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerInformationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerInformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.PowerLogIntervalSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.RealTimeClockGetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.RealTimeClockGetResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.RealTimeClockSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
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.types.Command;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* The {@link PlugwiseRelayDeviceHandler} handles channel updates and commands for a Plugwise device with a relay.
* Relay devices are the Circle, Circle+ and Stealth.
* </p>
* <p>
* A Circle maintains current energy usage by counting 'pulses' in a one or eight-second interval. Furthermore, it
* stores hourly energy usage as well in a buffer. Each entry in the buffer contains usage for the last 4 full hours of
* consumption. In order to convert pulses to energy (kWh) or power (W), a calculation is made in the {@link Energy}
* class with {@link PowerCalibration} data.
* </p>
* <p>
* A Circle+ is a special Circle. There is one Circle+ in a Plugwise network. The Circle+ serves as a master controller
* in a Plugwise network. It also provides clock data to the other devices and sends messages from and to the Stick.
* </p>
* <p>
* A Stealth behaves like a Circle but it has a more compact form factor.
* </p>
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseRelayDeviceHandler extends AbstractPlugwiseThingHandler {
private static final int INVALID_WATT_THRESHOLD = 10000;
private static final int POWER_STATE_RETRIES = 3;
private class PendingPowerStateChange {
final OnOffType onOff;
int retries;
PendingPowerStateChange(OnOffType onOff) {
this.onOff = onOff;
}
}
private final PlugwiseDeviceTask clockUpdateTask = new PlugwiseDeviceTask("Clock update", scheduler) {
@Override
public Duration getConfiguredInterval() {
return getChannelUpdateInterval(CHANNEL_CLOCK);
}
@Override
public void runTask() {
sendMessage(new ClockGetRequestMessage(macAddress));
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE && isLinked(CHANNEL_CLOCK);
}
};
private final PlugwiseDeviceTask currentPowerUpdateTask = new PlugwiseDeviceTask("Current power update",
scheduler) {
@Override
public Duration getConfiguredInterval() {
return getChannelUpdateInterval(CHANNEL_POWER);
}
@Override
public void runTask() {
if (isCalibrated()) {
sendMessage(new PowerInformationRequestMessage(macAddress));
}
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE && (isLinked(CHANNEL_POWER)
|| configuration.getPowerStateChanging() != PowerStateChanging.COMMAND_SWITCHING);
}
};
private final PlugwiseDeviceTask energyUpdateTask = new PlugwiseDeviceTask("Energy update", scheduler) {
@Override
public Duration getConfiguredInterval() {
return getChannelUpdateInterval(CHANNEL_ENERGY);
}
@Override
public void runTask() {
if (isRecentLogAddressKnown()) {
updateEnergy();
}
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE && isLinked(CHANNEL_ENERGY);
}
};
private final PlugwiseDeviceTask informationUpdateTask = new PlugwiseDeviceTask("Information update", scheduler) {
@Override
public Duration getConfiguredInterval() {
return PlugwiseUtils.minComparable(getChannelUpdateInterval(CHANNEL_STATE),
getChannelUpdateInterval(CHANNEL_ENERGY));
}
@Override
public void runTask() {
updateInformation();
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE && (isLinked(CHANNEL_STATE) || isLinked(CHANNEL_ENERGY));
}
};
private final PlugwiseDeviceTask realTimeClockUpdateTask = new PlugwiseDeviceTask("Real-time clock update",
scheduler) {
@Override
public Duration getConfiguredInterval() {
return getChannelUpdateInterval(CHANNEL_REAL_TIME_CLOCK);
}
@Override
public void runTask() {
sendMessage(new RealTimeClockGetRequestMessage(macAddress));
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE && deviceType == DeviceType.CIRCLE_PLUS
&& isLinked(CHANNEL_REAL_TIME_CLOCK);
}
};
private final PlugwiseDeviceTask setClockTask = new PlugwiseDeviceTask("Set clock", scheduler) {
@Override
public Duration getConfiguredInterval() {
return Duration.ofDays(1);
}
@Override
public void runTask() {
if (deviceType == DeviceType.CIRCLE_PLUS) {
// The Circle+ real-time clock needs to be updated first to prevent clock sync issues
sendCommandMessage(new RealTimeClockSetRequestMessage(macAddress, LocalDateTime.now()));
scheduler.schedule(() -> {
sendCommandMessage(new ClockSetRequestMessage(macAddress, LocalDateTime.now()));
}, 5, TimeUnit.SECONDS);
} else {
sendCommandMessage(new ClockSetRequestMessage(macAddress, LocalDateTime.now()));
}
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == ONLINE;
}
};
private final List<PlugwiseDeviceTask> recurringTasks = Stream
.of(clockUpdateTask, currentPowerUpdateTask, energyUpdateTask, informationUpdateTask,
realTimeClockUpdateTask, setClockTask)
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
private final Logger logger = LoggerFactory.getLogger(PlugwiseRelayDeviceHandler.class);
private final DeviceType deviceType;
private int recentLogAddress = -1;
private @NonNullByDefault({}) PlugwiseRelayConfig configuration;
private @NonNullByDefault({}) MACAddress macAddress;
private @Nullable PowerCalibration calibration;
private @Nullable Energy energy;
private @Nullable PendingPowerStateChange pendingPowerStateChange;
// Flag that keeps track of the pending "measurement interval" device configuration update. When the corresponding
// Thing configuration parameter changes it is set to true. When the Circle/Stealth goes online a command is sent to
// update the device configuration. When the Circle/Stealth acknowledges the command the flag is again set to false.
private boolean updateMeasurementInterval;
public PlugwiseRelayDeviceHandler(Thing thing) {
super(thing);
deviceType = getDeviceType();
}
private void calibrate() {
sendFastUpdateMessage(new PowerCalibrationRequestMessage(macAddress));
}
@Override
public void channelLinked(ChannelUID channelUID) {
updateTasks(recurringTasks);
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
updateTasks(recurringTasks);
}
private void correctPowerState(OnOffType powerState) {
if (configuration.getPowerStateChanging() == PowerStateChanging.ALWAYS_OFF && (powerState != OnOffType.OFF)) {
logger.debug("Correcting power state of {} ({}) to off", deviceType, macAddress);
handleOnOffCommand(OnOffType.OFF);
} else if (configuration.getPowerStateChanging() == PowerStateChanging.ALWAYS_ON
&& (powerState != OnOffType.ON)) {
logger.debug("Correcting power state of {} ({}) to on", deviceType, macAddress);
handleOnOffCommand(OnOffType.ON);
}
}
private double correctSign(double value) {
return configuration.isSuppliesPower() ? -Math.abs(value) : Math.abs(value);
}
@Override
public void dispose() {
stopTasks(recurringTasks);
super.dispose();
}
@Override
protected MACAddress getMACAddress() {
return macAddress;
}
private void handleAcknowledgement(AcknowledgementMessage message) {
boolean oldConfigurationPending = isConfigurationPending();
ExtensionCode extensionCode = message.getExtensionCode();
switch (extensionCode) {
case CLOCK_SET_ACK:
logger.debug("Received ACK for clock set of {} ({})", deviceType, macAddress);
sendMessage(new ClockGetRequestMessage(macAddress));
break;
case ON_ACK:
logger.debug("Received ACK for switching on {} ({})", deviceType, macAddress);
updateState(CHANNEL_STATE, OnOffType.ON);
break;
case ON_OFF_NACK:
logger.debug("Received NACK for switching on/off {} ({})", deviceType, macAddress);
break;
case OFF_ACK:
logger.debug("Received ACK for switching off {} ({})", deviceType, macAddress);
updateState(CHANNEL_STATE, OnOffType.OFF);
break;
case POWER_LOG_INTERVAL_SET_ACK:
logger.debug("Received ACK for power log interval set of {} ({})", deviceType, macAddress);
updateMeasurementInterval = false;
break;
case REAL_TIME_CLOCK_SET_ACK:
logger.debug("Received ACK for setting real-time clock of {} ({})", deviceType, macAddress);
sendMessage(new RealTimeClockGetRequestMessage(macAddress));
break;
case REAL_TIME_CLOCK_SET_NACK:
logger.debug("Received NACK for setting real-time clock of {} ({})", deviceType, macAddress);
break;
default:
logger.debug("{} ({}) {} acknowledgement", deviceType, macAddress, extensionCode);
break;
}
boolean newConfigurationPending = isConfigurationPending();
if (oldConfigurationPending != newConfigurationPending && !newConfigurationPending) {
Configuration newConfiguration = editConfiguration();
newConfiguration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, false);
updateConfiguration(newConfiguration);
}
updateStatusOnDetailChange();
}
private void handleCalibrationResponse(PowerCalibrationResponseMessage message) {
boolean wasCalibrated = isCalibrated();
calibration = message.getCalibration();
logger.debug("{} ({}) calibrated: {}", deviceType, macAddress, calibration);
if (!wasCalibrated) {
if (isRecentLogAddressKnown()) {
updateEnergy();
} else {
updateInformation();
}
sendFastUpdateMessage(new PowerInformationRequestMessage(macAddress));
}
}
private void handleClockGetResponse(ClockGetResponseMessage message) {
updateState(CHANNEL_CLOCK, new StringType(message.getTime()));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling command '{}' for {} ({}) channel '{}'", command, deviceType, macAddress,
channelUID.getId());
if (CHANNEL_STATE.equals(channelUID.getId()) && (command instanceof OnOffType)) {
if (configuration.getPowerStateChanging() == PowerStateChanging.COMMAND_SWITCHING) {
OnOffType onOff = (OnOffType) command;
pendingPowerStateChange = new PendingPowerStateChange(onOff);
handleOnOffCommand(onOff);
} else {
OnOffType onOff = configuration.getPowerStateChanging() == PowerStateChanging.ALWAYS_ON ? OnOffType.ON
: OnOffType.OFF;
logger.debug("Ignoring {} ({}) power state change (always {})", deviceType, macAddress, onOff);
updateState(CHANNEL_STATE, onOff);
}
}
}
private void handleInformationResponse(InformationResponseMessage message) {
recentLogAddress = message.getLogAddress();
OnOffType powerState = message.getPowerState() ? OnOffType.ON : OnOffType.OFF;
PendingPowerStateChange change = pendingPowerStateChange;
if (change != null) {
if (powerState == change.onOff) {
pendingPowerStateChange = null;
} else {
// Power state change message may be lost or the informationUpdateTask may have queried the power
// state just before the power state change message arrived
if (change.retries < POWER_STATE_RETRIES) {
change.retries++;
logger.warn("Retrying to switch {} ({}) {} (retry #{})", deviceType, macAddress, change.onOff,
change.retries);
handleOnOffCommand(change.onOff);
} else {
logger.warn("Failed to switch {} ({}) {} after {} retries", deviceType, macAddress, change.onOff,
change.retries);
pendingPowerStateChange = null;
}
}
}
if (pendingPowerStateChange == null) {
updateState(CHANNEL_STATE, powerState);
correctPowerState(powerState);
}
if (energy == null && isCalibrated()) {
updateEnergy();
}
updateProperties(message);
}
private void handleOnOffCommand(OnOffType command) {
sendCommandMessage(new PowerChangeRequestMessage(macAddress, command == OnOffType.ON));
sendFastUpdateMessage(new InformationRequestMessage(macAddress));
// Measurements take 2 seconds to become stable
scheduler.schedule(() -> sendFastUpdateMessage(new PowerInformationRequestMessage(macAddress)), 2,
TimeUnit.SECONDS);
}
private void handlePowerBufferResponse(PowerBufferResponseMessage message) {
PowerCalibration localCalibration = calibration;
if (localCalibration == null) {
calibrate();
return;
}
Energy mostRecentEnergy = message.getMostRecentDatapoint();
if (mostRecentEnergy != null) {
// When the current time is '11:44:55.888' and the measurement interval 1 hour, then the end of the most
// recent energy measurement interval is at '11:00:00.000'
LocalDateTime oneIntervalAgo = LocalDateTime.now().minus(configuration.getMeasurementInterval());
boolean isLastInterval = mostRecentEnergy.getEnd().isAfter(oneIntervalAgo);
if (isLastInterval) {
mostRecentEnergy.setInterval(configuration.getMeasurementInterval());
energy = mostRecentEnergy;
logger.trace("Updating {} ({}) energy with: {}", deviceType, macAddress, mostRecentEnergy);
updateState(CHANNEL_ENERGY, new QuantityType<>(correctSign(mostRecentEnergy.tokWh(localCalibration)),
SmartHomeUnits.KILOWATT_HOUR));
LocalDateTime start = mostRecentEnergy.getStart();
updateState(CHANNEL_ENERGY_STAMP,
start != null ? PlugwiseUtils.newDateTimeType(start) : UnDefType.NULL);
} else {
logger.trace("Most recent energy in buffer of {} ({}) is older than one interval ago: {}", deviceType,
macAddress, mostRecentEnergy);
}
} else {
logger.trace("Most recent energy in buffer of {} ({}) is null", deviceType, macAddress);
}
}
private void handlePowerInformationResponse(PowerInformationResponseMessage message) {
PowerCalibration localCalibration = calibration;
if (localCalibration == null) {
calibrate();
return;
}
Energy one = message.getOneSecond();
double watt = one.toWatt(localCalibration);
if (watt > INVALID_WATT_THRESHOLD) {
logger.debug("{} ({}) is in a kind of error state, skipping power information response", deviceType,
macAddress);
return;
}
updateState(CHANNEL_POWER, new QuantityType<>(correctSign(watt), SmartHomeUnits.WATT));
}
private void handleRealTimeClockGetResponse(RealTimeClockGetResponseMessage message) {
updateState(CHANNEL_REAL_TIME_CLOCK, PlugwiseUtils.newDateTimeType(message.getDateTime()));
}
@Override
public void handleReponseMessage(Message message) {
updateLastSeen();
switch (message.getType()) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
handleAcknowledgement((AcknowledgementMessage) message);
break;
case CLOCK_GET_RESPONSE:
handleClockGetResponse(((ClockGetResponseMessage) message));
break;
case DEVICE_INFORMATION_RESPONSE:
handleInformationResponse((InformationResponseMessage) message);
break;
case POWER_BUFFER_RESPONSE:
handlePowerBufferResponse((PowerBufferResponseMessage) message);
break;
case POWER_CALIBRATION_RESPONSE:
handleCalibrationResponse(((PowerCalibrationResponseMessage) message));
break;
case POWER_INFORMATION_RESPONSE:
handlePowerInformationResponse((PowerInformationResponseMessage) message);
break;
case REAL_TIME_CLOCK_GET_RESPONSE:
handleRealTimeClockGetResponse((RealTimeClockGetResponseMessage) message);
break;
default:
logger.trace("Received unhandled {} message from {} ({})", message.getType(), deviceType, macAddress);
break;
}
}
@Override
public void initialize() {
configuration = getConfigAs(PlugwiseRelayConfig.class);
macAddress = configuration.getMACAddress();
if (!isInitialized()) {
setUpdateCommandFlags(null, configuration);
}
if (configuration.isTemporarilyNotInNetwork()) {
updateStatus(OFFLINE);
}
updateTasks(recurringTasks);
super.initialize();
}
private boolean isCalibrated() {
return calibration != null;
}
@Override
protected boolean isConfigurationPending() {
return updateMeasurementInterval;
}
private boolean isRecentLogAddressKnown() {
return recentLogAddress >= 0;
}
@Override
protected void sendConfigurationUpdateCommands() {
logger.debug("Sending {} ({}) configuration update commands", deviceType, macAddress);
if (updateMeasurementInterval) {
logger.debug("Sending command to update {} ({}) power log measurement interval", deviceType, macAddress);
Duration consumptionInterval = configuration.isSuppliesPower() ? Duration.ZERO
: configuration.getMeasurementInterval();
Duration productionInterval = configuration.isSuppliesPower() ? configuration.getMeasurementInterval()
: Duration.ZERO;
sendCommandMessage(
new PowerLogIntervalSetRequestMessage(macAddress, consumptionInterval, productionInterval));
}
super.sendConfigurationUpdateCommands();
}
private void setUpdateCommandFlags(@Nullable PlugwiseRelayConfig oldConfiguration,
PlugwiseRelayConfig newConfiguration) {
boolean fullUpdate = newConfiguration.isUpdateConfiguration() && !isConfigurationPending();
if (fullUpdate) {
logger.debug("Updating all configuration properties of {} ({})", deviceType, macAddress);
}
updateMeasurementInterval = fullUpdate || (oldConfiguration != null
&& (!oldConfiguration.getMeasurementInterval().equals(newConfiguration.getMeasurementInterval())));
if (updateMeasurementInterval) {
logger.debug("Updating {} ({}) power log interval when online", deviceType, macAddress);
}
}
@Override
protected boolean shouldOnlineTaskBeScheduled() {
Bridge bridge = getBridge();
return !configuration.isTemporarilyNotInNetwork() && (bridge != null && bridge.getStatus() == ONLINE);
}
@Override
protected void updateConfiguration(Configuration configuration) {
PlugwiseRelayConfig oldConfiguration = this.configuration;
PlugwiseRelayConfig newConfiguration = configuration.as(PlugwiseRelayConfig.class);
setUpdateCommandFlags(oldConfiguration, newConfiguration);
configuration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, isConfigurationPending());
super.updateConfiguration(configuration);
}
private void updateEnergy() {
int previousLogAddress = recentLogAddress - 1;
while (previousLogAddress <= recentLogAddress) {
PowerBufferRequestMessage message = new PowerBufferRequestMessage(macAddress, previousLogAddress);
previousLogAddress = previousLogAddress + 1;
sendMessage(message);
}
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
super.updateStatus(status, statusDetail, description);
if (status == ONLINE) {
if (!isCalibrated()) {
calibrate();
}
if (editProperties().isEmpty()) {
updateInformation();
}
}
updateTasks(recurringTasks);
}
}

View File

@@ -0,0 +1,187 @@
/**
* 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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.config.PlugwiseScanConfig;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.LightCalibrationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.ScanParametersSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.SleepSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* The {@link PlugwiseScanHandler} handles channel updates and commands for a Plugwise Scan device.
* </p>
* <p>
* The Scan is a wireless PIR sensor that switches on groups of devices depending on the amount of daylight and whether
* motion is detected. When the daylight override setting is enabled on a Scan, the state of triggered behaves like that
* of a normal motion sensor.
* </p>
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseScanHandler extends AbstractSleepingEndDeviceHandler {
private final Logger logger = LoggerFactory.getLogger(PlugwiseScanHandler.class);
private final DeviceType deviceType = DeviceType.SCAN;
private @NonNullByDefault({}) PlugwiseScanConfig configuration;
private @NonNullByDefault({}) MACAddress macAddress;
// Flags that keep track of the pending Scan configuration updates. When the corresponding Thing configuration
// parameters change a flag is set to true. When the Scan goes online the respective command is sent to update the
// device configuration. When the Scan acknowledges a command the respective flag is again set to false.
private boolean updateScanParameters;
private boolean updateSleepParameters;
private boolean recalibrate;
public PlugwiseScanHandler(Thing thing) {
super(thing);
}
@Override
protected MACAddress getMACAddress() {
return macAddress;
}
@Override
protected Duration getWakeupDuration() {
return configuration.getWakeupDuration();
}
@Override
protected void handleAcknowledgement(AcknowledgementMessage message) {
boolean oldConfigurationPending = isConfigurationPending();
switch (message.getExtensionCode()) {
case LIGHT_CALIBRATION_ACK:
logger.debug("Received ACK for daylight override calibration of {} ({})", deviceType, macAddress);
recalibrate = false;
Configuration configuration = editConfiguration();
configuration.put(CONFIG_PROPERTY_RECALIBRATE, Boolean.FALSE);
updateConfiguration(configuration);
break;
case SCAN_PARAMETERS_SET_ACK:
logger.debug("Received ACK for parameters set of {} ({})", deviceType, macAddress);
updateScanParameters = false;
break;
case SCAN_PARAMETERS_SET_NACK:
logger.debug("Received NACK for parameters set of {} ({})", deviceType, macAddress);
break;
case SLEEP_SET_ACK:
logger.debug("Received ACK for sleep set of {} ({})", deviceType, macAddress);
updateSleepParameters = false;
break;
default:
logger.trace("Received unhandled {} message from {} ({})", message.getType(), deviceType, macAddress);
break;
}
boolean newConfigurationPending = isConfigurationPending();
if (oldConfigurationPending != newConfigurationPending && !newConfigurationPending) {
Configuration newConfiguration = editConfiguration();
newConfiguration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, false);
updateConfiguration(newConfiguration);
}
super.handleAcknowledgement(message);
}
@Override
public void initialize() {
configuration = getConfigAs(PlugwiseScanConfig.class);
macAddress = configuration.getMACAddress();
if (!isInitialized()) {
setUpdateCommandFlags(null, configuration);
}
super.initialize();
}
@Override
protected boolean isConfigurationPending() {
return updateScanParameters || updateSleepParameters || recalibrate;
}
@Override
protected void sendConfigurationUpdateCommands() {
logger.debug("Sending {} ({}) configuration update commands", deviceType, macAddress);
if (updateScanParameters) {
logger.debug("Sending command to update {} ({}) parameters", deviceType, macAddress);
sendCommandMessage(new ScanParametersSetRequestMessage(macAddress, configuration.getSensitivity(),
configuration.isDaylightOverride(), configuration.getSwitchOffDelay()));
}
if (updateSleepParameters) {
logger.debug("Sending command to update {} ({}) sleep parameters", deviceType, macAddress);
sendCommandMessage(new SleepSetRequestMessage(macAddress, configuration.getWakeupDuration(),
configuration.getWakeupInterval()));
}
if (recalibrate) {
logger.debug("Sending command to recalibrate {} ({}) daylight override", deviceType, macAddress);
sendCommandMessage(new LightCalibrationRequestMessage(macAddress));
}
super.sendConfigurationUpdateCommands();
}
private void setUpdateCommandFlags(@Nullable PlugwiseScanConfig oldConfiguration,
PlugwiseScanConfig newConfiguration) {
boolean fullUpdate = newConfiguration.isUpdateConfiguration() && !isConfigurationPending();
if (fullUpdate) {
logger.debug("Updating all configuration properties of {} ({})", deviceType, macAddress);
}
updateScanParameters = fullUpdate
|| (oldConfiguration != null && !oldConfiguration.equalScanParameters(newConfiguration));
if (updateScanParameters) {
logger.debug("Updating {} ({}) parameters when online", deviceType, macAddress);
}
updateSleepParameters = fullUpdate
|| (oldConfiguration != null && !oldConfiguration.equalSleepParameters(newConfiguration));
if (updateSleepParameters) {
logger.debug("Updating {} ({}) sleep parameters when online", deviceType, macAddress);
}
recalibrate = fullUpdate || newConfiguration.isRecalibrate();
if (recalibrate) {
logger.debug("Recalibrating {} ({}) daylight override when online", deviceType, macAddress);
}
}
@Override
protected void updateConfiguration(Configuration configuration) {
PlugwiseScanConfig oldConfiguration = this.configuration;
PlugwiseScanConfig newConfiguration = configuration.as(PlugwiseScanConfig.class);
setUpdateCommandFlags(oldConfiguration, newConfiguration);
configuration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, isConfigurationPending());
super.updateConfiguration(configuration);
}
}

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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.config.PlugwiseSenseConfig;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.SenseBoundariesSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.SenseReportIntervalSetRequest;
import org.openhab.binding.plugwise.internal.protocol.SenseReportRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.SleepSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryType;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* The {@link PlugwiseSenseHandler} handles channel updates and commands for a Plugwise Sense device.
* </p>
* <p>
* The Sense is a wireless temperature/humidity sensor that switches on groups of devices depending on the current
* temperature or humidity level. It also periodically reports back the current temperature and humidity levels.
* </p>
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSenseHandler extends AbstractSleepingEndDeviceHandler {
private final Logger logger = LoggerFactory.getLogger(PlugwiseSenseHandler.class);
private final DeviceType deviceType = DeviceType.SENSE;
private @NonNullByDefault({}) PlugwiseSenseConfig configuration;
private @NonNullByDefault({}) MACAddress macAddress;
// Flags that keep track of the pending Sense configuration updates. When the corresponding Thing configuration
// parameters change a flag is set to true. When the Sense goes online the respective command is sent to update the
// device configuration. When the Sense acknowledges a command the respective flag is again set to false.
private boolean updateBoundaryParameters;
private boolean updateMeasurementInterval;
private boolean updateSleepParameters;
public PlugwiseSenseHandler(Thing thing) {
super(thing);
}
@Override
protected MACAddress getMACAddress() {
return macAddress;
}
@Override
protected Duration getWakeupDuration() {
return configuration.getWakeupDuration();
}
@Override
protected void handleAcknowledgement(AcknowledgementMessage message) {
boolean oldConfigurationPending = isConfigurationPending();
switch (message.getExtensionCode()) {
case SENSE_BOUNDARIES_SET_ACK:
logger.debug("Received ACK for boundaries parameters set of {} ({})", deviceType, macAddress);
updateBoundaryParameters = false;
break;
case SENSE_BOUNDARIES_SET_NACK:
logger.debug("Received NACK for boundaries parameters set of {} ({})", deviceType, macAddress);
break;
case SENSE_INTERVAL_SET_ACK:
logger.debug("Received ACK for measurement interval set of {} ({})", deviceType, macAddress);
updateMeasurementInterval = false;
break;
case SENSE_INTERVAL_SET_NACK:
logger.debug("Received NACK for measurement interval set of {} ({})", deviceType, macAddress);
break;
case SLEEP_SET_ACK:
logger.debug("Received ACK for sleep set of {} ({})", deviceType, macAddress);
updateSleepParameters = false;
break;
default:
logger.trace("Received unhandled {} message from {} ({})", message.getType(), deviceType, macAddress);
break;
}
boolean newConfigurationPending = isConfigurationPending();
if (oldConfigurationPending != newConfigurationPending && !newConfigurationPending) {
Configuration newConfiguration = editConfiguration();
newConfiguration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, false);
updateConfiguration(newConfiguration);
}
super.handleAcknowledgement(message);
}
@Override
public void handleReponseMessage(Message message) {
switch (message.getType()) {
case SENSE_REPORT_REQUEST:
handleSenseReportRequestMessage((SenseReportRequestMessage) message);
break;
default:
super.handleReponseMessage(message);
break;
}
}
private void handleSenseReportRequestMessage(SenseReportRequestMessage message) {
updateLastSeen();
updateState(CHANNEL_HUMIDITY, new QuantityType<>(message.getHumidity().getValue(), SmartHomeUnits.PERCENT));
updateState(CHANNEL_TEMPERATURE, new QuantityType<>(message.getTemperature().getValue(), SIUnits.CELSIUS));
}
@Override
public void initialize() {
configuration = getConfigAs(PlugwiseSenseConfig.class);
macAddress = configuration.getMACAddress();
if (!isInitialized()) {
setUpdateCommandFlags(null, configuration);
}
super.initialize();
}
@Override
protected boolean isConfigurationPending() {
return updateBoundaryParameters || updateMeasurementInterval || updateSleepParameters;
}
@Override
protected void sendConfigurationUpdateCommands() {
logger.debug("Sending {} ({}) configuration update commands", deviceType, macAddress);
if (updateBoundaryParameters) {
SenseBoundariesSetRequestMessage message;
if (configuration.getBoundaryType() == BoundaryType.HUMIDITY) {
message = new SenseBoundariesSetRequestMessage(macAddress, configuration.getHumidityBoundaryMin(),
configuration.getHumidityBoundaryMax(), configuration.getBoundaryAction());
} else if (configuration.getBoundaryType() == BoundaryType.TEMPERATURE) {
message = new SenseBoundariesSetRequestMessage(macAddress, configuration.getTemperatureBoundaryMin(),
configuration.getTemperatureBoundaryMax(), configuration.getBoundaryAction());
} else {
message = new SenseBoundariesSetRequestMessage(macAddress);
}
logger.debug("Sending command to update {} ({}) boundary parameters", deviceType, macAddress);
sendCommandMessage(message);
}
if (updateMeasurementInterval) {
logger.debug("Sending command to update {} ({}) measurement interval", deviceType, macAddress);
sendCommandMessage(new SenseReportIntervalSetRequest(macAddress, configuration.getMeasurementInterval()));
}
if (updateSleepParameters) {
logger.debug("Sending command to update {} ({}) sleep parameters", deviceType, macAddress);
sendCommandMessage(new SleepSetRequestMessage(macAddress, configuration.getWakeupDuration(),
configuration.getWakeupInterval()));
}
super.sendConfigurationUpdateCommands();
}
private void setUpdateCommandFlags(@Nullable PlugwiseSenseConfig oldConfiguration,
PlugwiseSenseConfig newConfiguration) {
boolean fullUpdate = newConfiguration.isUpdateConfiguration() && !isConfigurationPending();
if (fullUpdate) {
logger.debug("Updating all configuration properties of {} ({})", deviceType, macAddress);
}
updateBoundaryParameters = fullUpdate
|| (oldConfiguration != null && !oldConfiguration.equalBoundaryParameters(newConfiguration));
if (updateBoundaryParameters) {
logger.debug("Updating {} ({}) boundary parameters when online", deviceType, macAddress);
}
updateMeasurementInterval = fullUpdate || (oldConfiguration != null
&& !oldConfiguration.getMeasurementInterval().equals(newConfiguration.getMeasurementInterval()));
if (updateMeasurementInterval) {
logger.debug("Updating {} ({}) measurement interval when online", deviceType, macAddress);
}
updateSleepParameters = fullUpdate
|| (oldConfiguration != null && !oldConfiguration.equalSleepParameters(newConfiguration));
if (updateSleepParameters) {
logger.debug("Updating {} ({}) sleep parameters when online", deviceType, macAddress);
}
}
@Override
protected void updateConfiguration(Configuration configuration) {
PlugwiseSenseConfig oldConfiguration = this.configuration;
PlugwiseSenseConfig newConfiguration = configuration.as(PlugwiseSenseConfig.class);
setUpdateCommandFlags(oldConfiguration, newConfiguration);
configuration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, isConfigurationPending());
super.updateConfiguration(configuration);
}
}

View File

@@ -0,0 +1,268 @@
/**
* 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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.CONFIG_PROPERTY_MAC_ADDRESS;
import static org.openhab.binding.plugwise.internal.protocol.field.DeviceType.STICK;
import static org.openhab.core.thing.ThingStatus.*;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.PlugwiseCommunicationHandler;
import org.openhab.binding.plugwise.internal.PlugwiseDeviceTask;
import org.openhab.binding.plugwise.internal.PlugwiseInitializationException;
import org.openhab.binding.plugwise.internal.PlugwiseMessagePriority;
import org.openhab.binding.plugwise.internal.PlugwiseUtils;
import org.openhab.binding.plugwise.internal.config.PlugwiseStickConfig;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.listener.PlugwiseStickStatusListener;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage.ExtensionCode;
import org.openhab.binding.plugwise.internal.protocol.InformationRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.NetworkStatusRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.NetworkStatusResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.io.transport.serial.SerialPortManager;
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.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* The {@link PlugwiseStickHandler} handles channel updates and commands for a Plugwise Stick device.
* </p>
* <p>
* The Stick is an USB ZigBee controller that communicates with the Circle+. It is a {@link Bridge} to the devices on a
* Plugwise ZigBee mesh network.
* </p>
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseStickHandler extends BaseBridgeHandler implements PlugwiseMessageListener {
private final PlugwiseDeviceTask onlineStateUpdateTask = new PlugwiseDeviceTask("Online state update", scheduler) {
@Override
public Duration getConfiguredInterval() {
return Duration.ofSeconds(20);
}
@Override
public void runTask() {
initialize();
}
@Override
public boolean shouldBeScheduled() {
return thing.getStatus() == OFFLINE;
}
};
private final Logger logger = LoggerFactory.getLogger(PlugwiseStickHandler.class);
private final PlugwiseCommunicationHandler communicationHandler;
private final List<PlugwiseStickStatusListener> statusListeners = new CopyOnWriteArrayList<>();
private PlugwiseStickConfig configuration = new PlugwiseStickConfig();
private @Nullable MACAddress circlePlusMAC;
private @Nullable MACAddress stickMAC;
public PlugwiseStickHandler(Bridge bridge, SerialPortManager serialPortManager) {
super(bridge);
communicationHandler = new PlugwiseCommunicationHandler(bridge.getUID(), () -> configuration,
serialPortManager);
}
public void addMessageListener(PlugwiseMessageListener listener) {
communicationHandler.addMessageListener(listener);
}
public void addMessageListener(PlugwiseMessageListener listener, MACAddress macAddress) {
communicationHandler.addMessageListener(listener, macAddress);
}
public void addStickStatusListener(PlugwiseStickStatusListener listener) {
statusListeners.add(listener);
listener.stickStatusChanged(thing.getStatus());
}
@Override
public void dispose() {
communicationHandler.stop();
communicationHandler.removeMessageListener(this);
onlineStateUpdateTask.stop();
}
public @Nullable MACAddress getCirclePlusMAC() {
return circlePlusMAC;
}
public @Nullable MACAddress getStickMAC() {
return stickMAC;
}
public @Nullable Thing getThingByMAC(MACAddress macAddress) {
for (Thing thing : getThing().getThings()) {
String thingMAC = (String) thing.getConfiguration().get(CONFIG_PROPERTY_MAC_ADDRESS);
if (thingMAC != null && macAddress.equals(new MACAddress(thingMAC))) {
return thing;
}
}
return null;
}
private void handleAcknowledgement(AcknowledgementMessage acknowledge) {
if (acknowledge.isExtended() && acknowledge.getExtensionCode() == ExtensionCode.CIRCLE_PLUS) {
circlePlusMAC = acknowledge.getMACAddress();
logger.debug("Received extended acknowledgement, Circle+ MAC: {}", circlePlusMAC);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling command, channelUID: {}, command: {}", channelUID, command);
}
private void handleDeviceInformationResponse(InformationResponseMessage message) {
if (message.getDeviceType() == STICK) {
updateProperties(message);
}
}
private void handleNetworkStatusResponse(NetworkStatusResponseMessage message) {
stickMAC = message.getMACAddress();
if (message.isOnline()) {
circlePlusMAC = message.getCirclePlusMAC();
logger.debug("The network is online: circlePlusMAC={}, stickMAC={}", circlePlusMAC, stickMAC);
updateStatus(ONLINE);
sendMessage(new InformationRequestMessage(stickMAC));
} else {
logger.debug("The network is offline: circlePlusMAC={}, stickMAC={}", circlePlusMAC, stickMAC);
updateStatus(OFFLINE);
}
}
@Override
public void handleReponseMessage(Message message) {
switch (message.getType()) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
handleAcknowledgement((AcknowledgementMessage) message);
break;
case DEVICE_INFORMATION_RESPONSE:
handleDeviceInformationResponse((InformationResponseMessage) message);
break;
case NETWORK_STATUS_RESPONSE:
handleNetworkStatusResponse((NetworkStatusResponseMessage) message);
break;
default:
logger.trace("Received unhandled {} message from {}", message.getType(), message.getMACAddress());
break;
}
}
@Override
public void initialize() {
configuration = getConfigAs(PlugwiseStickConfig.class);
communicationHandler.addMessageListener(this);
try {
communicationHandler.start();
sendMessage(new NetworkStatusRequestMessage());
} catch (PlugwiseInitializationException e) {
communicationHandler.stop();
communicationHandler.removeMessageListener(this);
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
public void removeMessageListener(PlugwiseMessageListener listener) {
communicationHandler.removeMessageListener(listener);
}
public void removeMessageListener(PlugwiseMessageListener listener, MACAddress macAddress) {
communicationHandler.addMessageListener(listener, macAddress);
}
public void removeStickStatusListener(PlugwiseStickStatusListener listener) {
statusListeners.remove(listener);
}
private void sendMessage(Message message) {
sendMessage(message, PlugwiseMessagePriority.UPDATE_AND_DISCOVERY);
}
public void sendMessage(Message message, PlugwiseMessagePriority priority) {
try {
communicationHandler.sendMessage(message, priority);
} catch (IOException e) {
communicationHandler.stop();
communicationHandler.removeMessageListener(this);
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
protected void updateProperties(InformationResponseMessage message) {
Map<String, String> properties = editProperties();
boolean update = PlugwiseUtils.updateProperties(properties, message);
if (update) {
updateProperties(properties);
}
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail detail, @Nullable String comment) {
ThingStatus oldStatus = thing.getStatus();
super.updateStatus(status, detail, comment);
ThingStatus newStatus = thing.getStatus();
if (!oldStatus.equals(newStatus)) {
logger.debug("Updating listeners with status {}", status);
for (PlugwiseStickStatusListener listener : statusListeners) {
listener.stickStatusChanged(status);
}
updateTask(onlineStateUpdateTask);
}
}
protected void updateTask(PlugwiseDeviceTask task) {
if (task.shouldBeScheduled()) {
if (!task.isScheduled() || task.getConfiguredInterval() != task.getInterval()) {
if (task.isScheduled()) {
task.stop();
}
task.update(DeviceType.STICK, getStickMAC());
task.start();
}
} else if (!task.shouldBeScheduled() && task.isScheduled()) {
task.stop();
}
}
}

View File

@@ -0,0 +1,154 @@
/**
* 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.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.config.PlugwiseSwitchConfig;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage.ExtensionCode;
import org.openhab.binding.plugwise.internal.protocol.BroadcastGroupSwitchResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.SleepSetRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* The {@link PlugwiseSwitchHandler} handles channel updates and commands for a Plugwise Switch device.
* </p>
* <p>
* The Switch is a mountable wireless switch with one or two buttons depending on what parts are in place. When one
* button is used this corresponds to only using the left button.
* </p>
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSwitchHandler extends AbstractSleepingEndDeviceHandler {
private final Logger logger = LoggerFactory.getLogger(PlugwiseSwitchHandler.class);
private final DeviceType deviceType = DeviceType.SWITCH;
private @NonNullByDefault({}) PlugwiseSwitchConfig configuration;
private @NonNullByDefault({}) MACAddress macAddress;
// Flag that keeps track of the pending "sleep parameters" Switch configuration update. When the corresponding
// Thing configuration parameters change it is set to true. When the Switch goes online a command is sent to
// update the device configuration. When the Switch acknowledges the command the flag is again set to false.
private boolean updateSleepParameters;
public PlugwiseSwitchHandler(Thing thing) {
super(thing);
}
@Override
protected MACAddress getMACAddress() {
return macAddress;
}
@Override
protected Duration getWakeupDuration() {
return configuration.getWakeupDuration();
}
@Override
protected void handleAcknowledgement(AcknowledgementMessage message) {
boolean oldConfigurationPending = isConfigurationPending();
if (message.getExtensionCode() == ExtensionCode.SLEEP_SET_ACK) {
logger.debug("Received ACK for sleep set of {} ({})", deviceType, macAddress);
updateSleepParameters = false;
}
boolean newConfigurationPending = isConfigurationPending();
if (oldConfigurationPending != newConfigurationPending && !newConfigurationPending) {
Configuration newConfiguration = editConfiguration();
newConfiguration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, false);
updateConfiguration(newConfiguration);
}
super.handleAcknowledgement(message);
}
@Override
protected void handleBroadcastGroupSwitchResponseMessage(BroadcastGroupSwitchResponseMessage message) {
if (message.getPortMask() == 1) {
updateState(CHANNEL_LEFT_BUTTON_STATE, message.getPowerState() ? OnOffType.ON : OnOffType.OFF);
} else if (message.getPortMask() == 2) {
updateState(CHANNEL_RIGHT_BUTTON_STATE, message.getPowerState() ? OnOffType.ON : OnOffType.OFF);
}
}
@Override
public void initialize() {
configuration = getConfigAs(PlugwiseSwitchConfig.class);
macAddress = configuration.getMACAddress();
if (!isInitialized()) {
setUpdateCommandFlags(null, configuration);
}
super.initialize();
}
@Override
protected boolean isConfigurationPending() {
return updateSleepParameters;
}
@Override
protected void sendConfigurationUpdateCommands() {
logger.debug("Sending {} ({}) configuration update commands", deviceType, macAddress);
if (updateSleepParameters) {
logger.debug("Sending command to update {} ({}) sleep parameters", deviceType, macAddress);
sendCommandMessage(new SleepSetRequestMessage(macAddress, configuration.getWakeupDuration(),
configuration.getWakeupInterval()));
}
super.sendConfigurationUpdateCommands();
}
private void setUpdateCommandFlags(@Nullable PlugwiseSwitchConfig oldConfiguration,
PlugwiseSwitchConfig newConfiguration) {
boolean fullUpdate = newConfiguration.isUpdateConfiguration() && !isConfigurationPending();
if (fullUpdate) {
logger.debug("Updating all configuration properties of {} ({})", deviceType, macAddress);
}
updateSleepParameters = fullUpdate
|| (oldConfiguration != null && !oldConfiguration.equalSleepParameters(newConfiguration));
if (updateSleepParameters) {
logger.debug("Updating {} ({}) sleep parameters when online", deviceType, macAddress);
}
}
@Override
protected void updateConfiguration(Configuration configuration) {
PlugwiseSwitchConfig oldConfiguration = this.configuration;
PlugwiseSwitchConfig newConfiguration = configuration.as(PlugwiseSwitchConfig.class);
setUpdateCommandFlags(oldConfiguration, newConfiguration);
configuration.put(CONFIG_PROPERTY_UPDATE_CONFIGURATION, isConfigurationPending());
super.updateConfiguration(configuration);
}
}

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.plugwise.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.Message;
/**
* Interface for listeners of Plugwise response messages.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface PlugwiseMessageListener {
void handleReponseMessage(Message message);
}

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.plugwise.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
import org.openhab.core.thing.ThingStatus;
/**
* Interface for listeners of {@link PlugwiseStickHandler} thing status changes.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface PlugwiseStickStatusListener {
public void stickStatusChanged(ThingStatus status);
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.*;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* Acknowledgement message class - ACKs are used in the Plugwise protocol to serve different means, from acknowledging a
* message sent to the Stick by the host, as well as confirmation messages from nodes in the network for various
* purposes. Not all purposes are yet reverse-engineered.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class AcknowledgementMessage extends Message {
public enum ExtensionCode {
NOT_EXTENDED(0),
SENSE_INTERVAL_SET_ACK(179),
SENSE_INTERVAL_SET_NACK(180),
SENSE_BOUNDARIES_SET_ACK(181),
SENSE_BOUNDARIES_SET_NACK(182),
LIGHT_CALIBRATION_ACK(189),
SCAN_PARAMETERS_SET_ACK(190),
SCAN_PARAMETERS_SET_NACK(191),
SUCCESS(193),
ERROR(194),
CIRCLE_PLUS(221),
CLOCK_SET_ACK(215),
ON_ACK(216),
POWER_CALIBRATION_ACK(218),
OFF_ACK(222),
REAL_TIME_CLOCK_SET_ACK(223),
TIMEOUT(225),
ON_OFF_NACK(226),
REAL_TIME_CLOCK_SET_NACK(231),
SLEEP_SET_ACK(246),
POWER_LOG_INTERVAL_SET_ACK(248),
UNKNOWN(999);
private static final Map<Integer, ExtensionCode> TYPES_BY_VALUE = new HashMap<>();
static {
for (ExtensionCode type : ExtensionCode.values()) {
TYPES_BY_VALUE.put(type.identifier, type);
}
}
public static ExtensionCode forValue(int value) {
return TYPES_BY_VALUE.get(value);
}
private int identifier;
private ExtensionCode(int value) {
identifier = value;
}
public int toInt() {
return identifier;
}
}
private static final Pattern V1_SHORT_PAYLOAD_PATTERN = Pattern.compile("(\\w{4})");
private static final Pattern V1_EXTENDED_PAYLOAD_PATTERN = Pattern.compile("(\\w{4})(\\w{16})");
private static final Pattern V2_EXTENDED_PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})");
private ExtensionCode code;
public AcknowledgementMessage(MessageType messageType, int sequenceNumber, String payload) {
super(messageType, sequenceNumber, payload);
}
public ExtensionCode getExtensionCode() {
if (isExtended()) {
return code;
} else {
return ExtensionCode.NOT_EXTENDED;
}
}
@Override
public String getPayload() {
return payloadToHexString();
}
public boolean isError() {
return code == ExtensionCode.ERROR;
}
public boolean isExtended() {
return code != ExtensionCode.NOT_EXTENDED && code != ExtensionCode.SUCCESS && code != ExtensionCode.ERROR;
}
public boolean isSuccess() {
return code == ExtensionCode.SUCCESS;
}
public boolean isTimeOut() {
return code == ExtensionCode.TIMEOUT;
}
@Override
protected void parsePayload() {
if (getType() == ACKNOWLEDGEMENT_V1) {
parseV1Payload();
} else if (getType() == ACKNOWLEDGEMENT_V2) {
parseV2Payload();
}
}
private void parseV1Payload() {
Matcher shortMatcher = V1_SHORT_PAYLOAD_PATTERN.matcher(payload);
Matcher extendedMatcher = V1_EXTENDED_PAYLOAD_PATTERN.matcher(payload);
if (extendedMatcher.matches()) {
code = ExtensionCode.forValue(Integer.parseInt(extendedMatcher.group(1), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
macAddress = new MACAddress(extendedMatcher.group(2));
} else if (shortMatcher.matches()) {
code = ExtensionCode.forValue(Integer.parseInt(shortMatcher.group(1), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
} else {
code = ExtensionCode.UNKNOWN;
throw new PlugwisePayloadMismatchException(ACKNOWLEDGEMENT_V1, V1_SHORT_PAYLOAD_PATTERN,
V1_EXTENDED_PAYLOAD_PATTERN, payload);
}
}
private void parseV2Payload() {
Matcher matcher = V2_EXTENDED_PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
code = ExtensionCode.forValue(Integer.parseInt(matcher.group(2), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
} else {
code = ExtensionCode.UNKNOWN;
throw new PlugwisePayloadMismatchException(ACKNOWLEDGEMENT_V2, V2_EXTENDED_PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.ANNOUNCE_AWAKE_REQUEST;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A sleeping end device (SED: Scan, Sense, Switch) sends this message to announce that is awake.
*
* @author Wouter Born - Initial contribution
*/
public class AnnounceAwakeRequestMessage extends Message {
public enum AwakeReason {
/** The SED joins the network for maintenance */
MAINTENANCE(0),
/** The SED joins a network for the first time */
JOIN_NETWORK(1),
/** The SED joins a network it has already joined, e.g. after reinserting a battery */
REJOIN_NETWORK(2),
/** When a SED switches a device group or when reporting values such as temperature/humidity */
NORMAL(3),
/** A human pressed the button on a SED to wake it up */
WAKEUP_BUTTON(5);
public static AwakeReason forValue(int value) {
return Arrays.stream(values()).filter(awakeReason -> awakeReason.id == value).findFirst().get();
}
private final int id;
AwakeReason(int id) {
this.id = id;
}
}
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})");
private AwakeReason awakeReason;
public AnnounceAwakeRequestMessage(int sequenceNumber, String payload) {
super(ANNOUNCE_AWAKE_REQUEST, sequenceNumber, payload);
}
public AwakeReason getAwakeReason() {
return awakeReason;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
awakeReason = AwakeReason.forValue(Integer.parseInt(matcher.group(2)));
} else {
throw new PlugwisePayloadMismatchException(ANNOUNCE_AWAKE_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.BROADCAST_GROUP_SWITCH_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A sleeping end device (SED: Scan, Sense, Switch) sends this message to switch groups on/off when the configured
* switching conditions have been met.
*
* @author Wouter Born - Initial contribution
*/
public class BroadcastGroupSwitchResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})(\\w{2})");
private int portMask;
private boolean powerState;
public BroadcastGroupSwitchResponseMessage(int sequenceNumber, String payload) {
super(BROADCAST_GROUP_SWITCH_RESPONSE, sequenceNumber, payload);
}
public int getPortMask() {
return portMask;
}
public boolean getPowerState() {
return powerState;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
portMask = Integer.parseInt(matcher.group(2));
powerState = (matcher.group(3).equals("01"));
} else {
throw new PlugwisePayloadMismatchException(BROADCAST_GROUP_SWITCH_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_GET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the current clock value of a device. This message is answered by a {@link ClockGetResponseMessage} which
* contains the clock value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockGetRequestMessage extends Message {
public ClockGetRequestMessage(MACAddress macAddress) {
super(CLOCK_GET_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_GET_RESPONSE;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the current clock value of a device. This message is the response of a {@link ClockGetRequestMessage}. Not
* all response fields have been reverse engineered.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockGetResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})");
private int hour;
private int minutes;
private int seconds;
private int weekday;
public ClockGetResponseMessage(int sequenceNumber, String payload) {
super(CLOCK_GET_RESPONSE, sequenceNumber, payload);
}
public int getHour() {
return hour;
}
public int getMinutes() {
return minutes;
}
public int getSeconds() {
return seconds;
}
public int getWeekday() {
return weekday;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
hour = Integer.parseInt(matcher.group(2), 16);
minutes = Integer.parseInt(matcher.group(3), 16);
seconds = Integer.parseInt(matcher.group(4), 16);
weekday = Integer.parseInt(matcher.group(5), 16);
} else {
throw new PlugwisePayloadMismatchException(CLOCK_GET_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
public String getTime() {
ZonedDateTime utcDateTime = ZonedDateTime.now(UTC).withHour(hour).withMinute(minutes).withSecond(seconds)
.withNano(0);
ZonedDateTime localDateTime = utcDateTime.withZoneSameInstant(ZoneId.systemDefault());
return DateTimeFormatter.ISO_LOCAL_TIME.format(localDateTime);
}
}

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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_SET_REQUEST;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the clock of the Circle+. Based on what is known about the Plugwise protocol, only the clock of the Circle+ has
* to be set. The Circle+ sets the clock of all other network nodes.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockSetRequestMessage extends Message {
private ZonedDateTime utcDateTime;
public ClockSetRequestMessage(MACAddress macAddress, LocalDateTime localDateTime) {
super(CLOCK_SET_REQUEST, macAddress);
// Nodes expect clock info to be in the UTC timezone
this.utcDateTime = localDateTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(UTC);
}
@Override
protected String payloadToHexString() {
String year = String.format("%02X", utcDateTime.getYear() - 2000);
String month = String.format("%02X", utcDateTime.getMonthValue());
String minutes = String.format("%04X",
(utcDateTime.getDayOfMonth() - 1) * 24 * 60 + (utcDateTime.getHour() * 60) + utcDateTime.getMinute());
// If we set logaddress to FFFFFFFFF then previous buffered data will be kept by the Circle+
String logaddress = "FFFFFFFF";
String hour = String.format("%02X", utcDateTime.getHour());
String minute = String.format("%02X", utcDateTime.getMinute());
String second = String.format("%02X", utcDateTime.getSecond());
// Monday = 0, ... , Sunday = 6
String dayOfWeek = String.format("%02X", utcDateTime.getDayOfWeek().getValue() - 1);
return year + month + minutes + logaddress + hour + minute + second + dayOfWeek;
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_INFORMATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests generic device information. This message is answered by an {@link InformationResponseMessage} which contains
* the device information.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class InformationRequestMessage extends Message {
public InformationRequestMessage(MACAddress macAddress) {
super(DEVICE_INFORMATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,126 @@
/**
* 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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_INFORMATION_RESPONSE;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains generic device information. This message is the response of an {@link InformationRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class InformationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{4})(\\w{8})(\\w{2})(\\w{2})(\\w{12})(\\w{8})(\\w{2})");
private int year;
private int month;
private int minutes;
private int logAddress;
private boolean powerState;
private int hertz;
private String hardwareVersion;
private LocalDateTime firmwareVersion;
private DeviceType deviceType;
public InformationResponseMessage(int sequenceNumber, String payload) {
super(DEVICE_INFORMATION_RESPONSE, sequenceNumber, payload);
}
public DeviceType getDeviceType() {
return deviceType;
}
public LocalDateTime getFirmwareVersion() {
return firmwareVersion;
}
public String getHardwareVersion() {
return hardwareVersion;
}
public int getHertz() {
return (hertz == 133) ? 50 : 60;
}
public int getLogAddress() {
return logAddress;
}
public int getMinutes() {
return minutes;
}
public int getMonth() {
return month;
}
public boolean getPowerState() {
return powerState;
}
public int getYear() {
return year;
}
private DeviceType intToDeviceType(int i) {
switch (i) {
case 0:
return DeviceType.STICK;
case 1:
return DeviceType.CIRCLE_PLUS;
case 2:
return DeviceType.CIRCLE;
case 3:
return DeviceType.SWITCH;
case 5:
return DeviceType.SENSE;
case 6:
return DeviceType.SCAN;
case 9:
return DeviceType.STEALTH;
default:
return null;
}
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
year = Integer.parseInt(matcher.group(2), 16) + 2000;
month = Integer.parseInt(matcher.group(3), 16);
minutes = Integer.parseInt(matcher.group(4), 16);
logAddress = (Integer.parseInt(matcher.group(5), 16) - 278528) / 32;
powerState = (matcher.group(6).equals("01"));
hertz = Integer.parseInt(matcher.group(7), 16);
hardwareVersion = matcher.group(8).substring(0, 4) + "-" + matcher.group(8).substring(4, 8) + "-"
+ matcher.group(8).substring(8, 12);
firmwareVersion = LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(matcher.group(9), 16)), UTC);
deviceType = intToDeviceType(Integer.parseInt(matcher.group(10), 16));
} else {
throw new PlugwisePayloadMismatchException(DEVICE_INFORMATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.LIGHT_CALIBRATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Calibrates the daylight override boundary of a Scan. The best time to do this is at night when lights are on.
*
* @author Wouter Born - Initial contribution
*/
public class LightCalibrationRequestMessage extends Message {
public LightCalibrationRequestMessage(MACAddress macAddress) {
super(LIGHT_CALIBRATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,162 @@
/**
* 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.plugwise.internal.protocol;
import java.io.UnsupportedEncodingException;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* Base class to represent Plugwise protocol data units.
*
* In general a message consists of a hex string containing the following parts:
* <ul>
* <li>a type indicator - many types are yet to be reverse engineered
* <li>a sequence number - messages are numbered so that we can keep track of them in an application
* <li>a MAC address - the destination of the message
* <li>a payload
* <li>a CRC checksum that is calculated using the previously mentioned segments of the message
* </ul>
*
* Before sending off a message in the Plugwise network they are prepended with a protocol header and trailer is
* added at the end.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public abstract class Message {
public static String getCRC(String string) {
int crc = 0x0000;
int polynomial = 0x1021; // 0001 0000 0010 0001 (0, 5, 12)
byte[] bytes = new byte[0];
try {
bytes = string.getBytes("ASCII");
} catch (UnsupportedEncodingException e) {
return "";
}
for (byte b : bytes) {
for (int i = 0; i < 8; i++) {
boolean bit = ((b >> (7 - i) & 1) == 1);
boolean c15 = ((crc >> 15 & 1) == 1);
crc <<= 1;
if (c15 ^ bit) {
crc ^= polynomial;
}
}
}
crc &= 0xFFFF;
return (String.format("%04X", crc));
}
protected MessageType type;
protected Integer sequenceNumber;
protected MACAddress macAddress;
protected String payload;
public Message(MessageType messageType) {
this(messageType, null, null, null);
}
public Message(MessageType messageType, Integer sequenceNumber, MACAddress macAddress, String payload) {
this.type = messageType;
this.sequenceNumber = sequenceNumber;
this.macAddress = macAddress;
this.payload = payload;
if (payload != null) {
parsePayload();
}
}
public Message(MessageType messageType, Integer sequenceNumber, String payload) {
this(messageType, sequenceNumber, null, payload);
}
public Message(MessageType messageType, MACAddress macAddress) {
this(messageType, null, macAddress, null);
}
public Message(MessageType messageType, MACAddress macAddress, String payload) {
this(messageType, null, macAddress, payload);
}
public Message(MessageType messageType, String payload) {
this(messageType, null, null, payload);
}
public MACAddress getMACAddress() {
return macAddress;
}
public String getPayload() {
return payload;
}
public int getSequenceNumber() {
return sequenceNumber;
}
public MessageType getType() {
return type;
}
// Method that implementation classes have to override, and that is responsible for parsing the payload into
// meaningful fields
protected void parsePayload() {
}
protected String payloadToHexString() {
return payload != null ? payload : "";
}
private String sequenceNumberToHexString() {
return String.format("%04X", sequenceNumber);
}
public void setSequenceNumber(Integer sequenceNumber) {
this.sequenceNumber = sequenceNumber;
}
public String toHexString() {
StringBuilder sb = new StringBuilder();
sb.append(typeToHexString());
if (sequenceNumber != null) {
sb.append(sequenceNumberToHexString());
}
if (macAddress != null) {
sb.append(macAddress);
}
sb.append(payloadToHexString());
String string = sb.toString();
String crc = getCRC(string);
return string + crc;
}
@Override
public String toString() {
return "Message [type=" + (type != null ? type.name() : null) + ", macAddress=" + macAddress
+ ", sequenceNumber=" + sequenceNumber + ", payload=" + payload + "]";
}
private String typeToHexString() {
return String.format("%04X", type.toInt());
}
}

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.plugwise.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* Creates instances of messages received from the Plugwise network.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class MessageFactory {
public Message createMessage(MessageType messageType, int sequenceNumber, String payload)
throws IllegalArgumentException {
switch (messageType) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
return new AcknowledgementMessage(messageType, sequenceNumber, payload);
case ANNOUNCE_AWAKE_REQUEST:
return new AnnounceAwakeRequestMessage(sequenceNumber, payload);
case BROADCAST_GROUP_SWITCH_RESPONSE:
return new BroadcastGroupSwitchResponseMessage(sequenceNumber, payload);
case CLOCK_GET_RESPONSE:
return new ClockGetResponseMessage(sequenceNumber, payload);
case DEVICE_INFORMATION_RESPONSE:
return new InformationResponseMessage(sequenceNumber, payload);
case DEVICE_ROLE_CALL_RESPONSE:
return new RoleCallResponseMessage(sequenceNumber, payload);
case MODULE_JOINED_NETWORK_REQUEST:
return new ModuleJoinedNetworkRequestMessage(sequenceNumber, payload);
case NETWORK_STATUS_RESPONSE:
return new NetworkStatusResponseMessage(sequenceNumber, payload);
case NODE_AVAILABLE:
return new NodeAvailableMessage(sequenceNumber, payload);
case PING_RESPONSE:
return new PingResponseMessage(sequenceNumber, payload);
case POWER_BUFFER_RESPONSE:
return new PowerBufferResponseMessage(sequenceNumber, payload);
case POWER_CALIBRATION_RESPONSE:
return new PowerCalibrationResponseMessage(sequenceNumber, payload);
case POWER_INFORMATION_RESPONSE:
return new PowerInformationResponseMessage(sequenceNumber, payload);
case REAL_TIME_CLOCK_GET_RESPONSE:
return new RealTimeClockGetResponseMessage(sequenceNumber, payload);
case SENSE_REPORT_REQUEST:
return new SenseReportRequestMessage(sequenceNumber, payload);
default:
throw new IllegalArgumentException("Unsupported message type: " + messageType);
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.MODULE_JOINED_NETWORK_REQUEST;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Module joined network request. Sent when a SED (re)joins the network. E.g. when you reinsert the battery of a Scan.
*
* @author Wouter Born - Initial contribution
*/
public class ModuleJoinedNetworkRequestMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})");
public ModuleJoinedNetworkRequestMessage(int sequenceNumber, String payload) {
super(MODULE_JOINED_NETWORK_REQUEST, sequenceNumber, payload);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
} else {
throw new PlugwisePayloadMismatchException(MODULE_JOINED_NETWORK_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_RESET_REQUEST;
/**
* Requests the Plugwise network to be reset. Currently not used in the binding.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkResetRequestMessage extends Message {
public NetworkResetRequestMessage(String payload) {
super(NETWORK_RESET_REQUEST, payload);
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_STATUS_REQUEST;
/**
* Requests the network status from the Stick. This message is answered by a {@link NetworkStatusResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkStatusRequestMessage extends Message {
public NetworkStatusRequestMessage() {
super(NETWORK_STATUS_REQUEST);
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_STATUS_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the current network status as well as the MAC address of the Circle+ that coordinates the network. The Stick
* sends this message as response of a {@link NetworkStatusRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkStatusResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{16})(\\w{4})(\\w{2})");
private boolean online;
private String networkID;
private String unknown1;
private String unknown2;
private String shortNetworkID;
private MACAddress circlePlusMAC;
public NetworkStatusResponseMessage(int sequenceNumber, String payload) {
super(NETWORK_STATUS_RESPONSE, sequenceNumber, payload);
}
public NetworkStatusResponseMessage(String payload) {
super(NETWORK_STATUS_RESPONSE, payload);
}
public MACAddress getCirclePlusMAC() {
return circlePlusMAC;
}
public String getNetworkID() {
return networkID;
}
public String getShortNetworkID() {
return shortNetworkID;
}
public String getUnknown1() {
return unknown1;
}
public String getUnknown2() {
return unknown2;
}
public boolean isOnline() {
return online;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
unknown1 = matcher.group(2);
online = (Integer.parseInt(matcher.group(3), 16) == 1);
networkID = matcher.group(4);
shortNetworkID = matcher.group(5);
unknown2 = matcher.group(6);
// now some serious protocol reverse-engineering assumption. Circle+ MAC = networkID with first two bytes
// replaced by 00
circlePlusMAC = new MACAddress("00" + networkID.substring(2));
} else {
throw new PlugwisePayloadMismatchException(NETWORK_STATUS_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
@Override
protected String payloadToHexString() {
return unknown1 + String.format("%02X", online ? 1 : 0) + networkID + shortNetworkID + unknown2;
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NODE_AVAILABLE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Node available messages are broadcasted by nodes that are not yet part of a network. They are currently unused
* because typically the network is configured using the Plugwise Source software, and never changed after.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NodeAvailableMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})");
public NodeAvailableMessage(int sequenceNumber, String payload) {
super(NODE_AVAILABLE, sequenceNumber, payload);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
} else {
throw new PlugwisePayloadMismatchException(NODE_AVAILABLE, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NODE_AVAILABLE_RESPONSE;
/**
* Response to a device when its {@link NodeAvailableMessage} is "accepted".
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NodeAvailableResponseMessage extends Message {
private boolean acceptanceCode;
private String destinationMAC;
public NodeAvailableResponseMessage(boolean code, String destination) {
super(NODE_AVAILABLE_RESPONSE);
acceptanceCode = code;
destinationMAC = destination;
}
public boolean isAcceptanceCode() {
return acceptanceCode;
}
@Override
protected String payloadToHexString() {
return String.format("%02X", acceptanceCode ? 1 : 0) + destinationMAC;
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.PING_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests a {@link PingResponseMessage} from a device.
*
* @author Wouter Born - Initial contribution
*/
public class PingRequestMessage extends Message {
public PingRequestMessage(MACAddress macAddress) {
super(PING_REQUEST, macAddress);
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.PING_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains network diagnostic information. This message is the response of a {@link PingRequestMessage}.
*
* @author Wouter Born - Initial contribution
*/
public class PingResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{4})");
private int inRSSI;
private int outRSSI;
private int pingMillis;
public PingResponseMessage(int sequenceNumber, String payload) {
super(PING_RESPONSE, sequenceNumber, payload);
}
public int getInRSSI() {
return inRSSI;
}
public int getOutRSSI() {
return outRSSI;
}
public int getPingMillis() {
return pingMillis;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
inRSSI = (Integer.parseInt(matcher.group(2), 16));
outRSSI = (Integer.parseInt(matcher.group(3), 16));
pingMillis = (Integer.parseInt(matcher.group(4), 16));
} else {
throw new PlugwisePayloadMismatchException(PING_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.plugwise.internal.protocol;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* The payload of a message does not match the expected message pattern. Thrown whenever the payload of a received
* message could not be parsed.
*
* @author Wouter Born - Initial contribution
*/
public class PlugwisePayloadMismatchException extends RuntimeException {
private static final long serialVersionUID = 1160553788698072410L;
public PlugwisePayloadMismatchException(MessageType messageType, Pattern pattern0, Pattern pattern1,
String payload) {
super(String.format("Plugwise %s payload mismatch: %s does not match %s or %s", messageType.name(), payload,
pattern0.pattern(), pattern1.pattern()));
}
public PlugwisePayloadMismatchException(MessageType messageType, Pattern pattern, String payload) {
super(String.format("Plugwise %s payload mismatch: %s does not match %s", messageType.name(), payload,
pattern.pattern()));
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_BUFFER_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the historical pulse measurements at a certain log address from a device (Circle, Circle+, Stealth). This
* message is answered by a {@link PowerBufferResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerBufferRequestMessage extends Message {
private int logAddress;
public PowerBufferRequestMessage(MACAddress macAddress, int logAddress) {
super(POWER_BUFFER_REQUEST, macAddress);
this.logAddress = logAddress;
}
@Override
protected String payloadToHexString() {
return String.format("%08X", (logAddress * 32 + 278528));
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_BUFFER_RESPONSE;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Contains the historical pulse measurements at a certain log address from a device (Circle, Circle+, Stealth). This
* message is the response of a {@link PowerBufferRequestMessage}. The consumed/produced {@link Energy} (kWh) of the
* datapoints can be calculated using {@link PowerCalibration} data.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerBufferResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})");
private static final String EMPTY_TIMESTAMP = "FFFFFFFF";
private Energy[] datapoints;
private int logAddress;
public PowerBufferResponseMessage(int sequenceNumber, String payload) {
super(POWER_BUFFER_RESPONSE, sequenceNumber, payload);
}
public Energy[] getDatapoints() {
return datapoints;
}
public int getLogAddress() {
return logAddress;
}
public Energy getMostRecentDatapoint() {
Energy result = null;
for (Energy datapoint : datapoints) {
if (datapoint != null) {
result = datapoint;
}
}
return result;
}
private Energy parseEnergy(String timeHex, String pulsesHex) {
ZonedDateTime utcDateTime = !timeHex.equals(EMPTY_TIMESTAMP) ? parseDateTime(timeHex) : null;
if (utcDateTime == null) {
return null;
}
long pulses = Long.parseLong(pulsesHex, 16);
return new Energy(utcDateTime, pulses);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
datapoints = new Energy[4];
datapoints[0] = parseEnergy(matcher.group(2), matcher.group(3));
datapoints[1] = parseEnergy(matcher.group(4), matcher.group(5));
datapoints[2] = parseEnergy(matcher.group(6), matcher.group(7));
datapoints[3] = parseEnergy(matcher.group(8), matcher.group(9));
logAddress = (Integer.parseInt(matcher.group(10), 16) - 278528) / 32;
} else {
throw new PlugwisePayloadMismatchException(POWER_BUFFER_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
private ZonedDateTime parseDateTime(String timeHex) {
int year = Integer.parseInt(timeHex.substring(0, 2), 16) + 2000;
int month = Integer.parseInt(timeHex.substring(2, 4), 16);
int minutes = Integer.parseInt(timeHex.substring(4, 8), 16);
return ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, UTC).plusMinutes(minutes);
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CALIBRATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Calibrates the power of a relay device (Circle, Circle+, Stealth). This message is answered by a
* {@link PowerCalibrationResponseMessage} which contains the {@link PowerCalibration} data.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerCalibrationRequestMessage extends Message {
public PowerCalibrationRequestMessage(MACAddress macAddress) {
super(POWER_CALIBRATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,78 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CALIBRATION_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Contains the power calibration data of a relay device (Circle, Circle+, Stealth). This message is the response of a
* {@link PowerCalibrationRequestMessage}. The {@link PowerCalibration} data is used to calculate power (W) and energy
* (kWh) from pulses with the {@link Energy} class.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerCalibrationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{8})(\\w{8})(\\w{8})(\\w{8})");
private double gainA;
private double gainB;
private double offsetTotal;
private double offsetNoise;
public PowerCalibrationResponseMessage(int sequenceNumber, String payload) {
super(POWER_CALIBRATION_RESPONSE, sequenceNumber, payload);
}
public double getGainA() {
return gainA;
}
public double getGainB() {
return gainB;
}
public double getOffsetNoise() {
return offsetNoise;
}
public double getOffsetTotal() {
return offsetTotal;
}
public PowerCalibration getCalibration() {
return new PowerCalibration(gainA, gainB, offsetNoise, offsetTotal);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
gainA = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(2), 16)));
gainB = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(3), 16)));
offsetTotal = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(4), 16)));
offsetNoise = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(5), 16)));
} else {
throw new PlugwisePayloadMismatchException(POWER_CALIBRATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CHANGE_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the power state of a relay device (Circle, Circle+, Stealth) to be switched on/off. The current power state
* of a device is retrieved by sending a {@link InformationRequestMessage} and reading the
* {@link InformationResponseMessage#getPowerState()} value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerChangeRequestMessage extends Message {
public PowerChangeRequestMessage(MACAddress macAddress, boolean powerState) {
super(POWER_CHANGE_REQUEST, macAddress, powerState ? "01" : "00");
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_INFORMATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Request real-time energy consumption from a relay device (Circle, Circle+, Stealth). This
* message is answered by a {@link PowerInformationResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerInformationRequestMessage extends Message {
public PowerInformationRequestMessage(MACAddress macAddress) {
super(POWER_INFORMATION_REQUEST, macAddress);
}
}

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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_INFORMATION_RESPONSE;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the real-time energy consumption of a relay device (Circle, Circle+, Stealth). This
* message is the response of a {@link PowerInformationRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerInformationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})(\\w{4})(\\w{8})(\\w{8})(\\w{4})");
private static final double NANOSECONDS_CORRECTION_DIVISOR = 0.000046875; // 46875 divided by nanos per second
private Energy oneSecond;
private Energy eightSecond;
private Energy oneHourConsumed;
private Energy oneHourProduced;
private long nanosCorrection;
public PowerInformationResponseMessage(int sequenceNumber, String payload) {
super(POWER_INFORMATION_RESPONSE, sequenceNumber, payload);
}
public Energy getEightSecond() {
return eightSecond;
}
public Energy getOneHourConsumed() {
return oneHourConsumed;
}
public Energy getOneHourProduced() {
return oneHourProduced;
}
public Energy getOneSecond() {
return oneSecond;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
ZonedDateTime utcNow = ZonedDateTime.now(UTC);
macAddress = new MACAddress(matcher.group(1));
nanosCorrection = Math.round(Integer.parseInt(matcher.group(6), 16) / NANOSECONDS_CORRECTION_DIVISOR);
oneSecond = new Energy(utcNow, Integer.parseInt(matcher.group(2), 16),
Duration.ofSeconds(1, nanosCorrection));
eightSecond = new Energy(utcNow, Integer.parseInt(matcher.group(3), 16),
Duration.ofSeconds(8, nanosCorrection));
oneHourConsumed = new Energy(utcNow, Long.parseLong(matcher.group(4), 16), Duration.ofHours(1));
oneHourProduced = new Energy(utcNow, Long.parseLong(matcher.group(5), 16), Duration.ofHours(1));
} else {
throw new PlugwisePayloadMismatchException(POWER_INFORMATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_LOG_INTERVAL_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the interval of historic power consumption and production measurements. These historic measurements are
* returned by the {@link PowerBufferRequestMessage}.
*
* @author Wouter Born - Initial contribution
*/
public class PowerLogIntervalSetRequestMessage extends Message {
private Duration consumptionInterval;
private Duration productionInterval;
public PowerLogIntervalSetRequestMessage(MACAddress macAddress, Duration consumptionInterval,
Duration productionInterval) {
super(POWER_LOG_INTERVAL_SET_REQUEST, macAddress);
this.consumptionInterval = consumptionInterval;
this.productionInterval = productionInterval;
}
@Override
protected String payloadToHexString() {
String consumptionIntervalHex = String.format("%04X", consumptionInterval.toMinutes());
String productionIntervalHex = String.format("%04X", productionInterval.toMinutes());
return consumptionIntervalHex + productionIntervalHex;
}
}

View File

@@ -0,0 +1,29 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_GET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the real-time clock value of a Circle+.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RealTimeClockGetRequestMessage extends Message {
public RealTimeClockGetRequestMessage(MACAddress macAddress) {
super(REAL_TIME_CLOCK_GET_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,99 @@
/**
* 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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_GET_RESPONSE;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the real-time clock value of a Circle+. This message is the response of a
* {@link RealTimeClockGetRequestMessage}. The Circle+ is the only device that holds a real-time clock value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RealTimeClockGetResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})");
private int seconds;
private int minutes;
private int hour;
private int weekday;
private int day;
private int month;
private int year;
public RealTimeClockGetResponseMessage(int sequenceNumber, String payload) {
super(REAL_TIME_CLOCK_GET_RESPONSE, sequenceNumber, payload);
}
public int getDay() {
return day;
}
public int getHour() {
return hour;
}
public int getMinutes() {
return minutes;
}
public int getMonth() {
return month;
}
public int getSeconds() {
return seconds;
}
public LocalDateTime getDateTime() {
ZonedDateTime utcDateTime = ZonedDateTime.of(year, month, day, hour, minutes, seconds, 0, UTC);
return utcDateTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
public int getWeekday() {
return weekday;
}
public int getYear() {
return year;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
// Real-time clock values in the message are decimals and not hexadecimals
seconds = Integer.parseInt(matcher.group(2));
minutes = Integer.parseInt(matcher.group(3));
hour = Integer.parseInt(matcher.group(4));
weekday = Integer.parseInt(matcher.group(5));
day = Integer.parseInt(matcher.group(6));
month = Integer.parseInt(matcher.group(7));
year = Integer.parseInt(matcher.group(8)) + 2000;
} else {
throw new PlugwisePayloadMismatchException(REAL_TIME_CLOCK_GET_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_SET_REQUEST;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the real-time clock value of a Circle+. The Circle+ is the only device that holds a real-time clock value.
*
* @author Wouter Born - Initial contribution
*/
public class RealTimeClockSetRequestMessage extends Message {
private ZonedDateTime utcDateTime;
public RealTimeClockSetRequestMessage(MACAddress macAddress, LocalDateTime localDateTime) {
super(REAL_TIME_CLOCK_SET_REQUEST, macAddress);
this.utcDateTime = localDateTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(UTC);
}
@Override
protected String payloadToHexString() {
// Real-time clock values in the message are decimals and not hexadecimals
String second = String.format("%02d", utcDateTime.getSecond());
String minute = String.format("%02d", utcDateTime.getMinute());
String hour = String.format("%02d", utcDateTime.getHour());
// Monday = 0, ... , Sunday = 6
String dayOfWeek = String.format("%02d", utcDateTime.getDayOfWeek().getValue() - 1);
String day = String.format("%02d", utcDateTime.getDayOfMonth());
String month = String.format("%02d", utcDateTime.getMonthValue());
String year = String.format("%02d", utcDateTime.getYear() - 2000);
return second + minute + hour + dayOfWeek + day + month + year;
}
}

View File

@@ -0,0 +1,44 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_ROLE_CALL_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the Circle+ to return the MAC address for a specific node. This message is answered by a
* {@link RoleCallResponseMessage} which contains the MAC address. Because a Plugwise network can have 64 devices,
* the node ID value has a range from 0 to 63.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RoleCallRequestMessage extends Message {
private int nodeID;
public RoleCallRequestMessage(MACAddress macAddress, int nodeID) {
super(DEVICE_ROLE_CALL_REQUEST, macAddress);
this.nodeID = nodeID;
}
@Override
public String getPayload() {
return String.format("%02X", nodeID);
}
@Override
protected String payloadToHexString() {
return String.format("%02X", nodeID);
}
}

View File

@@ -0,0 +1,70 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_ROLE_CALL_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* <p>
* The Circle+ sends this message as response to a {@link RoleCallRequestMessage}. It contains the MAC address for the
* the node identified by the node ID in the request message. When no node is known with given ID, the MAC
* address will be empty.
* </p>
* <p>
* The MAC address can belong to a relay device (Circle, Stealth) as well as a sleeping end device (SED: Scan, Sense,
* Switch). An {@link InformationRequestMessage} can be used to determine the actual device type (when it is online).
* </p>
* <p>
* The Circle+ MAC address can not be retrieved from the node list. The Circle+ MAC address can be retrieved with a
* {@link NetworkStatusRequestMessage}.
* </p>
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RoleCallResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{16})(\\w{2})");
private static final String EMPTY_MAC_ADDRESS = "FFFFFFFFFFFFFFFF";
private int nodeID;
private MACAddress nodeMAC;
public RoleCallResponseMessage(int sequenceNumber, String payload) {
super(DEVICE_ROLE_CALL_RESPONSE, sequenceNumber, payload);
}
public int getNodeID() {
return nodeID;
}
public MACAddress getNodeMAC() {
return nodeMAC;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
nodeMAC = matcher.group(2).equals(EMPTY_MAC_ADDRESS) ? null : new MACAddress(matcher.group(2));
nodeID = (Integer.parseInt(matcher.group(3), 16));
} else {
throw new PlugwisePayloadMismatchException(DEVICE_ROLE_CALL_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SCAN_PARAMETERS_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Sensitivity;
/**
* Sets the Scan motion detection parameters. These parameters control when the Scan sends on/off commands.
*
* @author Wouter Born - Initial contribution
*/
public class ScanParametersSetRequestMessage extends Message {
private Sensitivity sensitivity;
private boolean daylightOverride;
private Duration switchOffDelay;
public ScanParametersSetRequestMessage(MACAddress macAddress, Sensitivity sensitivity, boolean daylightOverride,
Duration switchOffDelay) {
super(SCAN_PARAMETERS_SET_REQUEST, macAddress);
this.sensitivity = sensitivity;
this.daylightOverride = daylightOverride;
this.switchOffDelay = switchOffDelay;
}
@Override
protected String payloadToHexString() {
String sensitivityHex = String.format("%02X", sensitivity.toInt());
String daylightOverrideHex = (daylightOverride ? "01" : "00");
String switchOffDelayHex = String.format("%02X", switchOffDelay.toMinutes());
return sensitivityHex + daylightOverrideHex + switchOffDelayHex;
}
}

View File

@@ -0,0 +1,74 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_BOUNDARIES_SET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryType;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* Sets the Sense boundary switching parameters. These parameters control when the Sense sends on/off commands.
*
* @author Wouter Born - Initial contribution
*/
public class SenseBoundariesSetRequestMessage extends Message {
private static final String MIN_BOUNDARY_VALUE = "0000";
private static final String MAX_BOUNDARY_VALUE = "FFFF";
private BoundaryType boundaryType;
private BoundaryAction boundaryAction;
private String lowerBoundaryHex;
private String upperBoundaryHex;
/**
* Disables Sense boundary switching.
*/
public SenseBoundariesSetRequestMessage(MACAddress macAddress) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.TEMPERATURE;
this.boundaryAction = BoundaryAction.OFF_BELOW_ON_ABOVE;
this.lowerBoundaryHex = MIN_BOUNDARY_VALUE;
this.upperBoundaryHex = MAX_BOUNDARY_VALUE;
}
public SenseBoundariesSetRequestMessage(MACAddress macAddress, Temperature lowerBoundary, Temperature upperBoundary,
BoundaryAction boundaryAction) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.TEMPERATURE;
this.boundaryAction = boundaryAction;
this.lowerBoundaryHex = lowerBoundary.toHex();
this.upperBoundaryHex = upperBoundary.toHex();
}
public SenseBoundariesSetRequestMessage(MACAddress macAddress, Humidity lowerBoundary, Humidity upperBoundary,
BoundaryAction boundaryAction) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.HUMIDITY;
this.boundaryAction = boundaryAction;
this.lowerBoundaryHex = lowerBoundary.toHex();
this.upperBoundaryHex = upperBoundary.toHex();
}
@Override
protected String payloadToHexString() {
String boundaryTypeHex = String.format("%02X", boundaryType.toInt());
String lowerBoundaryActionHex = String.format("%02X", boundaryAction.getLowerAction());
String upperBoundaryActionHex = String.format("%02X", boundaryAction.getUpperAction());
return boundaryTypeHex + upperBoundaryHex + upperBoundaryActionHex + lowerBoundaryHex + lowerBoundaryActionHex;
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_REPORT_INTERVAL_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the Sense temperature and humidity measurement report interval. Based on this interval, periodically a
* {@link SenseReportRequestMessage} is sent.
*
* @author Wouter Born - Initial contribution
*/
public class SenseReportIntervalSetRequest extends Message {
private Duration reportInterval;
public SenseReportIntervalSetRequest(MACAddress macAddress, Duration reportInterval) {
super(SENSE_REPORT_INTERVAL_SET_REQUEST, macAddress);
this.reportInterval = reportInterval;
}
@Override
protected String payloadToHexString() {
return String.format("%02X", reportInterval.toMinutes());
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_REPORT_REQUEST;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* A Sense periodically sends this message for updating the current temperature and humidity.
*
* @author Wouter Born - Initial contribution
*/
public class SenseReportRequestMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})(\\w{4})");
private Humidity humidity;
private Temperature temperature;
public SenseReportRequestMessage(int sequenceNumber, String payload) {
super(SENSE_REPORT_REQUEST, sequenceNumber, payload);
}
public Humidity getHumidity() {
return humidity;
}
public Temperature getTemperature() {
return temperature;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
humidity = new Humidity(matcher.group(2));
temperature = new Temperature(matcher.group(3));
} else {
throw new PlugwisePayloadMismatchException(SENSE_REPORT_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

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.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SLEEP_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets when sleeping end devices (Scan, Sense, Switch) sleep and wake-up .
*
* @author Wouter Born - Initial contribution
*/
public class SleepSetRequestMessage extends Message {
private static final Duration DEFAULT_SLEEP_DURATION = Duration.ofSeconds(5);
private Duration wakeupDuration;
private Duration sleepDuration;
private Duration wakeupInterval;
private int unknown;
public SleepSetRequestMessage(MACAddress macAddress, Duration wakeupDuration, Duration sleepDuration,
Duration wakeupInterval) {
super(SLEEP_SET_REQUEST, macAddress);
this.wakeupDuration = wakeupDuration;
this.sleepDuration = sleepDuration;
this.wakeupInterval = wakeupInterval;
}
public SleepSetRequestMessage(MACAddress macAddress, Duration wakeupDuration, Duration wakeupInterval) {
this(macAddress, wakeupDuration, DEFAULT_SLEEP_DURATION, wakeupInterval);
}
@Override
protected String payloadToHexString() {
String wakeupDurationHex = String.format("%02X", wakeupDuration.getSeconds());
String sleepDurationHex = String.format("%04X", sleepDuration.getSeconds());
String wakeupIntervalHex = String.format("%04X", wakeupInterval.toMinutes());
String unknownHex = String.format("%06X", unknown);
return wakeupDurationHex + sleepDurationHex + wakeupIntervalHex + unknownHex;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The boundary switch action of a Sense when the value is below/above the boundary minimum/maximum.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum BoundaryAction {
OFF_BELOW_ON_ABOVE(0, 1),
ON_BELOW_OFF_ABOVE(1, 0);
private final int lowerAction;
private final int upperAction;
BoundaryAction(int lowerAction, int upperAction) {
this.lowerAction = lowerAction;
this.upperAction = upperAction;
}
public int getLowerAction() {
return lowerAction;
}
public int getUpperAction() {
return upperAction;
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The boundary type that a Sense uses for switching.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum BoundaryType {
HUMIDITY(0),
TEMPERATURE(1),
NONE(2);
private final int identifier;
BoundaryType(int identifier) {
this.identifier = identifier;
}
public int toInt() {
return identifier;
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enumerates Plugwise devices.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum DeviceType {
STICK("Stick", false, false),
CIRCLE("Circle", true, false),
CIRCLE_PLUS("Circle+", true, false),
SCAN("Scan", false, true),
SENSE("Sense", false, true),
STEALTH("Stealth", true, false),
SWITCH("Switch", false, true),
UNKNOWN("Unknown", false, false);
private final String string;
private final boolean relayDevice;
private final boolean sleepingEndDevice;
DeviceType(String string, boolean relayDevice, boolean sleepingEndDevice) {
this.string = string;
this.relayDevice = relayDevice;
this.sleepingEndDevice = sleepingEndDevice;
}
public boolean isRelayDevice() {
return relayDevice;
}
public boolean isSleepingEndDevice() {
return sleepingEndDevice;
}
@Override
public String toString() {
return string;
}
}

View File

@@ -0,0 +1,123 @@
/**
* 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.plugwise.internal.protocol.field;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A simple class to represent energy usage, converting between Plugwise data representations.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class Energy {
private static final int WATTS_PER_KILOWATT = 1000;
private static final double PULSES_PER_KW_SECOND = 468.9385193;
private static final double PULSES_PER_W_SECOND = PULSES_PER_KW_SECOND / WATTS_PER_KILOWATT;
private @Nullable ZonedDateTime utcStart; // using UTC resolves wrong local start/end timestamps when DST changes
// occur
private ZonedDateTime utcEnd;
private long pulses;
private @Nullable Duration interval;
public Energy(ZonedDateTime utcEnd, long pulses) {
this.utcEnd = utcEnd;
this.pulses = pulses;
}
public Energy(ZonedDateTime utcEnd, long pulses, Duration interval) {
this.utcEnd = utcEnd;
this.pulses = pulses;
this.interval = interval;
updateStart(interval);
}
private double correctPulses(double pulses, PowerCalibration calibration) {
double gainA = calibration.getGainA();
double gainB = calibration.getGainB();
double offsetNoise = calibration.getOffsetNoise();
double offsetTotal = calibration.getOffsetTotal();
double correctedPulses = Math.pow(pulses + offsetNoise, 2) * gainB + (pulses + offsetNoise) * gainA
+ offsetTotal;
if ((pulses > 0 && correctedPulses < 0) || (pulses < 0 && correctedPulses > 0)) {
return 0;
}
return correctedPulses;
}
public LocalDateTime getEnd() {
return utcEnd.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
public @Nullable Duration getInterval() {
return interval;
}
public long getPulses() {
return pulses;
}
public @Nullable LocalDateTime getStart() {
ZonedDateTime localUtcStart = utcStart;
if (localUtcStart == null) {
return null;
}
return localUtcStart.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
private double intervalSeconds() {
Duration localInterval = interval;
if (localInterval == null) {
throw new IllegalStateException("Failed to calculate seconds because interval is null");
}
double seconds = localInterval.getSeconds();
seconds += localInterval.getNano() / ChronoUnit.SECONDS.getDuration().toNanos();
return seconds;
}
public void setInterval(Duration interval) {
this.interval = interval;
updateStart(interval);
}
public double tokWh(PowerCalibration calibration) {
return toWatt(calibration) * intervalSeconds()
/ (ChronoUnit.HOURS.getDuration().getSeconds() * WATTS_PER_KILOWATT);
}
@Override
public String toString() {
return "Energy [utcStart=" + utcStart + ", utcEnd=" + utcEnd + ", pulses=" + pulses + ", interval=" + interval
+ "]";
}
public double toWatt(PowerCalibration calibration) {
double averagePulses = pulses / intervalSeconds();
return correctPulses(averagePulses, calibration) / PULSES_PER_W_SECOND;
}
private void updateStart(Duration interval) {
utcStart = utcEnd.minus(interval);
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A relative humidity class that is used for converting from and to Plugwise protocol values.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Humidity {
private static final String EMPTY_VALUE = "FFFF";
private static final double MAX_HEX_VALUE = 65536;
private static final double MULTIPLIER = 125;
private static final double OFFSET = 6;
private final double value;
public Humidity(double value) {
this.value = value;
}
public Humidity(String hexValue) {
if (EMPTY_VALUE.equals(hexValue)) {
value = Double.MIN_VALUE;
} else {
value = MULTIPLIER * (Integer.parseInt(hexValue, 16) / MAX_HEX_VALUE) - OFFSET;
}
}
public double getValue() {
return value;
}
public String toHex() {
return String.format("%04X", Math.round((value + OFFSET) / MULTIPLIER * MAX_HEX_VALUE));
}
@Override
public String toString() {
return String.format("%.3f%%", value);
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The media access control (MAC) address of a Plugwise device, e.g.: 000D6F0000A1B2C3
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class MACAddress {
private final String macAddress;
public MACAddress(String macAddress) {
this.macAddress = macAddress.toUpperCase();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + macAddress.hashCode();
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MACAddress other = (MACAddress) obj;
if (!macAddress.equals(other.macAddress)) {
return false;
}
return true;
}
@Override
public String toString() {
return macAddress;
}
}

View File

@@ -0,0 +1,87 @@
/**
* 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.plugwise.internal.protocol.field;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Enumerates all Plugwise message types. Many are still missing, and require further protocol analysis.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public enum MessageType {
ACKNOWLEDGEMENT_V1(0x0000),
NODE_AVAILABLE(0x0006),
NODE_AVAILABLE_RESPONSE(0x0007),
NETWORK_RESET_REQUEST(0x0008),
NETWORK_STATUS_REQUEST(0x000A),
PING_REQUEST(0x000D),
PING_RESPONSE(0x000E),
NETWORK_STATUS_RESPONSE(0x0011),
POWER_INFORMATION_REQUEST(0x0012),
POWER_INFORMATION_RESPONSE(0x0013),
CLOCK_SET_REQUEST(0x0016),
POWER_CHANGE_REQUEST(0x0017),
DEVICE_ROLE_CALL_REQUEST(0x0018),
DEVICE_ROLE_CALL_RESPONSE(0x0019),
DEVICE_INFORMATION_REQUEST(0x0023),
DEVICE_INFORMATION_RESPONSE(0x0024),
POWER_CALIBRATION_REQUEST(0x0026),
POWER_CALIBRATION_RESPONSE(0x0027),
REAL_TIME_CLOCK_SET_REQUEST(0x0028),
REAL_TIME_CLOCK_GET_REQUEST(0x0029),
REAL_TIME_CLOCK_GET_RESPONSE(0x003A),
CLOCK_GET_REQUEST(0x003E),
CLOCK_GET_RESPONSE(0x003F),
POWER_BUFFER_REQUEST(0x0048),
POWER_BUFFER_RESPONSE(0x0049),
ANNOUNCE_AWAKE_REQUEST(0x004F),
SLEEP_SET_REQUEST(0x0050),
POWER_LOG_INTERVAL_SET_REQUEST(0x0057),
BROADCAST_GROUP_SWITCH_RESPONSE(0x0056),
MODULE_JOINED_NETWORK_REQUEST(0x0061),
ACKNOWLEDGEMENT_V2(0x0100),
SCAN_PARAMETERS_SET_REQUEST(0x0101),
LIGHT_CALIBRATION_REQUEST(0x0102),
SENSE_REPORT_INTERVAL_SET_REQUEST(0x0103),
SENSE_BOUNDARIES_SET_REQUEST(0x0104),
SENSE_REPORT_REQUEST(0x0105);
private static final Map<Integer, MessageType> TYPES_BY_VALUE = new HashMap<>();
static {
for (MessageType type : MessageType.values()) {
TYPES_BY_VALUE.put(type.identifier, type);
}
}
private final int identifier;
MessageType(int value) {
identifier = value;
}
public static @Nullable MessageType forValue(int value) {
return TYPES_BY_VALUE.get(value);
}
public int toInt() {
return identifier;
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The power calibration data of a relay device (Circle, Circle+, Stealth). It is used in {@link Energy} to calculate
* energy (kWh) and power (W) from pulses.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PowerCalibration {
private final double gainA;
private final double gainB;
private final double offsetTotal;
private final double offsetNoise;
public PowerCalibration(double gainA, double gainB, double offsetNoise, double offsetTotal) {
this.gainA = gainA;
this.gainB = gainB;
this.offsetNoise = offsetNoise;
this.offsetTotal = offsetTotal;
}
public double getGainA() {
return gainA;
}
public double getGainB() {
return gainB;
}
public double getOffsetTotal() {
return offsetTotal;
}
public double getOffsetNoise() {
return offsetNoise;
}
@Override
public String toString() {
return "PowerCalibration [gainA=" + gainA + ", gainB=" + gainB + ", offsetTotal=" + offsetTotal
+ ", offsetNoise=" + offsetNoise + "]";
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The motion sensitivity range of a Scan.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum Sensitivity {
HIGH(0x14),
MEDIUM(0x1E),
OFF(0xFF);
private final int value;
Sensitivity(int value) {
this.value = value;
}
public int toInt() {
return value;
}
}

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.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A temperature (Celsius) class that is used for converting from and to Plugwise protocol values.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Temperature {
private static final String EMPTY_VALUE = "FFFF";
private static final double MAX_HEX_VALUE = 65536;
private static final double MULTIPLIER = 175.72;
private static final double OFFSET = 46.85;
private final double value;
public Temperature(double value) {
this.value = value;
}
public Temperature(String hexValue) {
if (EMPTY_VALUE.equals(hexValue)) {
value = Double.MIN_VALUE;
} else {
value = MULTIPLIER * (Integer.parseInt(hexValue, 16) / MAX_HEX_VALUE) - OFFSET;
}
}
public double getValue() {
return value;
}
public String toHex() {
return String.format("%04X", Math.round((value + OFFSET) / MULTIPLIER * MAX_HEX_VALUE));
}
@Override
public String toString() {
return String.format("%.3f\u00B0C", value);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="plugwise" 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>Plugwise Binding</name>
<description>Monitor and control Plugwise ZigBee devices using the Stick. Supported devices are the Circle, Circle+,
Scan, Sense, Stealth and Switch.</description>
<author>Wouter Born</author>
</binding:binding>

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="bridge-type:plugwise:stick">
<parameter name="serialPort" type="text" required="true">
<label>Serial Port</label>
<context>serial-port</context>
<limitToOptions>false</limitToOptions>
<description>The serial port of the Stick, e.g. "/dev/ttyUSB0" for Linux or "COM1" for Windows</description>
</parameter>
<parameter name="messageWaitTime" type="integer" min="0" max="500" step="50">
<label>Message Wait Time</label>
<description>The time to wait between messages sent on the ZigBee network (in ms)</description>
<default>150</default>
<unitLabel>ms</unitLabel>
</parameter>
</config-description>
<config-description uri="channel-type:plugwise:fasterupdates">
<parameter name="updateInterval" type="integer" min="1" required="true" unit="s">
<label>Update Interval</label>
<description>Specifies at what rate the state is updated (in seconds)</description>
<default>15</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
<config-description uri="channel-type:plugwise:slowerupdates">
<parameter name="updateInterval" type="integer" min="1" required="true" unit="s">
<label>Update Interval</label>
<description>Specifies at what rate the state is updated (in seconds)</description>
<default>60</default>
<unitLabel>s</unitLabel>
</parameter>
</config-description>
<config-description uri="thing-type:plugwise:relay">
<parameter name="macAddress" type="text"
pattern="(000)(d|D)6(f|F)(0000)([0-9A-Fa-f]{6})|(000)(d|D)6(f|F)(000)([0-9A-Fa-f]{7})" required="true">
<label>MAC Address</label>
<description>The full device MAC address e.g. "000D6F0000A1B2C3"</description>
</parameter>
<parameter name="powerStateChanging" type="text" required="false">
<label>Power State Changing</label>
<description>Controls if the power state can be changed with commands or is always on/off</description>
<default>commandSwitching</default>
<options>
<option value="commandSwitching">Command switching</option>
<option value="alwaysOn">Always on</option>
<option value="alwaysOff">Always off</option>
</options>
</parameter>
<parameter name="suppliesPower" type="boolean" required="false">
<label>Supplies Power</label>
<description>Enables power production measurements</description>
<default>false</default>
</parameter>
<parameter name="measurementInterval" type="integer" min="5" max="60" step="5" required="false" unit="min">
<label>Measurement Interval</label>
<description>The energy measurement interval (in minutes)</description>
<default>60</default>
<unitLabel>m</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="temporarilyNotInNetwork" type="boolean" required="false">
<label>Temporarily Not in Network</label>
<description>Stops searching for an unplugged device on the ZigBee network traffic</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="updateConfiguration" type="boolean" required="true" readOnly="true">
<label>Update Configuration</label>
<description>Stores if the device configuration is up to date (automatically enabled/disabled)</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:plugwise:scan">
<parameter name="macAddress" type="text"
pattern="(000)(d|D)6(f|F)(0000)([0-9A-Fa-f]{6})|(000)(d|D)6(f|F)(000)([0-9A-Fa-f]{7})" required="true">
<label>MAC Address</label>
<description>The full device MAC address e.g. "000D6F0000A1B2C3"</description>
</parameter>
<parameter name="sensitivity" type="text" required="false">
<label>Sensitivity</label>
<description>The sensitivity of movement detection</description>
<default>medium</default>
<options>
<option value="off">Off</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</options>
</parameter>
<parameter name="switchOffDelay" type="integer" min="1" max="240" required="false" unit="min">
<label>Switch Off Delay</label>
<description>The delay the Scan waits before sending an off command when motion is no longer detected (in minutes)</description>
<default>5</default>
<unitLabel>m</unitLabel>
</parameter>
<parameter name="daylightOverride" type="boolean" required="false">
<label>Daylight Override</label>
<description>Disables movement detection when there is daylight</description>
<default>false</default>
</parameter>
<parameter name="wakeupInterval" type="integer" min="5" max="1440" step="60" required="false" unit="min">
<label>Wake-up Interval</label>
<description>The interval in which the Scan wakes up at least once (in minutes)</description>
<default>1440</default>
<unitLabel>m</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="wakeupDuration" type="integer" min="10" max="120" step="10" required="false" unit="s">
<label>Wake-up Duration</label>
<description>The number of seconds the Scan stays awake after it woke up</description>
<default>10</default>
<unitLabel>s</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="recalibrate" type="boolean" required="false">
<label>Recalibrate</label>
<description>Calculates a new daylight override boundary when the Scan wakes up</description>
<default>false</default>
<advanced>true</advanced>
</parameter>
<parameter name="updateConfiguration" type="boolean" required="true" readOnly="true">
<label>Update Configuration</label>
<description>Stores if the Scan configuration is up to date (automatically enabled/disabled)</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:plugwise:sense">
<parameter name="macAddress" type="text"
pattern="(000)(d|D)6(f|F)(0000)([0-9A-Fa-f]{6})|(000)(d|D)6(f|F)(000)([0-9A-Fa-f]{7})" required="true">
<label>MAC Address</label>
<description>The full device MAC address e.g. "000D6F0000A1B2C3"</description>
</parameter>
<parameter name="measurementInterval" type="integer" min="5" max="60" step="5" required="false" unit="min">
<label>Measurement Interval</label>
<description>The interval in which the Sense measures the temperature and humidity (in minutes)</description>
<default>15</default>
<unitLabel>m</unitLabel>
</parameter>
<parameter name="boundaryType" type="text" required="false">
<label>Boundary Type</label>
<description>The boundary type that is used for switching</description>
<default>none</default>
<options>
<option value="none">None</option>
<option value="temperature">Temperature</option>
<option value="humidity">Humidity</option>
</options>
</parameter>
<parameter name="boundaryAction" type="text" required="false">
<label>Boundary Action</label>
<description>The boundary switch action when the value is below/above the boundary minimum/maximum</description>
<default>offBelowOnAbove</default>
<options>
<option value="offBelowOnAbove">Off below / On above</option>
<option value="onBelowOffAbove">On below / Off above</option>
</options>
</parameter>
<parameter name="temperatureBoundaryMin" type="integer" min="0" max="60" step="5" required="false"
unit="Cel">
<label>Temperature Minimum</label>
<description>The minimum boundary for the temperature boundary action</description>
<default>15</default>
</parameter>
<parameter name="temperatureBoundaryMax" type="integer" min="0" max="60" step="5" required="false"
unit="Cel">
<label>Temperature Maximum</label>
<description>The maximum boundary for the temperature boundary action</description>
<default>25</default>
</parameter>
<parameter name="humidityBoundaryMin" type="integer" min="5" max="95" step="5" required="false" unit="%">
<label>Humidity Minimum</label>
<description>The minimum boundary for the humidity boundary action</description>
<default>45</default>
</parameter>
<parameter name="humidityBoundaryMax" type="integer" min="5" max="95" step="5" required="false" unit="%">
<label>Humidity Maximum</label>
<description>The maximum boundary for the humidity boundary action</description>
<default>65</default>
</parameter>
<parameter name="wakeupInterval" type="integer" min="5" max="1440" step="60" required="false" unit="min">
<label>Wake-up Interval</label>
<description>The interval in which the Sense wakes up at least once (in minutes)</description>
<default>1440</default>
<unitLabel>m</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="wakeupDuration" type="integer" min="10" max="120" step="10" required="false" unit="s">
<label>Wake-up Duration</label>
<description>The number of seconds the Sense stays awake after it woke up</description>
<default>10</default>
<unitLabel>s</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="updateConfiguration" type="boolean" required="true" readOnly="true">
<label>Update Configuration</label>
<description>Stores if the Sense configuration is up to date (automatically enabled/disabled)</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:plugwise:switch">
<parameter name="macAddress" type="text"
pattern="(000)(d|D)6(f|F)(0000)([0-9A-Fa-f]{6})|(000)(d|D)6(f|F)(000)([0-9A-Fa-f]{7})" required="true">
<label>MAC Address</label>
<description>The full device MAC address e.g. "000D6F0000A1B2C3"</description>
</parameter>
<parameter name="wakeupInterval" type="integer" min="5" max="1440" step="60" required="false" unit="min">
<label>Wake-up Interval</label>
<description>The interval in which the Switch wakes up at least once (in minutes)</description>
<default>1440</default>
<unitLabel>m</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="wakeupDuration" type="integer" min="10" max="120" step="10" required="false" unit="s">
<label>Wake-up Duration</label>
<description>The number of seconds the Switch stays awake after it woke up</description>
<default>10</default>
<unitLabel>s</unitLabel>
<advanced>true</advanced>
</parameter>
<parameter name="updateConfiguration" type="boolean" required="true" readOnly="true">
<label>Update Configuration</label>
<description>Stores if the Switch configuration is up to date (automatically enabled/disabled)</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,186 @@
# binding
binding.plugwise.name = Plugwise Binding
binding.plugwise.description = Monitor and control Plugwise ZigBee devices using the Stick. Supported devices are the Circle, Circle+, Scan, Sense, Stealth and Switch.
# bridge type configuration parameters
bridge-type.config.plugwise.stick.serialPort.label = Serial port
bridge-type.config.plugwise.stick.serialPort.description = The serial port of the Stick, e.g. "/dev/ttyUSB0" for Linux or "COM1" for Windows
bridge-type.config.plugwise.stick.messageWaitTime.label = Message wait time
bridge-type.config.plugwise.stick.messageWaitTime.description = The time to wait between messages sent on the ZigBee network (in ms)
# thing types
thing-type.plugwise.circle.label = Plugwise Circle
thing-type.plugwise.circle.description = A power outlet plug that provides energy measurement and switching control of appliances
thing-type.plugwise.circleplus.label = Plugwise Circle+
thing-type.plugwise.circleplus.description = A special Circle that coordinates the ZigBee network and acts as network gateway
thing-type.plugwise.scan.label = Plugwise Scan
thing-type.plugwise.scan.description = A wireless motion (PIR) and light sensor
thing-type.plugwise.sense.label = Plugwise Sense
thing-type.plugwise.sense.description = A wireless temperature and humidity sensor
thing-type.plugwise.stealth.label = Plugwise Stealth
thing-type.plugwise.stealth.description = A Circle with a more compact form factor that can be built-in
thing-type.plugwise.stick.label = Plugwise Stick
thing-type.plugwise.stick.description = A ZigBee USB controller used for communicating with the Circle+
thing-type.plugwise.switch.label = Plugwise Switch
thing-type.plugwise.switch.description = A wireless wall switch
# Relay thing type configuration parameters
thing-type.config.plugwise.relay.macAddress.label = MAC address
thing-type.config.plugwise.relay.macAddress.description = The full device MAC address e.g. "000D6F0000A1B2C3"
thing-type.config.plugwise.relay.powerStateChanging.label = Power state changing
thing-type.config.plugwise.relay.powerStateChanging.description = Controls if the power state can be changed with commands or is always on/off
thing-type.config.plugwise.relay.powerStateChanging.option.commandSwitching = Command switching
thing-type.config.plugwise.relay.powerStateChanging.option.alwaysOn = Always on
thing-type.config.plugwise.relay.powerStateChanging.option.alwaysOff = Always off
thing-type.config.plugwise.relay.suppliesPower.label = Supplies power
thing-type.config.plugwise.relay.suppliesPower.description = Enables power production measurements
thing-type.config.plugwise.relay.measurementInterval.label = Measurement interval
thing-type.config.plugwise.relay.measurementInterval.description = The energy measurement interval (in minutes)
thing-type.config.plugwise.relay.temporarilyNotInNetwork.label = Temporarily not in network
thing-type.config.plugwise.relay.temporarilyNotInNetwork.description = Stops searching for an unplugged device on the ZigBee network
thing-type.config.plugwise.relay.updateConfiguration.label = Update configuration
thing-type.config.plugwise.relay.updateConfiguration.description = Stores if the device configuration is up to date (automatically enabled/disabled)
# Scan thing type configuration parameters
thing-type.config.plugwise.scan.macAddress.label = MAC address
thing-type.config.plugwise.scan.macAddress.description = The full device MAC address e.g. "000D6F0000A1B2C3"
thing-type.config.plugwise.scan.sensitivity.label = Sensitivity
thing-type.config.plugwise.scan.sensitivity.description = The sensitivity of movement detection
thing-type.config.plugwise.scan.sensitivity.option.off = Off
thing-type.config.plugwise.scan.sensitivity.option.medium = Medium
thing-type.config.plugwise.scan.sensitivity.option.high = High
thing-type.config.plugwise.scan.switchOffDelay.label = Switch off delay
thing-type.config.plugwise.scan.switchOffDelay.description = The delay the Scan waits before sending an off command when motion is no longer detected (in minutes)
thing-type.config.plugwise.scan.daylightOverride.label = Daylight override
thing-type.config.plugwise.scan.daylightOverride.description = Disables movement detection when there is daylight
thing-type.config.plugwise.scan.wakeupInterval.label = Wake-up interval
thing-type.config.plugwise.scan.wakeupInterval.description = The interval in which the Scan wakes up at least once (in minutes)
thing-type.config.plugwise.scan.wakeupDuration.label = Wake-up duration
thing-type.config.plugwise.scan.wakeupDuration.description = The number of seconds the Scan stays awake after it woke up
thing-type.config.plugwise.scan.recalibrate.label = Recalibrate
thing-type.config.plugwise.scan.recalibrate.description = Calculates a new daylight override boundary when the Scan wakes up
thing-type.config.plugwise.scan.updateConfiguration.label = Update configuration
thing-type.config.plugwise.scan.updateConfiguration.description = Stores if the Scan configuration is up to date (automatically enabled/disabled)
# Sense thing type configuration parameters
thing-type.config.plugwise.sense.macAddress.label = MAC address
thing-type.config.plugwise.sense.macAddress.description = The full device MAC address e.g. "000D6F0000A1B2C3"
thing-type.config.plugwise.sense.measurementInterval.label = Measurement interval
thing-type.config.plugwise.sense.measurementInterval.description = The interval in which the Sense measures the temperature and humidity (in minutes)
thing-type.config.plugwise.sense.boundaryType.label = Boundary type
thing-type.config.plugwise.sense.boundaryType.description = The boundary type that is used for switching
thing-type.config.plugwise.sense.boundaryType.option.none = None
thing-type.config.plugwise.sense.boundaryType.option.temperature = Temperature
thing-type.config.plugwise.sense.boundaryType.option.humidity = Humidity
thing-type.config.plugwise.sense.boundaryAction.label = Boundary action
thing-type.config.plugwise.sense.boundaryAction.description = The boundary switch action when the value is below/above the boundary minimum/maximum
thing-type.config.plugwise.sense.boundaryAction.option.offBelowOnAbove = Off below / On above
thing-type.config.plugwise.sense.boundaryAction.option.onBelowOffAbove = On below / Off above
thing-type.config.plugwise.sense.temperatureBoundaryMin.label = Temperature minimum
thing-type.config.plugwise.sense.temperatureBoundaryMin.description = The minimum boundary for the temperature boundary action
thing-type.config.plugwise.sense.temperatureBoundaryMax.label = Temperature maximum
thing-type.config.plugwise.sense.temperatureBoundaryMax.description = The maximum boundary for the temperature boundary action
thing-type.config.plugwise.sense.humidityBoundaryMin.label = Humidity minimum
thing-type.config.plugwise.sense.humidityBoundaryMin.description = The minimum boundary for the humidity boundary action
thing-type.config.plugwise.sense.humidityBoundaryMax.label = Humidity maximum
thing-type.config.plugwise.sense.humidityBoundaryMax.description = The maximum boundary for the humidity boundary action
thing-type.config.plugwise.sense.wakeupInterval.label = Wake-up interval
thing-type.config.plugwise.sense.wakeupInterval.description = The interval in which the Sense wakes up at least once (in minutes)
thing-type.config.plugwise.sense.wakeupDuration.label = Wake-up duration
thing-type.config.plugwise.sense.wakeupDuration.description = The number of seconds the Sense stays awake after it woke up
thing-type.config.plugwise.sense.updateConfiguration.label = Update configuration
thing-type.config.plugwise.sense.updateConfiguration.description = Stores if the Sense configuration is up to date (automatically enabled/disabled)
# Switch thing type configuration parameters
thing-type.config.plugwise.switch.macAddress.label = MAC address
thing-type.config.plugwise.switch.macAddress.description = The full device MAC address e.g. "000D6F0000A1B2C3"
thing-type.config.plugwise.switch.wakeupInterval.label = Wake-up interval
thing-type.config.plugwise.switch.wakeupInterval.description = The interval in which the Switch wakes up at least once (in minutes)
thing-type.config.plugwise.switch.wakeupDuration.label = Wake-up duration
thing-type.config.plugwise.switch.wakeupDuration.description = The number of seconds the Switch stays awake after it woke up
thing-type.config.plugwise.switch.updateConfiguration.label = Update configuration
thing-type.config.plugwise.switch.updateConfiguration.description = Stores if the Switch configuration is up to date (automatically enabled/disabled)
# channel types
channel-type.plugwise.clock.label = Clock
channel-type.plugwise.clock.description = Time as indicated by the internal clock of the device
channel-type.plugwise.humidity.label = Humidity
channel-type.plugwise.humidity.description = Current relative humidity
channel-type.plugwise.energy.label = Energy
channel-type.plugwise.energy.description = Energy consumption/production during the last measurement interval
channel-type.plugwise.energystamp.label = Energy timestamp
channel-type.plugwise.energystamp.description = Timestamp of the start of the last energy measurement interval
channel-type.plugwise.lastseen.label = Last seen
channel-type.plugwise.lastseen.description = Timestamp of the last received message
channel-type.plugwise.leftbuttonstate.label = Left button state
channel-type.plugwise.leftbuttonstate.description = Current state of the left button
channel-type.plugwise.power.label = Power
channel-type.plugwise.power.description = Current power consumption/production
channel-type.plugwise.realtimeclock.label = Real-time clock
channel-type.plugwise.realtimeclock.description = Time as indicated by the real-time internal clock of the Circle+
channel-type.plugwise.rightbuttonstate.label = Right button state
channel-type.plugwise.rightbuttonstate.description = Current state of the right button
channel-type.plugwise.state.label = State
channel-type.plugwise.state.description = Switches the power state on/off
channel-type.plugwise.temperature.label = Temperature
channel-type.plugwise.temperature.description = Current temperature
channel-type.plugwise.triggered.label = Triggered
channel-type.plugwise.triggered.description = Most recent switch action initiated by the device
# channel type configuration parameters
channel-type.config.plugwise.slowerupdates.updateInterval.label = Update interval
channel-type.config.plugwise.slowerupdates.updateInterval.description = Specifies at what rate the state is updated (in seconds)
channel-type.config.plugwise.fasterupdates.updateInterval.label = Update interval
channel-type.config.plugwise.fasterupdates.updateInterval.description = Specifies at what rate the state is updated (in seconds)

View File

@@ -0,0 +1,186 @@
# binding
binding.plugwise.name = Plugwise Binding
binding.plugwise.description = Monitor en schakel Plugwise ZigBee apparaten met de Stick. Ondersteunde apparaten zijn de Circle, Circle+, Scan, Sense, Stealth en Switch.
# bridge type configuration parameters
bridge-type.config.plugwise.stick.serialPort.label = Seriële poort
bridge-type.config.plugwise.stick.serialPort.description = De seriële poort van de Stick, bv. "/dev/ttyUSB0" voor Linux of "COM1" voor Windows
bridge-type.config.plugwise.stick.messageWaitTime.label = Bericht wachttijd
bridge-type.config.plugwise.stick.messageWaitTime.description = De tijd die gewacht wordt tussen het versturen van berichten op het ZigBee netwerk (in ms)
# thing types
thing-type.plugwise.circle.label = Plugwise Circle
thing-type.plugwise.circle.description = Een wandcontactdoos stekker die energie meet en apparaten schakelt
thing-type.plugwise.circleplus.label = Plugwise Circle+
thing-type.plugwise.circleplus.description = Een speciale Circle die het ZigBee netwerk coördineert en als netwerkpoort fungeert
thing-type.plugwise.scan.label = Plugwise Scan
thing-type.plugwise.scan.description = Een draadloze bewegingsmelding (PIR) en lichtsterktemeter
thing-type.plugwise.sense.label = Plugwise Sense
thing-type.plugwise.sense.description = Een draadloze temperatuur- en luchtvochtigheidsmeter
thing-type.plugwise.stealth.label = Plugwise Stealth
thing-type.plugwise.stealth.description = Een Circle in een compactere vormfactor die ingebouwd kan worden
thing-type.plugwise.stick.label = Plugwise Stick
thing-type.plugwise.stick.description = Een ZigBee USB controller die met de Circle+ communiceert
thing-type.plugwise.switch.label = Plugwise Switch
thing-type.plugwise.switch.description = Een draadloze muurschakelaar
# Relay thing type configuration parameters
thing-type.config.plugwise.relay.macAddress.label = MAC-adres
thing-type.config.plugwise.relay.macAddress.description = Het volledige MAC-adres van het apparaat bv. "000D6F0000A1B2C3"
thing-type.config.plugwise.relay.powerStateChanging.label = Stroom schakelen
thing-type.config.plugwise.relay.powerStateChanging.description = Bepaald of de stroom met commando's wordt geschakeld of altijd aan/uit is
thing-type.config.plugwise.relay.powerStateChanging.option.commandSwitching = Commando schakelen
thing-type.config.plugwise.relay.powerStateChanging.option.alwaysOn = Altijd aan
thing-type.config.plugwise.relay.powerStateChanging.option.alwaysOff = Altijd uit
thing-type.config.plugwise.relay.suppliesPower.label = Levert stroom
thing-type.config.plugwise.relay.suppliesPower.description = Zet metingen van stroomproductie aan
thing-type.config.plugwise.relay.measurementInterval.label = Meetinterval
thing-type.config.plugwise.relay.measurementInterval.description = Het energie meetinterval (in minuten)
thing-type.config.plugwise.relay.temporarilyNotInNetwork.label = Tijdelijk niet in netwerk
thing-type.config.plugwise.relay.temporarilyNotInNetwork.description = Stopt het zoeken naar een ontkoppeld apparaat op het ZigBee netwerk
thing-type.config.plugwise.relay.updateConfiguration.label = Configuratie bijwerken
thing-type.config.plugwise.relay.updateConfiguration.description = Bewaard of de apparaat configuratie bijgewerkt is (automatische activatie/deactivatie)
# Scan thing type configuration parameters
thing-type.config.plugwise.scan.macAddress.label = MAC-adres
thing-type.config.plugwise.scan.macAddress.description = Het volledige MAC-adres van het apparaat bv. "000D6F0000A1B2C3"
thing-type.config.plugwise.scan.sensitivity.label = Gevoeligheid
thing-type.config.plugwise.scan.sensitivity.description = De gevoeligheid van bewegingsdetectie
thing-type.config.plugwise.scan.sensitivity.option.off = Uit
thing-type.config.plugwise.scan.sensitivity.option.medium = Gemiddeld
thing-type.config.plugwise.scan.sensitivity.option.high = Hoog
thing-type.config.plugwise.scan.switchOffDelay.label = Uitschakelvertraging
thing-type.config.plugwise.scan.switchOffDelay.description = De vertraging van het uitschakelen nadat de Scan geen beweging meer waarneemt (in minuten)
thing-type.config.plugwise.scan.daylightOverride.label = Daglicht opheffing
thing-type.config.plugwise.scan.daylightOverride.description = Schakelt bewegingsdetectie uit bij daglicht
thing-type.config.plugwise.scan.wakeupInterval.label = Ontwaakinterval
thing-type.config.plugwise.scan.wakeupInterval.description = Het interval waarin de Scan minstens eenmalig ontwaakt (in minuten)
thing-type.config.plugwise.scan.wakeupDuration.label = Ontwaakduur
thing-type.config.plugwise.scan.wakeupDuration.description = Het aantal seconden dat de Scan wakker blijft na ontwaking
thing-type.config.plugwise.scan.recalibrate.label = Herkalibreren
thing-type.config.plugwise.scan.recalibrate.description = Herkalibreert de daglicht opheffingsgrens wanneer de Scan ontwaakt
thing-type.config.plugwise.scan.updateConfiguration.label = Configuratie bijwerken
thing-type.config.plugwise.scan.updateConfiguration.description = Bewaard of de Scan configuratie bijgewerkt is (automatische activatie/deactivatie)
# Sense thing type configuration parameters
thing-type.config.plugwise.sense.macAddress.label = MAC-adres
thing-type.config.plugwise.sense.macAddress.description = Het volledige MAC-adres van het apparaat bv. "000D6F0000A1B2C3"
thing-type.config.plugwise.sense.measurementInterval.label = Meetinterval
thing-type.config.plugwise.sense.measurementInterval.description = Het interval waarin de Sense temperatuur en luchtvochtigheid meet (in minuten)
thing-type.config.plugwise.sense.boundaryType.label = Grenstype
thing-type.config.plugwise.sense.boundaryType.description = Het grenstype dat gebruikt wordt om te schakelen
thing-type.config.plugwise.sense.boundaryType.option.none = Geen
thing-type.config.plugwise.sense.boundaryType.option.temperature = Temperatuur
thing-type.config.plugwise.sense.boundaryType.option.humidity = Luchtvochtigheid
thing-type.config.plugwise.sense.boundaryAction.label = Grensactie
thing-type.config.plugwise.sense.boundaryAction.description = De schakelactie indien de meetwaarde beneden/boven het grensminimum/maximum komt
thing-type.config.plugwise.sense.boundaryAction.option.offBelowOnAbove = Uit beneden / Aan boven
thing-type.config.plugwise.sense.boundaryAction.option.onBelowOffAbove = Aan beneden / Uit boven
thing-type.config.plugwise.sense.temperatureBoundaryMin.label = Temperatuur minimum
thing-type.config.plugwise.sense.temperatureBoundaryMin.description = Het grensminimum van de temperatuur schakelactie
thing-type.config.plugwise.sense.temperatureBoundaryMax.label = Temperatuur maximum
thing-type.config.plugwise.sense.temperatureBoundaryMax.description = Het grensmaximum van de temperatuur schakelactie
thing-type.config.plugwise.sense.humidityBoundaryMin.label = Luchtvochtigheid minimum
thing-type.config.plugwise.sense.humidityBoundaryMin.description = Het grensminimum van de luchtvochtigheid schakelactie
thing-type.config.plugwise.sense.humidityBoundaryMax.label = Luchtvochtigheid maximum
thing-type.config.plugwise.sense.humidityBoundaryMax.description = Het grensmaximum van de luchtvochtigheid schakelactie
thing-type.config.plugwise.sense.wakeupInterval.label = Ontwaakinterval
thing-type.config.plugwise.sense.wakeupInterval.description = Het interval waarin de Sense minstens eenmalig ontwaakt (in minuten)
thing-type.config.plugwise.sense.wakeupDuration.label = Ontwaakduur
thing-type.config.plugwise.sense.wakeupDuration.description = Het aantal seconden dat de Sense wakker blijft na ontwaking
thing-type.config.plugwise.sense.updateConfiguration.label = Configuratie bijwerken
thing-type.config.plugwise.sense.updateConfiguration.description = Bewaard of de Sense configuratie bijgewerkt is (automatische activatie/deactivatie)
# Switch thing type configuration parameters
thing-type.config.plugwise.switch.macAddress.label = MAC-adres
thing-type.config.plugwise.switch.macAddress.description = Het volledige MAC-adres van het apparaat bv. "000D6F0000A1B2C3"
thing-type.config.plugwise.switch.wakeupInterval.label = Ontwaakinterval
thing-type.config.plugwise.switch.wakeupInterval.description = Het interval waarin de Switch minstens eenmalig ontwaakt (in minuten)
thing-type.config.plugwise.switch.wakeupDuration.label = Ontwaakduur
thing-type.config.plugwise.switch.wakeupDuration.description = Het aantal seconden dat de Switch wakker blijft na ontwaking
thing-type.config.plugwise.switch.updateConfiguration.label = Configuratie bijwerken
thing-type.config.plugwise.switch.updateConfiguration.description = Bewaard of de Switch configuratie bijgewerkt is (automatische activatie/deactivatie)
# channel types
channel-type.plugwise.clock.label = Klok
channel-type.plugwise.clock.description = Tijd aangegeven door de interne klok van het apparaat
channel-type.plugwise.humidity.label = Luchtvochtigheid
channel-type.plugwise.humidity.description = Huidige relatieve luchtvochtigheid
channel-type.plugwise.energy.label = Energie
channel-type.plugwise.energy.description = Energie verbruik/productie tijdens het laatste meetinterval
channel-type.plugwise.energystamp.label = Energie tijdstempel
channel-type.plugwise.energystamp.description = Tijdstempel van het begin van het laatste energie meetinterval
channel-type.plugwise.lastseen.label = Laatst gezien
channel-type.plugwise.lastseen.description = Tijdstempel van het laatst ontvangen bericht
channel-type.plugwise.leftbuttonstate.label = Linker knop status
channel-type.plugwise.leftbuttonstate.description = Huidige status van de linker knop
channel-type.plugwise.power.label = Vermogen
channel-type.plugwise.power.description = Huidige verbruik/productie vermogen
channel-type.plugwise.realtimeclock.label = Real-time klok
channel-type.plugwise.realtimeclock.description = Tijd aangegeven door de real-time interne klok van de Circle+
channel-type.plugwise.rightbuttonstate.label = Rechter knop status
channel-type.plugwise.rightbuttonstate.description = Huidige status van de rechter knop
channel-type.plugwise.state.label = Status
channel-type.plugwise.state.description = Schakelt de stroom status aan/uit
channel-type.plugwise.temperature.label = Temperatuur
channel-type.plugwise.temperature.description = Huidige temperatuur
channel-type.plugwise.triggered.label = Getriggerd
channel-type.plugwise.triggered.description = De meest recente door het apparaat geïnitieerde schakelactie
# channel type configuration parameters
channel-type.config.plugwise.slowerupdates.updateInterval.label = Bijwerk tijdsinterval
channel-type.config.plugwise.slowerupdates.updateInterval.description = Specificeert het tijdsinterval waarmee de status wordt bijgewerkt (in seconden)
channel-type.config.plugwise.fasterupdates.updateInterval.label = Bijwerk tijdsinterval
channel-type.config.plugwise.fasterupdates.updateInterval.description = Specificeert het tijdsinterval waarmee de status wordt bijgewerkt (in seconden)

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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">
<channel-type id="clock" advanced="true">
<item-type>String</item-type>
<label>Clock</label>
<description>Time as indicated by the internal clock of the device</description>
<state readOnly="true"></state>
<config-description-ref uri="channel-type:plugwise:slowerupdates"/>
</channel-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Current relative humidity</description>
<category>Humidity</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="energy">
<item-type>Number:Energy</item-type>
<label>Energy</label>
<description>Energy consumption/production during the last measurement interval</description>
<category>Energy</category>
<state readOnly="true" pattern="%.3f %unit%"/>
<config-description-ref uri="channel-type:plugwise:slowerupdates"/>
</channel-type>
<channel-type id="energystamp" advanced="true">
<item-type>DateTime</item-type>
<label>Energy Timestamp</label>
<description>Timestamp of the start of the last energy measurement interval</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastseen" advanced="true">
<item-type>DateTime</item-type>
<label>Last Seen</label>
<description>Timestamp of the last received message</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="leftbuttonstate">
<item-type>Switch</item-type>
<label>Left Button State</label>
<description>Current state of the left button</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="power">
<item-type>Number:Power</item-type>
<label>Power</label>
<description>Current power consumption/production</description>
<category>Energy</category>
<state readOnly="true" pattern="%.1f %unit%"/>
<config-description-ref uri="channel-type:plugwise:fasterupdates"/>
</channel-type>
<channel-type id="realtimeclock" advanced="true">
<item-type>DateTime</item-type>
<label>Real-time Clock</label>
<description>Time as indicated by the real-time internal clock of the Circle+</description>
<state readOnly="true"></state>
<config-description-ref uri="channel-type:plugwise:slowerupdates"/>
</channel-type>
<channel-type id="rightbuttonstate">
<item-type>Switch</item-type>
<label>Right Button State</label>
<description>Current state of the right button</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="state">
<item-type>Switch</item-type>
<label>State</label>
<description>Switches the power state on/off</description>
<category>PowerOutlet</category>
<state readOnly="false"/>
<config-description-ref uri="channel-type:plugwise:fasterupdates"/>
</channel-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="triggered">
<item-type>Switch</item-type>
<label>Triggered</label>
<description>Most recent switch action initiated by the device</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="circle">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Circle</label>
<description>A power outlet plug that provides energy measurement and switching control of appliances</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="circleplus">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Circle+</label>
<description>A special Circle that coordinates the ZigBee network and acts as network gateway</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="realtimeclock" typeId="realtimeclock"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="scan">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Scan</label>
<description>A wireless motion (PIR) and light sensor</description>
<channels>
<channel id="triggered" typeId="triggered"/>
<channel id="lastseen" typeId="lastseen"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:scan"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="sense">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Sense</label>
<description>A wireless temperature and humidity sensor</description>
<channels>
<channel id="humidity" typeId="humidity"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="temperature" typeId="temperature"/>
<channel id="triggered" typeId="triggered"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:sense"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="stealth">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Stealth</label>
<description>A Circle with a more compact form factor that can be built-in</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="stick">
<label>Plugwise Stick</label>
<description>A ZigBee USB controller used for communicating with the Circle+</description>
<representation-property>macAddress</representation-property>
<config-description-ref uri="bridge-type:plugwise:stick"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
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="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Switch</label>
<description>A wireless wall switch</description>
<channels>
<channel id="lastseen" typeId="lastseen"/>
<channel id="leftbuttonstate" typeId="leftbuttonstate"/>
<channel id="rightbuttonstate" typeId="rightbuttonstate"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:switch"/>
</thing-type>
</thing:thing-descriptions>