/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.zwave.internal.protocol.commandclass; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ArrayBlockingQueue; import org.openhab.binding.zwave.internal.config.ZWaveDbCommandClass; import org.openhab.binding.zwave.internal.protocol.SerialMessage; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageClass; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessagePriority; import org.openhab.binding.zwave.internal.protocol.SerialMessage.SerialMessageType; import org.openhab.binding.zwave.internal.protocol.ZWaveController; import org.openhab.binding.zwave.internal.protocol.ZWaveEndpoint; import org.openhab.binding.zwave.internal.protocol.ZWaveEventListener; import org.openhab.binding.zwave.internal.protocol.ZWaveNode; import org.openhab.binding.zwave.internal.protocol.event.ZWaveEvent; import org.openhab.binding.zwave.internal.protocol.event.ZWaveTransactionCompletedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thoughtworks.xstream.annotations.XStreamAlias; import com.thoughtworks.xstream.annotations.XStreamOmitField; /** * Wake Up Command Class. Enables a node to notify another device * that it woke up and is ready to receive commands. * * @author Jan-Willem Spuij * @author Chris Jackson * @since 1.3.0 */ @XStreamAlias("WakeUpCommandClass") public class ZWaveWakeUpCommandClass extends ZWaveCommandClass implements ZWaveCommandClassInitialization, ZWaveEventListener { @XStreamOmitField private static final Logger logger = LoggerFactory.getLogger(ZWaveWakeUpCommandClass.class); private static final int MAX_SUPPORTED_VERSION = 2; public static final int WAKE_UP_INTERVAL_SET = 0x04; public static final int WAKE_UP_INTERVAL_GET = 0x05; public static final int WAKE_UP_INTERVAL_REPORT = 0x06; public static final int WAKE_UP_NOTIFICATION = 0x07; public static final int WAKE_UP_NO_MORE_INFORMATION = 0x08; public static final int WAKE_UP_INTERVAL_CAPABILITIES_GET = 0x09; public static final int WAKE_UP_INTERVAL_CAPABILITIES_REPORT = 0x0A; private static final int MAX_BUFFFER_SIZE = 128; @XStreamOmitField private ArrayBlockingQueue<SerialMessage> wakeUpQueue; // From interval report @XStreamOmitField private boolean initReportDone = false; private int targetNodeId = 0; private int interval = 0; // From capabilities report @XStreamOmitField private boolean initCapabilitiesDone = false; private int minInterval = 0; private int maxInterval = 0; private int defaultInterval = 0; private int intervalStep = 0; private Date lastWakeup = null; @XStreamOmitField private volatile boolean isAwake = false; @XStreamOmitField private Timer timer = null; @XStreamOmitField private TimerTask timerTask = null; @XStreamOmitField private boolean initialiseDone = false; private boolean isGetSupported = true; /** * Creates a new instance of the ZWaveWakeUpCommandClass class. * * @param node the node this command class belongs to * @param controller the controller to use * @param endpoint the endpoint this Command class belongs to */ public ZWaveWakeUpCommandClass(ZWaveNode node, ZWaveController controller, ZWaveEndpoint endpoint) { super(node, controller, endpoint); wakeUpQueue = new ArrayBlockingQueue<SerialMessage>(MAX_BUFFFER_SIZE, true); timer = new Timer(); } /** * Resolves uninitialized fields after XML Deserialization. * * @return The current {@link ZWaveWakeUpCommandClass} instance. */ private Object readResolve() { wakeUpQueue = new ArrayBlockingQueue<SerialMessage>(MAX_BUFFFER_SIZE, true); timer = new Timer(); return this; } /** * {@inheritDoc} */ @Override public CommandClass getCommandClass() { return CommandClass.WAKE_UP; } /** * {@inheritDoc} */ @Override public int getMaxVersion() { return MAX_SUPPORTED_VERSION; }; /** * {@inheritDoc} */ @Override public void handleApplicationCommandRequest(SerialMessage serialMessage, int offset, int endpoint) { logger.debug("NODE {}: Received Wake Up Request", this.getNode().getNodeId()); int command = serialMessage.getMessagePayloadByte(offset); switch (command) { case WAKE_UP_INTERVAL_SET: case WAKE_UP_INTERVAL_GET: case WAKE_UP_INTERVAL_CAPABILITIES_GET: case WAKE_UP_NO_MORE_INFORMATION: logger.warn("Command {} not implemented.", command); return; case WAKE_UP_INTERVAL_REPORT: logger.trace("NODE {}: Process Wake Up Interval", this.getNode().getNodeId()); initReportDone = true; // according to open-zwave: it seems that some devices send incorrect interval report messages. Don't // know if they are spurious. // if not we should advance the node stage. if (serialMessage.getMessagePayload().length < offset + 4) { logger.error("NODE {}: Unusual response: WAKE_UP_INTERVAL_REPORT with length = {}. Ignored.", this.getNode().getNodeId(), serialMessage.getMessagePayload().length); return; } targetNodeId = serialMessage.getMessagePayloadByte(offset + 4); int receivedInterval = ((serialMessage.getMessagePayloadByte(offset + 1)) << 16) | ((serialMessage.getMessagePayloadByte(offset + 2)) << 8) | (serialMessage.getMessagePayloadByte(offset + 3)); logger.debug("NODE {}: Wake up interval report, value = {} seconds, targetNodeId = {}", this.getNode().getNodeId(), receivedInterval, targetNodeId); this.interval = receivedInterval; ZWaveWakeUpEvent event = new ZWaveWakeUpEvent(getNode().getNodeId(), WAKE_UP_INTERVAL_REPORT); this.getController().notifyEventListeners(event); break; case WAKE_UP_INTERVAL_CAPABILITIES_REPORT: logger.trace("NODE {}: Process Wake Up Interval Capabilities", this.getNode().getNodeId()); initCapabilitiesDone = true; this.minInterval = ((serialMessage.getMessagePayloadByte(offset + 1)) << 16) | ((serialMessage.getMessagePayloadByte(offset + 2)) << 8) | (serialMessage.getMessagePayloadByte(offset + 3)); this.maxInterval = ((serialMessage.getMessagePayloadByte(offset + 4)) << 16) | ((serialMessage.getMessagePayloadByte(offset + 5)) << 8) | (serialMessage.getMessagePayloadByte(offset + 6)); this.defaultInterval = ((serialMessage.getMessagePayloadByte(offset + 7)) << 16) | ((serialMessage.getMessagePayloadByte(offset + 8)) << 8) | (serialMessage.getMessagePayloadByte(offset + 9)); this.intervalStep = ((serialMessage.getMessagePayloadByte(offset + 10)) << 16) | ((serialMessage.getMessagePayloadByte(offset + 11)) << 8) | (serialMessage.getMessagePayloadByte(offset + 12)); logger.debug("NODE {}: Wake up interval capabilities report", this.getNode().getNodeId()); logger.debug("NODE {}: Minimum interval = {}", this.getNode().getNodeId(), this.minInterval); logger.debug("NODE {}: Maximum interval = {}", this.getNode().getNodeId(), this.maxInterval); logger.debug("NODE {}: Default interval = {}", this.getNode().getNodeId(), this.defaultInterval); logger.debug("NODE {}: Interval step = {}", this.getNode().getNodeId(), this.intervalStep); break; case WAKE_UP_NOTIFICATION: logger.debug("NODE {}: Received WAKE_UP_NOTIFICATION", this.getNode().getNodeId()); serialMessage.setTransactionCanceled(); // Set the awake flag. This will also empty the queue this.setAwake(true); break; default: logger.warn(String.format("NODE %d: Unsupported Command 0x%02X for command class %s (0x%02X).", this.getNode().getNodeId(), command, this.getCommandClass().getLabel(), this.getCommandClass().getKey())); } } /** * Gets a SerialMessage with the WAKE_UP_NO_MORE_INFORMATION command. * * @return the serial message */ public SerialMessage getNoMoreInformationMessage() { logger.debug("NODE {}: Creating new message for application command WAKE_UP_NO_MORE_INFORMATION", this.getNode().getNodeId()); SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.Immediate); byte[] newPayload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(), (byte) WAKE_UP_NO_MORE_INFORMATION }; result.setMessagePayload(newPayload); return result; } /** * If the device is awake, it returns true to indicate that this message can be sent * immediately. If the device is not awake, it puts the message in the wake-up queue * to send the message on next wake-up. * The message is only added if it's not the WAKE_UP_NO_MORE_INFORMATION message * since we don't want to send this at the next wakeup. * This combines the previous 'putInWakeUpQueue' with 'isAlive'. * * @param serialMessage the message to put in the wake-up queue. * @return true if the message can be sent immediately */ public boolean processOutgoingWakeupMessage(SerialMessage serialMessage) { // The message is Ok, if we're awake, send it now... if (isAwake) { // We're sending a frame, so we need to stop the timer if it's running resetSleepTimer(); return true; } // Make sure we never add the WAKE_UP_NO_MORE_INFORMATION message to the queue if (serialMessage.getMessagePayload().length >= 2 && serialMessage.getMessagePayload()[2] == (byte) WAKE_UP_NO_MORE_INFORMATION) { logger.debug("NODE {}: Last MSG not queuing.", this.getNode().getNodeId()); return false; } if (this.wakeUpQueue.contains(serialMessage)) { logger.debug("NODE {}: Message already on the wake-up queue. Removing original.", this.getNode().getNodeId()); this.wakeUpQueue.remove(serialMessage); } logger.debug("NODE {}: Putting message {} in wakeup queue.", this.getNode().getNodeId(), serialMessage.getMessageClass()); this.wakeUpQueue.add(serialMessage); // This message has been queued - don't send it now... return false; } /** * Gets a SerialMessage with the WAKE UP INTERVAL GET command * * @return the serial message */ public SerialMessage getIntervalMessage() { if (isGetSupported == false) { logger.debug("NODE {}: Node doesn't support get requests", this.getNode().getNodeId()); return null; } logger.debug("NODE {}: Creating new message for application command WAKE_UP_INTERVAL_GET", this.getNode().getNodeId()); SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Config); byte[] newPayload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(), (byte) WAKE_UP_INTERVAL_GET }; result.setMessagePayload(newPayload); return result; } /** * Gets a SerialMessage with the WAKE UP INTERVAL CAPABILITIES GET command * * @return the serial message */ public SerialMessage getIntervalCapabilitiesMessage() { if (isGetSupported == false) { logger.debug("NODE {}: Node doesn't support get requests", this.getNode().getNodeId()); return null; } logger.debug("NODE {}: Creating new message for application command WAKE_UP_INTERVAL_CAPABILITIES_GET", this.getNode().getNodeId()); SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.ApplicationCommandHandler, SerialMessagePriority.Config); byte[] newPayload = { (byte) this.getNode().getNodeId(), 2, (byte) getCommandClass().getKey(), (byte) WAKE_UP_INTERVAL_CAPABILITIES_GET }; result.setMessagePayload(newPayload); return result; } /** * Returns the interval at which the device wakes up every time. * * @return the interval in seconds. */ public int getInterval() { return interval; } /** * Returns the minimum wake up interval that can be set to the device. * * @return the minInterval in seconds. */ public int getMinInterval() { return minInterval; } /** * Returns the maximum wake up interval that can be set to the device. * * @return the maxInterval in seconds. */ public int getMaxInterval() { return maxInterval; } /** * Returns the factory default wake up interval for the device. * * @return the defaultInterval in seconds. */ public int getDefaultInterval() { return defaultInterval; } /** * Returns the minimum step that must be taken into account when setting the interval for the device. * * @return the intervalStep in seconds. */ public int getIntervalStep() { return intervalStep; } /** * {@inheritDoc} */ @Override public Collection<SerialMessage> initialize(boolean refresh) { ArrayList<SerialMessage> result = new ArrayList<SerialMessage>(2); if (refresh == true) { initReportDone = false; initCapabilitiesDone = false; } if (initReportDone == false) { // get wake up interval. result.add(getIntervalMessage()); } if (initCapabilitiesDone == false && this.getVersion() > 1) { // get default values for wake up interval. result.add(getIntervalCapabilitiesMessage()); } return result; } /** * Event handler for incoming Z-Wave events. We monitor Z-Wave events for completed * transactions. Once a transaction is completed for the WAKE_UP_NO_MORE_INFORMATION * event, we set the node state to asleep. * {@inheritDoc} */ @Override public void ZWaveIncomingEvent(ZWaveEvent event) { if (!(event instanceof ZWaveTransactionCompletedEvent)) { return; } SerialMessage serialMessage = ((ZWaveTransactionCompletedEvent) event).getCompletedMessage(); if (serialMessage.getMessageClass() != SerialMessageClass.SendData && serialMessage.getMessageType() != SerialMessageType.Request) { return; } byte[] payload = serialMessage.getMessagePayload(); // Check if it's addressed to this node if (payload.length == 0 || (payload[0] & 0xFF) != this.getNode().getNodeId()) { return; } // We now know that this is a message to this node. // If it's not the WAKE_UP_NO_MORE_INFORMATION, then we need to set the wakeup timer if (payload.length >= 4 && (payload[2] & 0xFF) == this.getCommandClass().getKey() && (payload[3] & 0xFF) == WAKE_UP_NO_MORE_INFORMATION) { // This is confirmation of our 'go to sleep' message logger.debug("NODE {}: Went to sleep", this.getNode().getNodeId()); this.setAwake(false); return; } // Send the next message in the wake-up queue if (!this.wakeUpQueue.isEmpty()) { // Get the next message from the queue. // Bump its priority to highest to try and send it while the node is awake serialMessage = this.wakeUpQueue.poll(); serialMessage.setPriority(SerialMessagePriority.Immediate); this.getController().sendData(serialMessage); } else if (isAwake() == true) { // No more messages in the queue. // Start a timer to send the "Go To Sleep" message // This gives other tasks some time to do something if they want setSleepTimer(); } } /** * Returns whether the node is awake. * * @return the isAwake */ public boolean isAwake() { return isAwake; } /** * Sets whether the node is awake. * If the node is awake we send the first message in the wake-up queue. * The remaining messages are triggered within the notification handler * * @param isAwake the isAwake to set */ public void setAwake(boolean isAwake) { this.isAwake = isAwake; if (isAwake) { logger.debug("NODE {}: Is awake with {} messages in the wake-up queue.", this.getNode().getNodeId(), this.wakeUpQueue.size()); this.lastWakeup = Calendar.getInstance().getTime(); ZWaveWakeUpEvent event = new ZWaveWakeUpEvent(getNode().getNodeId(), WAKE_UP_NOTIFICATION); this.getController().notifyEventListeners(event); // Handle the wake-up queue for this node. // We send the first message, and when that's ACKed, we sent the next if (!this.wakeUpQueue.isEmpty()) { // Get the next message from the queue. // Bump its priority to highest to try and send it while the node is awake SerialMessage serialMessage = this.wakeUpQueue.poll(); serialMessage.setPriority(SerialMessagePriority.Immediate); this.getController().sendData(serialMessage); } else { // No messages in the queue. // Start a timer to send the "Go To Sleep" message // This gives other tasks some time to do something if they want setSleepTimer(); } } else { logger.debug("NODE {}: Is sleeping", this.getNode().getNodeId()); } } /** * Sends a command to the device to set the wakeup interval. * The wakeup node is set to the controller. * * @param interval the wakeup interval in seconds * @return the serial message * @author Chris Jackson */ public SerialMessage setInterval(int interval) { logger.debug("NODE {}: Creating new message for application command WAKE_UP_INTERVAL_SET to {}", this.getNode().getNodeId(), interval); SerialMessage result = new SerialMessage(this.getNode().getNodeId(), SerialMessageClass.SendData, SerialMessageType.Request, SerialMessageClass.SendData, SerialMessagePriority.Config); byte[] newPayload = { (byte) this.getNode().getNodeId(), 6, (byte) getCommandClass().getKey(), (byte) WAKE_UP_INTERVAL_SET, (byte) ((interval >> 16) & 0xff), (byte) ((interval >> 8) & 0xff), (byte) (interval & 0xff), (byte) getController().getOwnNodeId() }; result.setMessagePayload(newPayload); byte[] buffer = result.getMessageBuffer(); logger.debug("NODE {}: Sending REQUEST Message = {}", result.getMessageNode(), SerialMessage.bb2hex(buffer)); return result; } /** * Gets the size of the wake up queue * * @return number of messages currently queued */ public int getWakeupQueueLength() { return wakeUpQueue.size(); } /** * Gets the target node for the Wakeup command class * * @return wakeup target node id */ public int getTargetNodeId() { return targetNodeId; } /** * Gets the time the node last woke up * * @return Date of the last wakeup */ public Date getLastWakeup() { return lastWakeup; } // The following timer implements a re-triggerable timer. The timer is triggered // when there are no more messages to be sent in the wake-up queue. When the timer // times out it will send the 'Go To Sleep' message to the node. // The timer just provides some time for anything further to be sent as // a result of any processing. private class WakeupTimerTask extends TimerTask { ZWaveWakeUpCommandClass wakeup; WakeupTimerTask(ZWaveWakeUpCommandClass wakeup) { this.wakeup = wakeup; } @Override public void run() { if (!wakeup.isAwake()) { logger.debug("NODE {}: Already asleep", wakeup.getNode().getNodeId()); return; } // Tell the device to back to sleep. logger.debug("NODE {}: No more messages, go back to sleep", wakeup.getNode().getNodeId()); wakeup.getController().sendData(wakeup.getNoMoreInformationMessage()); } } public synchronized void setSleepTimer() { // Stop any existing timer resetSleepTimer(); // Create the timer task timerTask = new WakeupTimerTask(this); // Start the timer timer.schedule(timerTask, 1000); } public synchronized void resetSleepTimer() { // Stop any existing timer if (timerTask != null) { timerTask.cancel(); } timerTask = null; } @Override public boolean setOptions(ZWaveDbCommandClass options) { if (options.isGetSupported != null) { isGetSupported = options.isGetSupported; } return true; } /** * ZWave wake-up event. * Notifies users that a device has woken up or changed its wakeup parameters * * @author Chris Jackson * @since 1.5.0 */ public class ZWaveWakeUpEvent extends ZWaveEvent { private final int event; /** * Constructor. Creates a new instance of the ZWaveWakeUpEvent * class. * * @param nodeId the nodeId of the event. */ public ZWaveWakeUpEvent(int nodeId, int event) { super(nodeId, 1); this.event = event; } public int getEvent() { return this.event; } } }