added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user