added migrated 2.x add-ons

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

View File

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

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import static java.util.stream.Collectors.*;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link PlugwiseBinding} class defines common constants, which are used across the whole binding.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseBindingConstants {
public static final String BINDING_ID = "plugwise";
// List of all Channel IDs
public static final String CHANNEL_CLOCK = "clock";
public static final String CHANNEL_ENERGY = "energy";
public static final String CHANNEL_ENERGY_STAMP = "energystamp";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_LAST_SEEN = "lastseen";
public static final String CHANNEL_LEFT_BUTTON_STATE = "leftbuttonstate";
public static final String CHANNEL_POWER = "power";
public static final String CHANNEL_REAL_TIME_CLOCK = "realtimeclock";
public static final String CHANNEL_RIGHT_BUTTON_STATE = "rightbuttonstate";
public static final String CHANNEL_STATE = "state";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_TRIGGERED = "triggered";
// List of all configuration properties
public static final String CONFIG_PROPERTY_MAC_ADDRESS = "macAddress";
public static final String CONFIG_PROPERTY_RECALIBRATE = "recalibrate";
public static final String CONFIG_PROPERTY_SERIAL_PORT = "serialPort";
public static final String CONFIG_PROPERTY_UPDATE_CONFIGURATION = "updateConfiguration";
public static final String CONFIG_PROPERTY_UPDATE_INTERVAL = "updateInterval";
// List of all property IDs
public static final String PROPERTY_HERTZ = "hertz";
public static final String PROPERTY_MAC_ADDRESS = "macAddress";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_CIRCLE = new ThingTypeUID(BINDING_ID, "circle");
public static final ThingTypeUID THING_TYPE_CIRCLE_PLUS = new ThingTypeUID(BINDING_ID, "circleplus");
public static final ThingTypeUID THING_TYPE_SCAN = new ThingTypeUID(BINDING_ID, "scan");
public static final ThingTypeUID THING_TYPE_SENSE = new ThingTypeUID(BINDING_ID, "sense");
public static final ThingTypeUID THING_TYPE_STEALTH = new ThingTypeUID(BINDING_ID, "stealth");
public static final ThingTypeUID THING_TYPE_STICK = new ThingTypeUID(BINDING_ID, "stick");
public static final ThingTypeUID THING_TYPE_SWITCH = new ThingTypeUID(BINDING_ID, "switch");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(THING_TYPE_CIRCLE, THING_TYPE_CIRCLE_PLUS, THING_TYPE_SCAN, THING_TYPE_SENSE, THING_TYPE_STEALTH,
THING_TYPE_STICK, THING_TYPE_SWITCH)
.collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
}

View File

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

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import java.io.IOException;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.config.PlugwiseStickConfig;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.ThingUID;
/**
* The {@link PlugwiseCommunicationHandler} handles all serial communication with the Plugwise Stick.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseCommunicationHandler {
private final PlugwiseCommunicationContext context;
private final PlugwiseMessageProcessor messageProcessor;
private final PlugwiseMessageSender messageSender;
private boolean initialized = false;
public PlugwiseCommunicationHandler(ThingUID bridgeUID, Supplier<PlugwiseStickConfig> configurationSupplier,
SerialPortManager serialPortManager) {
context = new PlugwiseCommunicationContext(bridgeUID, configurationSupplier, serialPortManager);
messageProcessor = new PlugwiseMessageProcessor(context);
messageSender = new PlugwiseMessageSender(context);
}
public void addMessageListener(PlugwiseMessageListener listener) {
context.getFilteredListeners().addListener(listener);
}
public void addMessageListener(PlugwiseMessageListener listener, MACAddress macAddress) {
context.getFilteredListeners().addListener(listener, macAddress);
}
public void removeMessageListener(PlugwiseMessageListener listener) {
context.getFilteredListeners().removeListener(listener);
}
public void sendMessage(Message message, PlugwiseMessagePriority priority) throws IOException {
if (initialized) {
messageSender.sendMessage(message, priority);
}
}
public void start() throws PlugwiseInitializationException {
try {
context.clearQueues();
context.initializeSerialPort();
messageSender.start();
messageProcessor.start();
initialized = true;
} catch (PlugwiseInitializationException e) {
initialized = false;
throw e;
}
}
public void stop() {
messageSender.stop();
messageProcessor.stop();
context.closeSerialPort();
initialized = false;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import java.time.Duration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A recurring Plugwise device task that can for instance be extended for updating a channel or setting the clock.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class PlugwiseDeviceTask {
private final Logger logger = LoggerFactory.getLogger(PlugwiseDeviceTask.class);
private final ReentrantLock lock = new ReentrantLock();
private final String name;
private final ScheduledExecutorService scheduler;
private @Nullable DeviceType deviceType;
private @Nullable Duration interval;
private @Nullable MACAddress macAddress;
private @Nullable ScheduledFuture<?> future;
private Runnable scheduledRunnable = new Runnable() {
@Override
public void run() {
try {
lock.lock();
logger.debug("Running '{}' Plugwise task for {} ({})", name, deviceType, macAddress);
runTask();
} catch (Exception e) {
logger.warn("Error while running '{}' Plugwise task for {} ({})", name, deviceType, macAddress, e);
} finally {
lock.unlock();
}
}
};
public PlugwiseDeviceTask(String name, ScheduledExecutorService scheduler) {
this.name = name;
this.scheduler = scheduler;
}
public abstract Duration getConfiguredInterval();
public @Nullable Duration getInterval() {
return interval;
}
public String getName() {
return name;
}
public boolean isScheduled() {
return future != null && !future.isCancelled();
}
public abstract void runTask();
public abstract boolean shouldBeScheduled();
public void start() {
try {
lock.lock();
if (!isScheduled()) {
Duration configuredInterval = getConfiguredInterval();
future = scheduler.scheduleWithFixedDelay(scheduledRunnable, 0, configuredInterval.getSeconds(),
TimeUnit.SECONDS);
interval = configuredInterval;
logger.debug("Scheduled '{}' Plugwise task for {} ({}) with {} seconds interval", name, deviceType,
macAddress, configuredInterval.getSeconds());
}
} finally {
lock.unlock();
}
}
public void stop() {
try {
lock.lock();
if (isScheduled()) {
ScheduledFuture<?> localFuture = future;
if (localFuture != null) {
localFuture.cancel(true);
}
future = null;
logger.debug("Stopped '{}' Plugwise task for {} ({})", name, deviceType, macAddress);
}
} finally {
lock.unlock();
}
}
public void update(DeviceType deviceType, @Nullable MACAddress macAddress) {
this.deviceType = deviceType;
this.macAddress = macAddress;
}
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A filtered message listener listens to either all messages or only those of a device that has a certain MAC address.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseFilteredMessageListener {
private final PlugwiseMessageListener listener;
private final @Nullable MACAddress macAddress;
public PlugwiseFilteredMessageListener(PlugwiseMessageListener listener) {
this(listener, null);
}
public PlugwiseFilteredMessageListener(PlugwiseMessageListener listener, @Nullable MACAddress macAddress) {
this.listener = listener;
this.macAddress = macAddress;
}
public PlugwiseMessageListener getListener() {
return listener;
}
public @Nullable MACAddress getMACAddress() {
return macAddress;
}
public boolean matches(Message message) {
MACAddress localMACAddress = macAddress;
return localMACAddress == null || localMACAddress.equals(message.getMACAddress());
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.listener.PlugwiseMessageListener;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PlugwiseFilteredMessageListenerList} keeps track of a list of {@link PlugwiseFilteredMessageListener}s and
* facilitates listener operations such as message notification.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseFilteredMessageListenerList {
private final Logger logger = LoggerFactory.getLogger(PlugwiseFilteredMessageListenerList.class);
private final List<PlugwiseFilteredMessageListener> filteredListeners = new CopyOnWriteArrayList<>();
public void addListener(PlugwiseMessageListener listener) {
if (!isExistingListener(listener)) {
filteredListeners.add(new PlugwiseFilteredMessageListener(listener));
}
}
public void addListener(PlugwiseMessageListener listener, MACAddress macAddress) {
if (!isExistingListener(listener, macAddress)) {
filteredListeners.add(new PlugwiseFilteredMessageListener(listener, macAddress));
}
}
public boolean isExistingListener(PlugwiseMessageListener listener) {
return filteredListeners.stream().anyMatch(filteredListener -> filteredListener.getListener().equals(listener));
}
public boolean isExistingListener(PlugwiseMessageListener listener, MACAddress macAddress) {
return filteredListeners.stream().anyMatch(filteredListener -> filteredListener.getListener().equals(listener)
&& macAddress.equals(filteredListener.getMACAddress()));
}
public void notifyListeners(Message message) {
for (PlugwiseFilteredMessageListener filteredListener : filteredListeners) {
if (filteredListener.matches(message)) {
try {
filteredListener.getListener().handleReponseMessage(message);
} catch (Exception e) {
logger.warn("Listener failed to handle message: {}", message, e);
}
}
}
}
public void removeListener(PlugwiseMessageListener listener) {
List<PlugwiseFilteredMessageListener> removedListeners = new ArrayList<>();
for (PlugwiseFilteredMessageListener filteredListener : filteredListeners) {
if (filteredListener.getListener().equals(listener)) {
removedListeners.add(filteredListener);
}
}
filteredListeners.removeAll(removedListeners);
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.handler.PlugwiseRelayDeviceHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseScanHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseSenseHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
import org.openhab.binding.plugwise.internal.handler.PlugwiseSwitchHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link PlugwiseHandlerFactory} is responsible for creating Plugwise things and thing handlers.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.plugwise")
public class PlugwiseHandlerFactory extends BaseThingHandlerFactory {
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegistrations = new HashMap<>();
private final SerialPortManager serialPortManager;
@Activate
public PlugwiseHandlerFactory(final @Reference SerialPortManager serialPortManager) {
this.serialPortManager = serialPortManager;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_STICK)) {
PlugwiseStickHandler handler = new PlugwiseStickHandler((Bridge) thing, serialPortManager);
registerDiscoveryService(handler);
return handler;
} else if (thingTypeUID.equals(THING_TYPE_CIRCLE) || thingTypeUID.equals(THING_TYPE_CIRCLE_PLUS)
|| thingTypeUID.equals(THING_TYPE_STEALTH)) {
return new PlugwiseRelayDeviceHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SCAN)) {
return new PlugwiseScanHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SENSE)) {
return new PlugwiseSenseHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_SWITCH)) {
return new PlugwiseSwitchHandler(thing);
}
return null;
}
private void registerDiscoveryService(PlugwiseStickHandler handler) {
PlugwiseThingDiscoveryService discoveryService = new PlugwiseThingDiscoveryService(handler);
discoveryService.activate();
this.discoveryServiceRegistrations.put(handler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
ServiceRegistration<?> registration = this.discoveryServiceRegistrations.get(thingHandler.getThing().getUID());
if (registration != null) {
PlugwiseThingDiscoveryService discoveryService = (PlugwiseThingDiscoveryService) bundleContext
.getService(registration.getReference());
discoveryService.deactivate();
registration.unregister();
discoveryServiceRegistrations.remove(thingHandler.getThing().getUID());
}
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception used during Stick initialization.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class PlugwiseInitializationException extends Exception {
private static final long serialVersionUID = 2095258016390913221L;
public PlugwiseInitializationException(String msg) {
super(msg);
}
public PlugwiseInitializationException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* When there are multiple queued messages, the message priority and date/time determine which message is sent first.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum PlugwiseMessagePriority {
/**
* Messages caused by Thing channel commands have the highest priority, e.g. to switch power on/off
*/
COMMAND,
/**
* Messages that update the state of Thing channels immediately after a command has been sent.
*/
FAST_UPDATE,
/**
* Messages for normal state updates and Thing discovery. E.g. scheduled tasks that update the state of a
* channel.
*/
UPDATE_AND_DISCOVERY
}

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import java.time.LocalDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.Message;
/**
* A queued message that is being sent or waiting to be sent to the Stick.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseQueuedMessage {
private final PlugwiseMessagePriority priority;
private final LocalDateTime dateTime = LocalDateTime.now();
private final Message message;
private int attempts;
public PlugwiseQueuedMessage(Message message, PlugwiseMessagePriority priority) {
this.message = message;
this.priority = priority;
}
public int getAttempts() {
return attempts;
}
public LocalDateTime getDateTime() {
return dateTime;
}
public Message getMessage() {
return message;
}
public PlugwiseMessagePriority getPriority() {
return priority;
}
public void increaseAttempts() {
attempts++;
}
}

View File

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

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.*;
import static org.openhab.binding.plugwise.internal.protocol.field.DeviceType.*;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
/**
* Utility class for sharing utility methods between objects.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public final class PlugwiseUtils {
private PlugwiseUtils() {
// Hidden utility class constructor
}
public static DeviceType getDeviceType(ThingTypeUID uid) {
if (uid.equals(THING_TYPE_CIRCLE)) {
return CIRCLE;
} else if (uid.equals(THING_TYPE_CIRCLE_PLUS)) {
return CIRCLE_PLUS;
} else if (uid.equals(THING_TYPE_SCAN)) {
return SCAN;
} else if (uid.equals(THING_TYPE_SENSE)) {
return SENSE;
} else if (uid.equals(THING_TYPE_STEALTH)) {
return STEALTH;
} else if (uid.equals(THING_TYPE_SWITCH)) {
return SWITCH;
} else {
return UNKNOWN;
}
}
public static @Nullable ThingTypeUID getThingTypeUID(DeviceType deviceType) {
if (deviceType == CIRCLE) {
return THING_TYPE_CIRCLE;
} else if (deviceType == CIRCLE_PLUS) {
return THING_TYPE_CIRCLE_PLUS;
} else if (deviceType == SCAN) {
return THING_TYPE_SCAN;
} else if (deviceType == SENSE) {
return THING_TYPE_SENSE;
} else if (deviceType == STEALTH) {
return THING_TYPE_STEALTH;
} else if (deviceType == SWITCH) {
return THING_TYPE_SWITCH;
} else {
return null;
}
}
public static String lowerCamelToUpperUnderscore(String text) {
return text.replaceAll("([a-z])([A-Z]+)", "$1_$2").toUpperCase();
}
public static <T extends Comparable<T>> T minComparable(T first, T second) {
return first.compareTo(second) <= 0 ? first : second;
}
public static DateTimeType newDateTimeType(LocalDateTime localDateTime) {
return new DateTimeType(localDateTime.atZone(ZoneId.systemDefault()));
}
public static void stopBackgroundThread(@Nullable Thread thread) {
if (thread != null) {
thread.interrupt();
try {
thread.join();
} catch (InterruptedException e) {
Thread.interrupted();
}
}
}
public static String upperUnderscoreToLowerCamel(String text) {
String upperCamel = StringUtils.remove(WordUtils.capitalizeFully(text, new char[] { '_' }), "_");
return upperCamel.substring(0, 1).toLowerCase() + upperCamel.substring(1);
}
@SuppressWarnings("null")
public static boolean updateProperties(Map<String, String> properties, InformationResponseMessage message) {
boolean update = false;
// Update firmware version property
String oldFirmware = properties.get(Thing.PROPERTY_FIRMWARE_VERSION);
String newFirmware = DateTimeFormatter.ISO_LOCAL_DATE.format(message.getFirmwareVersion());
if (oldFirmware == null || !oldFirmware.equals(newFirmware)) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, newFirmware);
update = true;
}
// Update hardware version property
String oldHardware = properties.get(Thing.PROPERTY_HARDWARE_VERSION);
String newHardware = message.getHardwareVersion();
if (oldHardware == null || !oldHardware.equals(newHardware)) {
properties.put(Thing.PROPERTY_HARDWARE_VERSION, newHardware);
update = true;
}
// Update hertz property for devices with a relay
if (message.getDeviceType().isRelayDevice()) {
String oldHertz = properties.get(PlugwiseBindingConstants.PROPERTY_HERTZ);
String newHertz = Integer.toString(message.getHertz());
if (oldHertz == null || !oldHertz.equals(newHertz)) {
properties.put(PlugwiseBindingConstants.PROPERTY_HERTZ, newHertz);
update = true;
}
}
// Update MAC address property
String oldMACAddress = properties.get(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS);
String newMACAddress = message.getMACAddress().toString();
if (oldMACAddress == null || !oldMACAddress.equals(newMACAddress)) {
properties.put(PlugwiseBindingConstants.PROPERTY_MAC_ADDRESS, newMACAddress);
update = true;
}
return update;
}
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.config.PlugwiseRelayConfig.PowerStateChanging.COMMAND_SWITCHING;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* The {@link PlugwiseRelayConfig} class represents the configuration for a Plugwise relay device (Circle, Circle+,
* Stealth).
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseRelayConfig {
public enum PowerStateChanging {
COMMAND_SWITCHING,
ALWAYS_ON,
ALWAYS_OFF
}
private String macAddress = "";
private String powerStateChanging = upperUnderscoreToLowerCamel(COMMAND_SWITCHING.name());
private boolean suppliesPower = false;
private int measurementInterval = 60; // minutes
private boolean temporarilyNotInNetwork = false;
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public PowerStateChanging getPowerStateChanging() {
return PowerStateChanging.valueOf(lowerCamelToUpperUnderscore(powerStateChanging));
}
public boolean isSuppliesPower() {
return suppliesPower;
}
public Duration getMeasurementInterval() {
return Duration.ofMinutes(measurementInterval);
}
public boolean isTemporarilyNotInNetwork() {
return temporarilyNotInNetwork;
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
@Override
public String toString() {
return "PlugwiseRelayConfig [macAddress=" + macAddress + ", powerStateChanging=" + powerStateChanging
+ ", suppliesPower=" + suppliesPower + ", measurementInterval=" + measurementInterval
+ ", temporarilyNotInNetwork=" + temporarilyNotInNetwork + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.protocol.field.Sensitivity.MEDIUM;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Sensitivity;
/**
* The {@link PlugwiseScanConfig} class represents the configuration for a Plugwise Scan.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseScanConfig {
private String macAddress = "";
private String sensitivity = upperUnderscoreToLowerCamel(MEDIUM.name());
private int switchOffDelay = 5; // minutes
private boolean daylightOverride = false;
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean recalibrate = false;
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Sensitivity getSensitivity() {
return Sensitivity.valueOf(lowerCamelToUpperUnderscore(sensitivity));
}
public Duration getSwitchOffDelay() {
return Duration.ofMinutes(switchOffDelay);
}
public boolean isDaylightOverride() {
return daylightOverride;
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isRecalibrate() {
return recalibrate;
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalScanParameters(PlugwiseScanConfig other) {
return this.sensitivity.equals(other.sensitivity) && this.switchOffDelay == other.switchOffDelay
&& this.daylightOverride == other.daylightOverride;
}
public boolean equalSleepParameters(PlugwiseScanConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseScanConfig [macAddress=" + macAddress + ", sensitivity=" + sensitivity + ", switchOffDelay="
+ switchOffDelay + ", daylightOverride=" + daylightOverride + ", wakeupInterval=" + wakeupInterval
+ ", wakeupDuration=" + wakeupDuration + ", recalibrate=" + recalibrate + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.config;
import static org.openhab.binding.plugwise.internal.PlugwiseUtils.*;
import static org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction.OFF_BELOW_ON_ABOVE;
import static org.openhab.binding.plugwise.internal.protocol.field.BoundaryType.NONE;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryType;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* The {@link PlugwiseScanConfig} class represents the configuration for a Plugwise Sense.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSenseConfig {
private String macAddress = "";
private int measurementInterval = 15; // minutes
private String boundaryType = upperUnderscoreToLowerCamel(NONE.name());
private String boundaryAction = upperUnderscoreToLowerCamel(OFF_BELOW_ON_ABOVE.name());
private int temperatureBoundaryMin = 15; // degrees Celsius
private int temperatureBoundaryMax = 25; // degrees Celsius
private int humidityBoundaryMin = 45; // relative humidity (RH)
private int humidityBoundaryMax = 65; // relative humidity (RH)
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Duration getMeasurementInterval() {
return Duration.ofMinutes(measurementInterval);
}
public BoundaryType getBoundaryType() {
return BoundaryType.valueOf(lowerCamelToUpperUnderscore(boundaryType));
}
public BoundaryAction getBoundaryAction() {
return BoundaryAction.valueOf(lowerCamelToUpperUnderscore(boundaryAction));
}
public Temperature getTemperatureBoundaryMin() {
return new Temperature(temperatureBoundaryMin);
}
public Temperature getTemperatureBoundaryMax() {
return new Temperature(temperatureBoundaryMax);
}
public Humidity getHumidityBoundaryMin() {
return new Humidity(humidityBoundaryMin);
}
public Humidity getHumidityBoundaryMax() {
return new Humidity(humidityBoundaryMax);
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalBoundaryParameters(PlugwiseSenseConfig other) {
return boundaryType.equals(other.boundaryType) && boundaryAction.equals(other.boundaryAction)
&& temperatureBoundaryMin == other.temperatureBoundaryMin
&& temperatureBoundaryMax == other.temperatureBoundaryMax
&& humidityBoundaryMin == other.humidityBoundaryMin && humidityBoundaryMax == other.humidityBoundaryMax;
}
public boolean equalSleepParameters(PlugwiseSenseConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseSenseConfig [macAddress=" + macAddress + ", measurementInterval=" + measurementInterval
+ ", boundaryType=" + boundaryType + ", boundaryAction=" + boundaryAction + ", temperatureBoundaryMin="
+ temperatureBoundaryMin + ", temperatureBoundaryMax=" + temperatureBoundaryMax
+ ", humidityBoundaryMin=" + humidityBoundaryMin + ", humidityBoundaryMax=" + humidityBoundaryMax
+ ", wakeupInterval=" + wakeupInterval + ", wakeupDuration=" + wakeupDuration + ", updateConfiguration="
+ updateConfiguration + "]";
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PlugwiseStickConfig} class represents the configuration for a Plugwise Stick.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseStickConfig {
private String serialPort = "";
private int messageWaitTime = 150; // milliseconds
public String getSerialPort() {
return serialPort;
}
public int getMessageWaitTime() {
return messageWaitTime;
}
public void setSerialPort(String serialPort) {
this.serialPort = serialPort;
}
public void setMessageWaitTime(int messageWaitTime) {
this.messageWaitTime = messageWaitTime;
}
@Override
public String toString() {
return "PlugwiseStickConfig [serialPort=" + serialPort + ", messageWaitTime=" + messageWaitTime + "]";
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.config;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* The {@link PlugwiseSwitchConfig} class represents the configuration for a Plugwise Switch.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PlugwiseSwitchConfig {
private String macAddress = "";
private int wakeupInterval = 1440; // minutes (1 day)
private int wakeupDuration = 10; // seconds
private boolean updateConfiguration = true;
public MACAddress getMACAddress() {
return new MACAddress(macAddress);
}
public Duration getWakeupInterval() {
return Duration.ofMinutes(wakeupInterval);
}
public Duration getWakeupDuration() {
return Duration.ofSeconds(wakeupDuration);
}
public boolean isUpdateConfiguration() {
return updateConfiguration;
}
public boolean equalSleepParameters(PlugwiseSwitchConfig other) {
return this.wakeupInterval == other.wakeupInterval && this.wakeupDuration == other.wakeupDuration;
}
@Override
public String toString() {
return "PlugwiseSwitchConfig [macAddress=" + macAddress + ", wakeupInterval=" + wakeupInterval
+ ", wakeupDuration=" + wakeupDuration + ", updateConfiguration=" + updateConfiguration + "]";
}
}

View File

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

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.handler;
import static org.openhab.binding.plugwise.internal.PlugwiseBindingConstants.CHANNEL_TRIGGERED;
import static org.openhab.core.thing.ThingStatus.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage;
import org.openhab.binding.plugwise.internal.protocol.AnnounceAwakeRequestMessage.AwakeReason;
import org.openhab.binding.plugwise.internal.protocol.BroadcastGroupSwitchResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.InformationResponseMessage;
import org.openhab.binding.plugwise.internal.protocol.Message;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Thing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractPlugwiseThingHandler} handles common Plugwise sleeping end device (SED) channel updates and
* commands.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractSleepingEndDeviceHandler extends AbstractPlugwiseThingHandler {
private static final int SED_PROPERTIES_COUNT = 3;
private final Logger logger = LoggerFactory.getLogger(AbstractSleepingEndDeviceHandler.class);
public AbstractSleepingEndDeviceHandler(Thing thing) {
super(thing);
}
protected abstract Duration getWakeupDuration();
protected void handleAcknowledgement(AcknowledgementMessage message) {
updateStatusOnDetailChange();
}
protected void handleAnnounceAwakeRequest(AnnounceAwakeRequestMessage message) {
AwakeReason awakeReason = message.getAwakeReason();
if (awakeReason == AwakeReason.MAINTENANCE || awakeReason == AwakeReason.WAKEUP_BUTTON
|| editProperties().size() < SED_PROPERTIES_COUNT) {
updateInformation();
if (isConfigurationPending() && !recentlySendConfigurationUpdate()) {
sendConfigurationUpdateCommands();
}
}
}
protected void handleBroadcastGroupSwitchResponseMessage(BroadcastGroupSwitchResponseMessage message) {
updateState(CHANNEL_TRIGGERED, message.getPowerState() ? OnOffType.ON : OnOffType.OFF);
}
protected void handleInformationResponse(InformationResponseMessage message) {
updateProperties(message);
}
@Override
public void handleReponseMessage(Message message) {
updateLastSeen();
switch (message.getType()) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
handleAcknowledgement((AcknowledgementMessage) message);
break;
case ANNOUNCE_AWAKE_REQUEST:
handleAnnounceAwakeRequest((AnnounceAwakeRequestMessage) message);
break;
case BROADCAST_GROUP_SWITCH_RESPONSE:
handleBroadcastGroupSwitchResponseMessage((BroadcastGroupSwitchResponseMessage) message);
break;
case DEVICE_INFORMATION_RESPONSE:
handleInformationResponse((InformationResponseMessage) message);
break;
default:
logger.trace("Received unhandled {} message from {} ({})", message.getType(), getDeviceType(),
getMACAddress());
break;
}
}
@Override
protected boolean shouldOnlineTaskBeScheduled() {
return thing.getStatus() == ONLINE;
}
@Override
protected void updateOnlineState() {
if (thing.getStatus() == ONLINE && getWakeupDuration().minus(durationSinceLastSeen()).isNegative()) {
updateStatus(OFFLINE, getThingStatusDetail());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.Message;
/**
* Interface for listeners of Plugwise response messages.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface PlugwiseMessageListener {
void handleReponseMessage(Message message);
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.listener;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.handler.PlugwiseStickHandler;
import org.openhab.core.thing.ThingStatus;
/**
* Interface for listeners of {@link PlugwiseStickHandler} thing status changes.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public interface PlugwiseStickStatusListener {
public void stickStatusChanged(ThingStatus status);
}

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.*;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* Acknowledgement message class - ACKs are used in the Plugwise protocol to serve different means, from acknowledging a
* message sent to the Stick by the host, as well as confirmation messages from nodes in the network for various
* purposes. Not all purposes are yet reverse-engineered.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class AcknowledgementMessage extends Message {
public enum ExtensionCode {
NOT_EXTENDED(0),
SENSE_INTERVAL_SET_ACK(179),
SENSE_INTERVAL_SET_NACK(180),
SENSE_BOUNDARIES_SET_ACK(181),
SENSE_BOUNDARIES_SET_NACK(182),
LIGHT_CALIBRATION_ACK(189),
SCAN_PARAMETERS_SET_ACK(190),
SCAN_PARAMETERS_SET_NACK(191),
SUCCESS(193),
ERROR(194),
CIRCLE_PLUS(221),
CLOCK_SET_ACK(215),
ON_ACK(216),
POWER_CALIBRATION_ACK(218),
OFF_ACK(222),
REAL_TIME_CLOCK_SET_ACK(223),
TIMEOUT(225),
ON_OFF_NACK(226),
REAL_TIME_CLOCK_SET_NACK(231),
SLEEP_SET_ACK(246),
POWER_LOG_INTERVAL_SET_ACK(248),
UNKNOWN(999);
private static final Map<Integer, ExtensionCode> TYPES_BY_VALUE = new HashMap<>();
static {
for (ExtensionCode type : ExtensionCode.values()) {
TYPES_BY_VALUE.put(type.identifier, type);
}
}
public static ExtensionCode forValue(int value) {
return TYPES_BY_VALUE.get(value);
}
private int identifier;
private ExtensionCode(int value) {
identifier = value;
}
public int toInt() {
return identifier;
}
}
private static final Pattern V1_SHORT_PAYLOAD_PATTERN = Pattern.compile("(\\w{4})");
private static final Pattern V1_EXTENDED_PAYLOAD_PATTERN = Pattern.compile("(\\w{4})(\\w{16})");
private static final Pattern V2_EXTENDED_PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})");
private ExtensionCode code;
public AcknowledgementMessage(MessageType messageType, int sequenceNumber, String payload) {
super(messageType, sequenceNumber, payload);
}
public ExtensionCode getExtensionCode() {
if (isExtended()) {
return code;
} else {
return ExtensionCode.NOT_EXTENDED;
}
}
@Override
public String getPayload() {
return payloadToHexString();
}
public boolean isError() {
return code == ExtensionCode.ERROR;
}
public boolean isExtended() {
return code != ExtensionCode.NOT_EXTENDED && code != ExtensionCode.SUCCESS && code != ExtensionCode.ERROR;
}
public boolean isSuccess() {
return code == ExtensionCode.SUCCESS;
}
public boolean isTimeOut() {
return code == ExtensionCode.TIMEOUT;
}
@Override
protected void parsePayload() {
if (getType() == ACKNOWLEDGEMENT_V1) {
parseV1Payload();
} else if (getType() == ACKNOWLEDGEMENT_V2) {
parseV2Payload();
}
}
private void parseV1Payload() {
Matcher shortMatcher = V1_SHORT_PAYLOAD_PATTERN.matcher(payload);
Matcher extendedMatcher = V1_EXTENDED_PAYLOAD_PATTERN.matcher(payload);
if (extendedMatcher.matches()) {
code = ExtensionCode.forValue(Integer.parseInt(extendedMatcher.group(1), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
macAddress = new MACAddress(extendedMatcher.group(2));
} else if (shortMatcher.matches()) {
code = ExtensionCode.forValue(Integer.parseInt(shortMatcher.group(1), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
} else {
code = ExtensionCode.UNKNOWN;
throw new PlugwisePayloadMismatchException(ACKNOWLEDGEMENT_V1, V1_SHORT_PAYLOAD_PATTERN,
V1_EXTENDED_PAYLOAD_PATTERN, payload);
}
}
private void parseV2Payload() {
Matcher matcher = V2_EXTENDED_PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
code = ExtensionCode.forValue(Integer.parseInt(matcher.group(2), 16));
if (code == null) {
code = ExtensionCode.UNKNOWN;
}
} else {
code = ExtensionCode.UNKNOWN;
throw new PlugwisePayloadMismatchException(ACKNOWLEDGEMENT_V2, V2_EXTENDED_PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.ANNOUNCE_AWAKE_REQUEST;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A sleeping end device (SED: Scan, Sense, Switch) sends this message to announce that is awake.
*
* @author Wouter Born - Initial contribution
*/
public class AnnounceAwakeRequestMessage extends Message {
public enum AwakeReason {
/** The SED joins the network for maintenance */
MAINTENANCE(0),
/** The SED joins a network for the first time */
JOIN_NETWORK(1),
/** The SED joins a network it has already joined, e.g. after reinserting a battery */
REJOIN_NETWORK(2),
/** When a SED switches a device group or when reporting values such as temperature/humidity */
NORMAL(3),
/** A human pressed the button on a SED to wake it up */
WAKEUP_BUTTON(5);
public static AwakeReason forValue(int value) {
return Arrays.stream(values()).filter(awakeReason -> awakeReason.id == value).findFirst().get();
}
private final int id;
AwakeReason(int id) {
this.id = id;
}
}
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})");
private AwakeReason awakeReason;
public AnnounceAwakeRequestMessage(int sequenceNumber, String payload) {
super(ANNOUNCE_AWAKE_REQUEST, sequenceNumber, payload);
}
public AwakeReason getAwakeReason() {
return awakeReason;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
awakeReason = AwakeReason.forValue(Integer.parseInt(matcher.group(2)));
} else {
throw new PlugwisePayloadMismatchException(ANNOUNCE_AWAKE_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.BROADCAST_GROUP_SWITCH_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* A sleeping end device (SED: Scan, Sense, Switch) sends this message to switch groups on/off when the configured
* switching conditions have been met.
*
* @author Wouter Born - Initial contribution
*/
public class BroadcastGroupSwitchResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})(\\w{2})");
private int portMask;
private boolean powerState;
public BroadcastGroupSwitchResponseMessage(int sequenceNumber, String payload) {
super(BROADCAST_GROUP_SWITCH_RESPONSE, sequenceNumber, payload);
}
public int getPortMask() {
return portMask;
}
public boolean getPowerState() {
return powerState;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
portMask = Integer.parseInt(matcher.group(2));
powerState = (matcher.group(3).equals("01"));
} else {
throw new PlugwisePayloadMismatchException(BROADCAST_GROUP_SWITCH_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_GET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the current clock value of a device. This message is answered by a {@link ClockGetResponseMessage} which
* contains the clock value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockGetRequestMessage extends Message {
public ClockGetRequestMessage(MACAddress macAddress) {
super(CLOCK_GET_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_GET_RESPONSE;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the current clock value of a device. This message is the response of a {@link ClockGetRequestMessage}. Not
* all response fields have been reverse engineered.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockGetResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})");
private int hour;
private int minutes;
private int seconds;
private int weekday;
public ClockGetResponseMessage(int sequenceNumber, String payload) {
super(CLOCK_GET_RESPONSE, sequenceNumber, payload);
}
public int getHour() {
return hour;
}
public int getMinutes() {
return minutes;
}
public int getSeconds() {
return seconds;
}
public int getWeekday() {
return weekday;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
hour = Integer.parseInt(matcher.group(2), 16);
minutes = Integer.parseInt(matcher.group(3), 16);
seconds = Integer.parseInt(matcher.group(4), 16);
weekday = Integer.parseInt(matcher.group(5), 16);
} else {
throw new PlugwisePayloadMismatchException(CLOCK_GET_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
public String getTime() {
ZonedDateTime utcDateTime = ZonedDateTime.now(UTC).withHour(hour).withMinute(minutes).withSecond(seconds)
.withNano(0);
ZonedDateTime localDateTime = utcDateTime.withZoneSameInstant(ZoneId.systemDefault());
return DateTimeFormatter.ISO_LOCAL_TIME.format(localDateTime);
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.CLOCK_SET_REQUEST;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the clock of the Circle+. Based on what is known about the Plugwise protocol, only the clock of the Circle+ has
* to be set. The Circle+ sets the clock of all other network nodes.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class ClockSetRequestMessage extends Message {
private ZonedDateTime utcDateTime;
public ClockSetRequestMessage(MACAddress macAddress, LocalDateTime localDateTime) {
super(CLOCK_SET_REQUEST, macAddress);
// Nodes expect clock info to be in the UTC timezone
this.utcDateTime = localDateTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(UTC);
}
@Override
protected String payloadToHexString() {
String year = String.format("%02X", utcDateTime.getYear() - 2000);
String month = String.format("%02X", utcDateTime.getMonthValue());
String minutes = String.format("%04X",
(utcDateTime.getDayOfMonth() - 1) * 24 * 60 + (utcDateTime.getHour() * 60) + utcDateTime.getMinute());
// If we set logaddress to FFFFFFFFF then previous buffered data will be kept by the Circle+
String logaddress = "FFFFFFFF";
String hour = String.format("%02X", utcDateTime.getHour());
String minute = String.format("%02X", utcDateTime.getMinute());
String second = String.format("%02X", utcDateTime.getSecond());
// Monday = 0, ... , Sunday = 6
String dayOfWeek = String.format("%02X", utcDateTime.getDayOfWeek().getValue() - 1);
return year + month + minutes + logaddress + hour + minute + second + dayOfWeek;
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_INFORMATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests generic device information. This message is answered by an {@link InformationResponseMessage} which contains
* the device information.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class InformationRequestMessage extends Message {
public InformationRequestMessage(MACAddress macAddress) {
super(DEVICE_INFORMATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,126 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_INFORMATION_RESPONSE;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.DeviceType;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains generic device information. This message is the response of an {@link InformationRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class InformationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{4})(\\w{8})(\\w{2})(\\w{2})(\\w{12})(\\w{8})(\\w{2})");
private int year;
private int month;
private int minutes;
private int logAddress;
private boolean powerState;
private int hertz;
private String hardwareVersion;
private LocalDateTime firmwareVersion;
private DeviceType deviceType;
public InformationResponseMessage(int sequenceNumber, String payload) {
super(DEVICE_INFORMATION_RESPONSE, sequenceNumber, payload);
}
public DeviceType getDeviceType() {
return deviceType;
}
public LocalDateTime getFirmwareVersion() {
return firmwareVersion;
}
public String getHardwareVersion() {
return hardwareVersion;
}
public int getHertz() {
return (hertz == 133) ? 50 : 60;
}
public int getLogAddress() {
return logAddress;
}
public int getMinutes() {
return minutes;
}
public int getMonth() {
return month;
}
public boolean getPowerState() {
return powerState;
}
public int getYear() {
return year;
}
private DeviceType intToDeviceType(int i) {
switch (i) {
case 0:
return DeviceType.STICK;
case 1:
return DeviceType.CIRCLE_PLUS;
case 2:
return DeviceType.CIRCLE;
case 3:
return DeviceType.SWITCH;
case 5:
return DeviceType.SENSE;
case 6:
return DeviceType.SCAN;
case 9:
return DeviceType.STEALTH;
default:
return null;
}
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
year = Integer.parseInt(matcher.group(2), 16) + 2000;
month = Integer.parseInt(matcher.group(3), 16);
minutes = Integer.parseInt(matcher.group(4), 16);
logAddress = (Integer.parseInt(matcher.group(5), 16) - 278528) / 32;
powerState = (matcher.group(6).equals("01"));
hertz = Integer.parseInt(matcher.group(7), 16);
hardwareVersion = matcher.group(8).substring(0, 4) + "-" + matcher.group(8).substring(4, 8) + "-"
+ matcher.group(8).substring(8, 12);
firmwareVersion = LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(matcher.group(9), 16)), UTC);
deviceType = intToDeviceType(Integer.parseInt(matcher.group(10), 16));
} else {
throw new PlugwisePayloadMismatchException(DEVICE_INFORMATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.LIGHT_CALIBRATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Calibrates the daylight override boundary of a Scan. The best time to do this is at night when lights are on.
*
* @author Wouter Born - Initial contribution
*/
public class LightCalibrationRequestMessage extends Message {
public LightCalibrationRequestMessage(MACAddress macAddress) {
super(LIGHT_CALIBRATION_REQUEST, macAddress);
}
}

View File

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

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* Creates instances of messages received from the Plugwise network.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class MessageFactory {
public Message createMessage(MessageType messageType, int sequenceNumber, String payload)
throws IllegalArgumentException {
switch (messageType) {
case ACKNOWLEDGEMENT_V1:
case ACKNOWLEDGEMENT_V2:
return new AcknowledgementMessage(messageType, sequenceNumber, payload);
case ANNOUNCE_AWAKE_REQUEST:
return new AnnounceAwakeRequestMessage(sequenceNumber, payload);
case BROADCAST_GROUP_SWITCH_RESPONSE:
return new BroadcastGroupSwitchResponseMessage(sequenceNumber, payload);
case CLOCK_GET_RESPONSE:
return new ClockGetResponseMessage(sequenceNumber, payload);
case DEVICE_INFORMATION_RESPONSE:
return new InformationResponseMessage(sequenceNumber, payload);
case DEVICE_ROLE_CALL_RESPONSE:
return new RoleCallResponseMessage(sequenceNumber, payload);
case MODULE_JOINED_NETWORK_REQUEST:
return new ModuleJoinedNetworkRequestMessage(sequenceNumber, payload);
case NETWORK_STATUS_RESPONSE:
return new NetworkStatusResponseMessage(sequenceNumber, payload);
case NODE_AVAILABLE:
return new NodeAvailableMessage(sequenceNumber, payload);
case PING_RESPONSE:
return new PingResponseMessage(sequenceNumber, payload);
case POWER_BUFFER_RESPONSE:
return new PowerBufferResponseMessage(sequenceNumber, payload);
case POWER_CALIBRATION_RESPONSE:
return new PowerCalibrationResponseMessage(sequenceNumber, payload);
case POWER_INFORMATION_RESPONSE:
return new PowerInformationResponseMessage(sequenceNumber, payload);
case REAL_TIME_CLOCK_GET_RESPONSE:
return new RealTimeClockGetResponseMessage(sequenceNumber, payload);
case SENSE_REPORT_REQUEST:
return new SenseReportRequestMessage(sequenceNumber, payload);
default:
throw new IllegalArgumentException("Unsupported message type: " + messageType);
}
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.MODULE_JOINED_NETWORK_REQUEST;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Module joined network request. Sent when a SED (re)joins the network. E.g. when you reinsert the battery of a Scan.
*
* @author Wouter Born - Initial contribution
*/
public class ModuleJoinedNetworkRequestMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})");
public ModuleJoinedNetworkRequestMessage(int sequenceNumber, String payload) {
super(MODULE_JOINED_NETWORK_REQUEST, sequenceNumber, payload);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
} else {
throw new PlugwisePayloadMismatchException(MODULE_JOINED_NETWORK_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_RESET_REQUEST;
/**
* Requests the Plugwise network to be reset. Currently not used in the binding.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkResetRequestMessage extends Message {
public NetworkResetRequestMessage(String payload) {
super(NETWORK_RESET_REQUEST, payload);
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_STATUS_REQUEST;
/**
* Requests the network status from the Stick. This message is answered by a {@link NetworkStatusResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkStatusRequestMessage extends Message {
public NetworkStatusRequestMessage() {
super(NETWORK_STATUS_REQUEST);
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NETWORK_STATUS_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the current network status as well as the MAC address of the Circle+ that coordinates the network. The Stick
* sends this message as response of a {@link NetworkStatusRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NetworkStatusResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{16})(\\w{4})(\\w{2})");
private boolean online;
private String networkID;
private String unknown1;
private String unknown2;
private String shortNetworkID;
private MACAddress circlePlusMAC;
public NetworkStatusResponseMessage(int sequenceNumber, String payload) {
super(NETWORK_STATUS_RESPONSE, sequenceNumber, payload);
}
public NetworkStatusResponseMessage(String payload) {
super(NETWORK_STATUS_RESPONSE, payload);
}
public MACAddress getCirclePlusMAC() {
return circlePlusMAC;
}
public String getNetworkID() {
return networkID;
}
public String getShortNetworkID() {
return shortNetworkID;
}
public String getUnknown1() {
return unknown1;
}
public String getUnknown2() {
return unknown2;
}
public boolean isOnline() {
return online;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
unknown1 = matcher.group(2);
online = (Integer.parseInt(matcher.group(3), 16) == 1);
networkID = matcher.group(4);
shortNetworkID = matcher.group(5);
unknown2 = matcher.group(6);
// now some serious protocol reverse-engineering assumption. Circle+ MAC = networkID with first two bytes
// replaced by 00
circlePlusMAC = new MACAddress("00" + networkID.substring(2));
} else {
throw new PlugwisePayloadMismatchException(NETWORK_STATUS_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
@Override
protected String payloadToHexString() {
return unknown1 + String.format("%02X", online ? 1 : 0) + networkID + shortNetworkID + unknown2;
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NODE_AVAILABLE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Node available messages are broadcasted by nodes that are not yet part of a network. They are currently unused
* because typically the network is configured using the Plugwise Source software, and never changed after.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NodeAvailableMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})");
public NodeAvailableMessage(int sequenceNumber, String payload) {
super(NODE_AVAILABLE, sequenceNumber, payload);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
} else {
throw new PlugwisePayloadMismatchException(NODE_AVAILABLE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.NODE_AVAILABLE_RESPONSE;
/**
* Response to a device when its {@link NodeAvailableMessage} is "accepted".
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class NodeAvailableResponseMessage extends Message {
private boolean acceptanceCode;
private String destinationMAC;
public NodeAvailableResponseMessage(boolean code, String destination) {
super(NODE_AVAILABLE_RESPONSE);
acceptanceCode = code;
destinationMAC = destination;
}
public boolean isAcceptanceCode() {
return acceptanceCode;
}
@Override
protected String payloadToHexString() {
return String.format("%02X", acceptanceCode ? 1 : 0) + destinationMAC;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.PING_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests a {@link PingResponseMessage} from a device.
*
* @author Wouter Born - Initial contribution
*/
public class PingRequestMessage extends Message {
public PingRequestMessage(MACAddress macAddress) {
super(PING_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.PING_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains network diagnostic information. This message is the response of a {@link PingRequestMessage}.
*
* @author Wouter Born - Initial contribution
*/
public class PingResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{4})");
private int inRSSI;
private int outRSSI;
private int pingMillis;
public PingResponseMessage(int sequenceNumber, String payload) {
super(PING_RESPONSE, sequenceNumber, payload);
}
public int getInRSSI() {
return inRSSI;
}
public int getOutRSSI() {
return outRSSI;
}
public int getPingMillis() {
return pingMillis;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
inRSSI = (Integer.parseInt(matcher.group(2), 16));
outRSSI = (Integer.parseInt(matcher.group(3), 16));
pingMillis = (Integer.parseInt(matcher.group(4), 16));
} else {
throw new PlugwisePayloadMismatchException(PING_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
/**
* The payload of a message does not match the expected message pattern. Thrown whenever the payload of a received
* message could not be parsed.
*
* @author Wouter Born - Initial contribution
*/
public class PlugwisePayloadMismatchException extends RuntimeException {
private static final long serialVersionUID = 1160553788698072410L;
public PlugwisePayloadMismatchException(MessageType messageType, Pattern pattern0, Pattern pattern1,
String payload) {
super(String.format("Plugwise %s payload mismatch: %s does not match %s or %s", messageType.name(), payload,
pattern0.pattern(), pattern1.pattern()));
}
public PlugwisePayloadMismatchException(MessageType messageType, Pattern pattern, String payload) {
super(String.format("Plugwise %s payload mismatch: %s does not match %s", messageType.name(), payload,
pattern.pattern()));
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_BUFFER_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the historical pulse measurements at a certain log address from a device (Circle, Circle+, Stealth). This
* message is answered by a {@link PowerBufferResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerBufferRequestMessage extends Message {
private int logAddress;
public PowerBufferRequestMessage(MACAddress macAddress, int logAddress) {
super(POWER_BUFFER_REQUEST, macAddress);
this.logAddress = logAddress;
}
@Override
protected String payloadToHexString() {
return String.format("%08X", (logAddress * 32 + 278528));
}
}

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_BUFFER_RESPONSE;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Contains the historical pulse measurements at a certain log address from a device (Circle, Circle+, Stealth). This
* message is the response of a {@link PowerBufferRequestMessage}. The consumed/produced {@link Energy} (kWh) of the
* datapoints can be calculated using {@link PowerCalibration} data.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerBufferResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})(\\w{8})");
private static final String EMPTY_TIMESTAMP = "FFFFFFFF";
private Energy[] datapoints;
private int logAddress;
public PowerBufferResponseMessage(int sequenceNumber, String payload) {
super(POWER_BUFFER_RESPONSE, sequenceNumber, payload);
}
public Energy[] getDatapoints() {
return datapoints;
}
public int getLogAddress() {
return logAddress;
}
public Energy getMostRecentDatapoint() {
Energy result = null;
for (Energy datapoint : datapoints) {
if (datapoint != null) {
result = datapoint;
}
}
return result;
}
private Energy parseEnergy(String timeHex, String pulsesHex) {
ZonedDateTime utcDateTime = !timeHex.equals(EMPTY_TIMESTAMP) ? parseDateTime(timeHex) : null;
if (utcDateTime == null) {
return null;
}
long pulses = Long.parseLong(pulsesHex, 16);
return new Energy(utcDateTime, pulses);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
datapoints = new Energy[4];
datapoints[0] = parseEnergy(matcher.group(2), matcher.group(3));
datapoints[1] = parseEnergy(matcher.group(4), matcher.group(5));
datapoints[2] = parseEnergy(matcher.group(6), matcher.group(7));
datapoints[3] = parseEnergy(matcher.group(8), matcher.group(9));
logAddress = (Integer.parseInt(matcher.group(10), 16) - 278528) / 32;
} else {
throw new PlugwisePayloadMismatchException(POWER_BUFFER_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
private ZonedDateTime parseDateTime(String timeHex) {
int year = Integer.parseInt(timeHex.substring(0, 2), 16) + 2000;
int month = Integer.parseInt(timeHex.substring(2, 4), 16);
int minutes = Integer.parseInt(timeHex.substring(4, 8), 16);
return ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, UTC).plusMinutes(minutes);
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CALIBRATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Calibrates the power of a relay device (Circle, Circle+, Stealth). This message is answered by a
* {@link PowerCalibrationResponseMessage} which contains the {@link PowerCalibration} data.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerCalibrationRequestMessage extends Message {
public PowerCalibrationRequestMessage(MACAddress macAddress) {
super(POWER_CALIBRATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CALIBRATION_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.PowerCalibration;
/**
* Contains the power calibration data of a relay device (Circle, Circle+, Stealth). This message is the response of a
* {@link PowerCalibrationRequestMessage}. The {@link PowerCalibration} data is used to calculate power (W) and energy
* (kWh) from pulses with the {@link Energy} class.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerCalibrationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{8})(\\w{8})(\\w{8})(\\w{8})");
private double gainA;
private double gainB;
private double offsetTotal;
private double offsetNoise;
public PowerCalibrationResponseMessage(int sequenceNumber, String payload) {
super(POWER_CALIBRATION_RESPONSE, sequenceNumber, payload);
}
public double getGainA() {
return gainA;
}
public double getGainB() {
return gainB;
}
public double getOffsetNoise() {
return offsetNoise;
}
public double getOffsetTotal() {
return offsetTotal;
}
public PowerCalibration getCalibration() {
return new PowerCalibration(gainA, gainB, offsetNoise, offsetTotal);
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
gainA = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(2), 16)));
gainB = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(3), 16)));
offsetTotal = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(4), 16)));
offsetNoise = Float.intBitsToFloat((int) (Long.parseLong(matcher.group(5), 16)));
} else {
throw new PlugwisePayloadMismatchException(POWER_CALIBRATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_CHANGE_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the power state of a relay device (Circle, Circle+, Stealth) to be switched on/off. The current power state
* of a device is retrieved by sending a {@link InformationRequestMessage} and reading the
* {@link InformationResponseMessage#getPowerState()} value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerChangeRequestMessage extends Message {
public PowerChangeRequestMessage(MACAddress macAddress, boolean powerState) {
super(POWER_CHANGE_REQUEST, macAddress, powerState ? "01" : "00");
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_INFORMATION_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Request real-time energy consumption from a relay device (Circle, Circle+, Stealth). This
* message is answered by a {@link PowerInformationResponseMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerInformationRequestMessage extends Message {
public PowerInformationRequestMessage(MACAddress macAddress) {
super(POWER_INFORMATION_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_INFORMATION_RESPONSE;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Energy;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the real-time energy consumption of a relay device (Circle, Circle+, Stealth). This
* message is the response of a {@link PowerInformationRequestMessage}.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class PowerInformationResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})(\\w{4})(\\w{8})(\\w{8})(\\w{4})");
private static final double NANOSECONDS_CORRECTION_DIVISOR = 0.000046875; // 46875 divided by nanos per second
private Energy oneSecond;
private Energy eightSecond;
private Energy oneHourConsumed;
private Energy oneHourProduced;
private long nanosCorrection;
public PowerInformationResponseMessage(int sequenceNumber, String payload) {
super(POWER_INFORMATION_RESPONSE, sequenceNumber, payload);
}
public Energy getEightSecond() {
return eightSecond;
}
public Energy getOneHourConsumed() {
return oneHourConsumed;
}
public Energy getOneHourProduced() {
return oneHourProduced;
}
public Energy getOneSecond() {
return oneSecond;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
ZonedDateTime utcNow = ZonedDateTime.now(UTC);
macAddress = new MACAddress(matcher.group(1));
nanosCorrection = Math.round(Integer.parseInt(matcher.group(6), 16) / NANOSECONDS_CORRECTION_DIVISOR);
oneSecond = new Energy(utcNow, Integer.parseInt(matcher.group(2), 16),
Duration.ofSeconds(1, nanosCorrection));
eightSecond = new Energy(utcNow, Integer.parseInt(matcher.group(3), 16),
Duration.ofSeconds(8, nanosCorrection));
oneHourConsumed = new Energy(utcNow, Long.parseLong(matcher.group(4), 16), Duration.ofHours(1));
oneHourProduced = new Energy(utcNow, Long.parseLong(matcher.group(5), 16), Duration.ofHours(1));
} else {
throw new PlugwisePayloadMismatchException(POWER_INFORMATION_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.POWER_LOG_INTERVAL_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the interval of historic power consumption and production measurements. These historic measurements are
* returned by the {@link PowerBufferRequestMessage}.
*
* @author Wouter Born - Initial contribution
*/
public class PowerLogIntervalSetRequestMessage extends Message {
private Duration consumptionInterval;
private Duration productionInterval;
public PowerLogIntervalSetRequestMessage(MACAddress macAddress, Duration consumptionInterval,
Duration productionInterval) {
super(POWER_LOG_INTERVAL_SET_REQUEST, macAddress);
this.consumptionInterval = consumptionInterval;
this.productionInterval = productionInterval;
}
@Override
protected String payloadToHexString() {
String consumptionIntervalHex = String.format("%04X", consumptionInterval.toMinutes());
String productionIntervalHex = String.format("%04X", productionInterval.toMinutes());
return consumptionIntervalHex + productionIntervalHex;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_GET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the real-time clock value of a Circle+.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RealTimeClockGetRequestMessage extends Message {
public RealTimeClockGetRequestMessage(MACAddress macAddress) {
super(REAL_TIME_CLOCK_GET_REQUEST, macAddress);
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_GET_RESPONSE;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Contains the real-time clock value of a Circle+. This message is the response of a
* {@link RealTimeClockGetRequestMessage}. The Circle+ is the only device that holds a real-time clock value.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RealTimeClockGetResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern
.compile("(\\w{16})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})(\\w{2})");
private int seconds;
private int minutes;
private int hour;
private int weekday;
private int day;
private int month;
private int year;
public RealTimeClockGetResponseMessage(int sequenceNumber, String payload) {
super(REAL_TIME_CLOCK_GET_RESPONSE, sequenceNumber, payload);
}
public int getDay() {
return day;
}
public int getHour() {
return hour;
}
public int getMinutes() {
return minutes;
}
public int getMonth() {
return month;
}
public int getSeconds() {
return seconds;
}
public LocalDateTime getDateTime() {
ZonedDateTime utcDateTime = ZonedDateTime.of(year, month, day, hour, minutes, seconds, 0, UTC);
return utcDateTime.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
public int getWeekday() {
return weekday;
}
public int getYear() {
return year;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
// Real-time clock values in the message are decimals and not hexadecimals
seconds = Integer.parseInt(matcher.group(2));
minutes = Integer.parseInt(matcher.group(3));
hour = Integer.parseInt(matcher.group(4));
weekday = Integer.parseInt(matcher.group(5));
day = Integer.parseInt(matcher.group(6));
month = Integer.parseInt(matcher.group(7));
year = Integer.parseInt(matcher.group(8)) + 2000;
} else {
throw new PlugwisePayloadMismatchException(REAL_TIME_CLOCK_GET_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static java.time.ZoneOffset.UTC;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.REAL_TIME_CLOCK_SET_REQUEST;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the real-time clock value of a Circle+. The Circle+ is the only device that holds a real-time clock value.
*
* @author Wouter Born - Initial contribution
*/
public class RealTimeClockSetRequestMessage extends Message {
private ZonedDateTime utcDateTime;
public RealTimeClockSetRequestMessage(MACAddress macAddress, LocalDateTime localDateTime) {
super(REAL_TIME_CLOCK_SET_REQUEST, macAddress);
this.utcDateTime = localDateTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(UTC);
}
@Override
protected String payloadToHexString() {
// Real-time clock values in the message are decimals and not hexadecimals
String second = String.format("%02d", utcDateTime.getSecond());
String minute = String.format("%02d", utcDateTime.getMinute());
String hour = String.format("%02d", utcDateTime.getHour());
// Monday = 0, ... , Sunday = 6
String dayOfWeek = String.format("%02d", utcDateTime.getDayOfWeek().getValue() - 1);
String day = String.format("%02d", utcDateTime.getDayOfMonth());
String month = String.format("%02d", utcDateTime.getMonthValue());
String year = String.format("%02d", utcDateTime.getYear() - 2000);
return second + minute + hour + dayOfWeek + day + month + year;
}
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_ROLE_CALL_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Requests the Circle+ to return the MAC address for a specific node. This message is answered by a
* {@link RoleCallResponseMessage} which contains the MAC address. Because a Plugwise network can have 64 devices,
* the node ID value has a range from 0 to 63.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RoleCallRequestMessage extends Message {
private int nodeID;
public RoleCallRequestMessage(MACAddress macAddress, int nodeID) {
super(DEVICE_ROLE_CALL_REQUEST, macAddress);
this.nodeID = nodeID;
}
@Override
public String getPayload() {
return String.format("%02X", nodeID);
}
@Override
protected String payloadToHexString() {
return String.format("%02X", nodeID);
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.DEVICE_ROLE_CALL_RESPONSE;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* <p>
* The Circle+ sends this message as response to a {@link RoleCallRequestMessage}. It contains the MAC address for the
* the node identified by the node ID in the request message. When no node is known with given ID, the MAC
* address will be empty.
* </p>
* <p>
* The MAC address can belong to a relay device (Circle, Stealth) as well as a sleeping end device (SED: Scan, Sense,
* Switch). An {@link InformationRequestMessage} can be used to determine the actual device type (when it is online).
* </p>
* <p>
* The Circle+ MAC address can not be retrieved from the node list. The Circle+ MAC address can be retrieved with a
* {@link NetworkStatusRequestMessage}.
* </p>
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
public class RoleCallResponseMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{16})(\\w{2})");
private static final String EMPTY_MAC_ADDRESS = "FFFFFFFFFFFFFFFF";
private int nodeID;
private MACAddress nodeMAC;
public RoleCallResponseMessage(int sequenceNumber, String payload) {
super(DEVICE_ROLE_CALL_RESPONSE, sequenceNumber, payload);
}
public int getNodeID() {
return nodeID;
}
public MACAddress getNodeMAC() {
return nodeMAC;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
nodeMAC = matcher.group(2).equals(EMPTY_MAC_ADDRESS) ? null : new MACAddress(matcher.group(2));
nodeID = (Integer.parseInt(matcher.group(3), 16));
} else {
throw new PlugwisePayloadMismatchException(DEVICE_ROLE_CALL_RESPONSE, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SCAN_PARAMETERS_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Sensitivity;
/**
* Sets the Scan motion detection parameters. These parameters control when the Scan sends on/off commands.
*
* @author Wouter Born - Initial contribution
*/
public class ScanParametersSetRequestMessage extends Message {
private Sensitivity sensitivity;
private boolean daylightOverride;
private Duration switchOffDelay;
public ScanParametersSetRequestMessage(MACAddress macAddress, Sensitivity sensitivity, boolean daylightOverride,
Duration switchOffDelay) {
super(SCAN_PARAMETERS_SET_REQUEST, macAddress);
this.sensitivity = sensitivity;
this.daylightOverride = daylightOverride;
this.switchOffDelay = switchOffDelay;
}
@Override
protected String payloadToHexString() {
String sensitivityHex = String.format("%02X", sensitivity.toInt());
String daylightOverrideHex = (daylightOverride ? "01" : "00");
String switchOffDelayHex = String.format("%02X", switchOffDelay.toMinutes());
return sensitivityHex + daylightOverrideHex + switchOffDelayHex;
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_BOUNDARIES_SET_REQUEST;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryAction;
import org.openhab.binding.plugwise.internal.protocol.field.BoundaryType;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* Sets the Sense boundary switching parameters. These parameters control when the Sense sends on/off commands.
*
* @author Wouter Born - Initial contribution
*/
public class SenseBoundariesSetRequestMessage extends Message {
private static final String MIN_BOUNDARY_VALUE = "0000";
private static final String MAX_BOUNDARY_VALUE = "FFFF";
private BoundaryType boundaryType;
private BoundaryAction boundaryAction;
private String lowerBoundaryHex;
private String upperBoundaryHex;
/**
* Disables Sense boundary switching.
*/
public SenseBoundariesSetRequestMessage(MACAddress macAddress) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.TEMPERATURE;
this.boundaryAction = BoundaryAction.OFF_BELOW_ON_ABOVE;
this.lowerBoundaryHex = MIN_BOUNDARY_VALUE;
this.upperBoundaryHex = MAX_BOUNDARY_VALUE;
}
public SenseBoundariesSetRequestMessage(MACAddress macAddress, Temperature lowerBoundary, Temperature upperBoundary,
BoundaryAction boundaryAction) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.TEMPERATURE;
this.boundaryAction = boundaryAction;
this.lowerBoundaryHex = lowerBoundary.toHex();
this.upperBoundaryHex = upperBoundary.toHex();
}
public SenseBoundariesSetRequestMessage(MACAddress macAddress, Humidity lowerBoundary, Humidity upperBoundary,
BoundaryAction boundaryAction) {
super(SENSE_BOUNDARIES_SET_REQUEST, macAddress);
this.boundaryType = BoundaryType.HUMIDITY;
this.boundaryAction = boundaryAction;
this.lowerBoundaryHex = lowerBoundary.toHex();
this.upperBoundaryHex = upperBoundary.toHex();
}
@Override
protected String payloadToHexString() {
String boundaryTypeHex = String.format("%02X", boundaryType.toInt());
String lowerBoundaryActionHex = String.format("%02X", boundaryAction.getLowerAction());
String upperBoundaryActionHex = String.format("%02X", boundaryAction.getUpperAction());
return boundaryTypeHex + upperBoundaryHex + upperBoundaryActionHex + lowerBoundaryHex + lowerBoundaryActionHex;
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_REPORT_INTERVAL_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets the Sense temperature and humidity measurement report interval. Based on this interval, periodically a
* {@link SenseReportRequestMessage} is sent.
*
* @author Wouter Born - Initial contribution
*/
public class SenseReportIntervalSetRequest extends Message {
private Duration reportInterval;
public SenseReportIntervalSetRequest(MACAddress macAddress, Duration reportInterval) {
super(SENSE_REPORT_INTERVAL_SET_REQUEST, macAddress);
this.reportInterval = reportInterval;
}
@Override
protected String payloadToHexString() {
return String.format("%02X", reportInterval.toMinutes());
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SENSE_REPORT_REQUEST;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.plugwise.internal.protocol.field.Humidity;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
import org.openhab.binding.plugwise.internal.protocol.field.Temperature;
/**
* A Sense periodically sends this message for updating the current temperature and humidity.
*
* @author Wouter Born - Initial contribution
*/
public class SenseReportRequestMessage extends Message {
private static final Pattern PAYLOAD_PATTERN = Pattern.compile("(\\w{16})(\\w{4})(\\w{4})");
private Humidity humidity;
private Temperature temperature;
public SenseReportRequestMessage(int sequenceNumber, String payload) {
super(SENSE_REPORT_REQUEST, sequenceNumber, payload);
}
public Humidity getHumidity() {
return humidity;
}
public Temperature getTemperature() {
return temperature;
}
@Override
protected void parsePayload() {
Matcher matcher = PAYLOAD_PATTERN.matcher(payload);
if (matcher.matches()) {
macAddress = new MACAddress(matcher.group(1));
humidity = new Humidity(matcher.group(2));
temperature = new Temperature(matcher.group(3));
} else {
throw new PlugwisePayloadMismatchException(SENSE_REPORT_REQUEST, PAYLOAD_PATTERN, payload);
}
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol;
import static org.openhab.binding.plugwise.internal.protocol.field.MessageType.SLEEP_SET_REQUEST;
import java.time.Duration;
import org.openhab.binding.plugwise.internal.protocol.field.MACAddress;
/**
* Sets when sleeping end devices (Scan, Sense, Switch) sleep and wake-up .
*
* @author Wouter Born - Initial contribution
*/
public class SleepSetRequestMessage extends Message {
private static final Duration DEFAULT_SLEEP_DURATION = Duration.ofSeconds(5);
private Duration wakeupDuration;
private Duration sleepDuration;
private Duration wakeupInterval;
private int unknown;
public SleepSetRequestMessage(MACAddress macAddress, Duration wakeupDuration, Duration sleepDuration,
Duration wakeupInterval) {
super(SLEEP_SET_REQUEST, macAddress);
this.wakeupDuration = wakeupDuration;
this.sleepDuration = sleepDuration;
this.wakeupInterval = wakeupInterval;
}
public SleepSetRequestMessage(MACAddress macAddress, Duration wakeupDuration, Duration wakeupInterval) {
this(macAddress, wakeupDuration, DEFAULT_SLEEP_DURATION, wakeupInterval);
}
@Override
protected String payloadToHexString() {
String wakeupDurationHex = String.format("%02X", wakeupDuration.getSeconds());
String sleepDurationHex = String.format("%04X", sleepDuration.getSeconds());
String wakeupIntervalHex = String.format("%04X", wakeupInterval.toMinutes());
String unknownHex = String.format("%06X", unknown);
return wakeupDurationHex + sleepDurationHex + wakeupIntervalHex + unknownHex;
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The boundary switch action of a Sense when the value is below/above the boundary minimum/maximum.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum BoundaryAction {
OFF_BELOW_ON_ABOVE(0, 1),
ON_BELOW_OFF_ABOVE(1, 0);
private final int lowerAction;
private final int upperAction;
BoundaryAction(int lowerAction, int upperAction) {
this.lowerAction = lowerAction;
this.upperAction = upperAction;
}
public int getLowerAction() {
return lowerAction;
}
public int getUpperAction() {
return upperAction;
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The boundary type that a Sense uses for switching.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum BoundaryType {
HUMIDITY(0),
TEMPERATURE(1),
NONE(2);
private final int identifier;
BoundaryType(int identifier) {
this.identifier = identifier;
}
public int toInt() {
return identifier;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Enumerates Plugwise devices.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum DeviceType {
STICK("Stick", false, false),
CIRCLE("Circle", true, false),
CIRCLE_PLUS("Circle+", true, false),
SCAN("Scan", false, true),
SENSE("Sense", false, true),
STEALTH("Stealth", true, false),
SWITCH("Switch", false, true),
UNKNOWN("Unknown", false, false);
private final String string;
private final boolean relayDevice;
private final boolean sleepingEndDevice;
DeviceType(String string, boolean relayDevice, boolean sleepingEndDevice) {
this.string = string;
this.relayDevice = relayDevice;
this.sleepingEndDevice = sleepingEndDevice;
}
public boolean isRelayDevice() {
return relayDevice;
}
public boolean isSleepingEndDevice() {
return sleepingEndDevice;
}
@Override
public String toString() {
return string;
}
}

View File

@@ -0,0 +1,123 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A simple class to represent energy usage, converting between Plugwise data representations.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class Energy {
private static final int WATTS_PER_KILOWATT = 1000;
private static final double PULSES_PER_KW_SECOND = 468.9385193;
private static final double PULSES_PER_W_SECOND = PULSES_PER_KW_SECOND / WATTS_PER_KILOWATT;
private @Nullable ZonedDateTime utcStart; // using UTC resolves wrong local start/end timestamps when DST changes
// occur
private ZonedDateTime utcEnd;
private long pulses;
private @Nullable Duration interval;
public Energy(ZonedDateTime utcEnd, long pulses) {
this.utcEnd = utcEnd;
this.pulses = pulses;
}
public Energy(ZonedDateTime utcEnd, long pulses, Duration interval) {
this.utcEnd = utcEnd;
this.pulses = pulses;
this.interval = interval;
updateStart(interval);
}
private double correctPulses(double pulses, PowerCalibration calibration) {
double gainA = calibration.getGainA();
double gainB = calibration.getGainB();
double offsetNoise = calibration.getOffsetNoise();
double offsetTotal = calibration.getOffsetTotal();
double correctedPulses = Math.pow(pulses + offsetNoise, 2) * gainB + (pulses + offsetNoise) * gainA
+ offsetTotal;
if ((pulses > 0 && correctedPulses < 0) || (pulses < 0 && correctedPulses > 0)) {
return 0;
}
return correctedPulses;
}
public LocalDateTime getEnd() {
return utcEnd.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
public @Nullable Duration getInterval() {
return interval;
}
public long getPulses() {
return pulses;
}
public @Nullable LocalDateTime getStart() {
ZonedDateTime localUtcStart = utcStart;
if (localUtcStart == null) {
return null;
}
return localUtcStart.withZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime();
}
private double intervalSeconds() {
Duration localInterval = interval;
if (localInterval == null) {
throw new IllegalStateException("Failed to calculate seconds because interval is null");
}
double seconds = localInterval.getSeconds();
seconds += localInterval.getNano() / ChronoUnit.SECONDS.getDuration().toNanos();
return seconds;
}
public void setInterval(Duration interval) {
this.interval = interval;
updateStart(interval);
}
public double tokWh(PowerCalibration calibration) {
return toWatt(calibration) * intervalSeconds()
/ (ChronoUnit.HOURS.getDuration().getSeconds() * WATTS_PER_KILOWATT);
}
@Override
public String toString() {
return "Energy [utcStart=" + utcStart + ", utcEnd=" + utcEnd + ", pulses=" + pulses + ", interval=" + interval
+ "]";
}
public double toWatt(PowerCalibration calibration) {
double averagePulses = pulses / intervalSeconds();
return correctPulses(averagePulses, calibration) / PULSES_PER_W_SECOND;
}
private void updateStart(Duration interval) {
utcStart = utcEnd.minus(interval);
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A relative humidity class that is used for converting from and to Plugwise protocol values.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Humidity {
private static final String EMPTY_VALUE = "FFFF";
private static final double MAX_HEX_VALUE = 65536;
private static final double MULTIPLIER = 125;
private static final double OFFSET = 6;
private final double value;
public Humidity(double value) {
this.value = value;
}
public Humidity(String hexValue) {
if (EMPTY_VALUE.equals(hexValue)) {
value = Double.MIN_VALUE;
} else {
value = MULTIPLIER * (Integer.parseInt(hexValue, 16) / MAX_HEX_VALUE) - OFFSET;
}
}
public double getValue() {
return value;
}
public String toHex() {
return String.format("%04X", Math.round((value + OFFSET) / MULTIPLIER * MAX_HEX_VALUE));
}
@Override
public String toString() {
return String.format("%.3f%%", value);
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The media access control (MAC) address of a Plugwise device, e.g.: 000D6F0000A1B2C3
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class MACAddress {
private final String macAddress;
public MACAddress(String macAddress) {
this.macAddress = macAddress.toUpperCase();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + macAddress.hashCode();
return result;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MACAddress other = (MACAddress) obj;
if (!macAddress.equals(other.macAddress)) {
return false;
}
return true;
}
@Override
public String toString() {
return macAddress;
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Enumerates all Plugwise message types. Many are still missing, and require further protocol analysis.
*
* @author Wouter Born, Karel Goderis - Initial contribution
*/
@NonNullByDefault
public enum MessageType {
ACKNOWLEDGEMENT_V1(0x0000),
NODE_AVAILABLE(0x0006),
NODE_AVAILABLE_RESPONSE(0x0007),
NETWORK_RESET_REQUEST(0x0008),
NETWORK_STATUS_REQUEST(0x000A),
PING_REQUEST(0x000D),
PING_RESPONSE(0x000E),
NETWORK_STATUS_RESPONSE(0x0011),
POWER_INFORMATION_REQUEST(0x0012),
POWER_INFORMATION_RESPONSE(0x0013),
CLOCK_SET_REQUEST(0x0016),
POWER_CHANGE_REQUEST(0x0017),
DEVICE_ROLE_CALL_REQUEST(0x0018),
DEVICE_ROLE_CALL_RESPONSE(0x0019),
DEVICE_INFORMATION_REQUEST(0x0023),
DEVICE_INFORMATION_RESPONSE(0x0024),
POWER_CALIBRATION_REQUEST(0x0026),
POWER_CALIBRATION_RESPONSE(0x0027),
REAL_TIME_CLOCK_SET_REQUEST(0x0028),
REAL_TIME_CLOCK_GET_REQUEST(0x0029),
REAL_TIME_CLOCK_GET_RESPONSE(0x003A),
CLOCK_GET_REQUEST(0x003E),
CLOCK_GET_RESPONSE(0x003F),
POWER_BUFFER_REQUEST(0x0048),
POWER_BUFFER_RESPONSE(0x0049),
ANNOUNCE_AWAKE_REQUEST(0x004F),
SLEEP_SET_REQUEST(0x0050),
POWER_LOG_INTERVAL_SET_REQUEST(0x0057),
BROADCAST_GROUP_SWITCH_RESPONSE(0x0056),
MODULE_JOINED_NETWORK_REQUEST(0x0061),
ACKNOWLEDGEMENT_V2(0x0100),
SCAN_PARAMETERS_SET_REQUEST(0x0101),
LIGHT_CALIBRATION_REQUEST(0x0102),
SENSE_REPORT_INTERVAL_SET_REQUEST(0x0103),
SENSE_BOUNDARIES_SET_REQUEST(0x0104),
SENSE_REPORT_REQUEST(0x0105);
private static final Map<Integer, MessageType> TYPES_BY_VALUE = new HashMap<>();
static {
for (MessageType type : MessageType.values()) {
TYPES_BY_VALUE.put(type.identifier, type);
}
}
private final int identifier;
MessageType(int value) {
identifier = value;
}
public static @Nullable MessageType forValue(int value) {
return TYPES_BY_VALUE.get(value);
}
public int toInt() {
return identifier;
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The power calibration data of a relay device (Circle, Circle+, Stealth). It is used in {@link Energy} to calculate
* energy (kWh) and power (W) from pulses.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class PowerCalibration {
private final double gainA;
private final double gainB;
private final double offsetTotal;
private final double offsetNoise;
public PowerCalibration(double gainA, double gainB, double offsetNoise, double offsetTotal) {
this.gainA = gainA;
this.gainB = gainB;
this.offsetNoise = offsetNoise;
this.offsetTotal = offsetTotal;
}
public double getGainA() {
return gainA;
}
public double getGainB() {
return gainB;
}
public double getOffsetTotal() {
return offsetTotal;
}
public double getOffsetNoise() {
return offsetNoise;
}
@Override
public String toString() {
return "PowerCalibration [gainA=" + gainA + ", gainB=" + gainB + ", offsetTotal=" + offsetTotal
+ ", offsetNoise=" + offsetNoise + "]";
}
}

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The motion sensitivity range of a Scan.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public enum Sensitivity {
HIGH(0x14),
MEDIUM(0x1E),
OFF(0xFF);
private final int value;
Sensitivity(int value) {
this.value = value;
}
public int toInt() {
return value;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.plugwise.internal.protocol.field;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* A temperature (Celsius) class that is used for converting from and to Plugwise protocol values.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class Temperature {
private static final String EMPTY_VALUE = "FFFF";
private static final double MAX_HEX_VALUE = 65536;
private static final double MULTIPLIER = 175.72;
private static final double OFFSET = 46.85;
private final double value;
public Temperature(double value) {
this.value = value;
}
public Temperature(String hexValue) {
if (EMPTY_VALUE.equals(hexValue)) {
value = Double.MIN_VALUE;
} else {
value = MULTIPLIER * (Integer.parseInt(hexValue, 16) / MAX_HEX_VALUE) - OFFSET;
}
}
public double getValue() {
return value;
}
public String toHex() {
return String.format("%04X", Math.round((value + OFFSET) / MULTIPLIER * MAX_HEX_VALUE));
}
@Override
public String toString() {
return String.format("%.3f\u00B0C", value);
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="plugwise" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Plugwise Binding</name>
<description>Monitor and control Plugwise ZigBee devices using the Stick. Supported devices are the Circle, Circle+,
Scan, Sense, Stealth and Switch.</description>
<author>Wouter Born</author>
</binding:binding>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="circle">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Circle</label>
<description>A power outlet plug that provides energy measurement and switching control of appliances</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="circleplus">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Circle+</label>
<description>A special Circle that coordinates the ZigBee network and acts as network gateway</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="realtimeclock" typeId="realtimeclock"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="scan">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Scan</label>
<description>A wireless motion (PIR) and light sensor</description>
<channels>
<channel id="triggered" typeId="triggered"/>
<channel id="lastseen" typeId="lastseen"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:scan"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sense">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Sense</label>
<description>A wireless temperature and humidity sensor</description>
<channels>
<channel id="humidity" typeId="humidity"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="temperature" typeId="temperature"/>
<channel id="triggered" typeId="triggered"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:sense"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="stealth">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Stealth</label>
<description>A Circle with a more compact form factor that can be built-in</description>
<channels>
<channel id="clock" typeId="clock"/>
<channel id="energy" typeId="energy"/>
<channel id="energystamp" typeId="energystamp"/>
<channel id="lastseen" typeId="lastseen"/>
<channel id="power" typeId="power"/>
<channel id="state" typeId="state"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:relay"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="stick">
<label>Plugwise Stick</label>
<description>A ZigBee USB controller used for communicating with the Circle+</description>
<representation-property>macAddress</representation-property>
<config-description-ref uri="bridge-type:plugwise:stick"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="plugwise"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="switch">
<supported-bridge-type-refs>
<bridge-type-ref id="stick"/>
</supported-bridge-type-refs>
<label>Plugwise Switch</label>
<description>A wireless wall switch</description>
<channels>
<channel id="lastseen" typeId="lastseen"/>
<channel id="leftbuttonstate" typeId="leftbuttonstate"/>
<channel id="rightbuttonstate" typeId="rightbuttonstate"/>
</channels>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:plugwise:switch"/>
</thing-type>
</thing:thing-descriptions>