/**
* 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.insteonplm.internal.device;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.PriorityQueue;
import org.openhab.binding.insteonplm.InsteonPLMBindingConfig;
import org.openhab.binding.insteonplm.internal.device.DeviceType.FeatureGroup;
import org.openhab.binding.insteonplm.internal.driver.Driver;
import org.openhab.binding.insteonplm.internal.message.FieldException;
import org.openhab.binding.insteonplm.internal.message.Msg;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* The InsteonDevice class holds known per-device state of a single Insteon device,
* including the address, what port(modem) to reach it on etc.
* Note that some Insteon devices de facto consist of two devices (let's say
* a relay and a sensor), but operate under the same address. Such devices will
* be represented just by a single InsteonDevice. Their different personalities
* will then be represented by DeviceFeatures.
*
* @author Bernd Pfrommer
* @since 1.5.0
*/
public class InsteonDevice {
private static final Logger logger = LoggerFactory.getLogger(InsteonDevice.class);
public static enum DeviceStatus {
INITIALIZED,
POLLING
}
private InsteonAddress m_address = new InsteonAddress();
private ArrayList<String> m_ports = new ArrayList<String>();
private long m_pollInterval = -1L; // in milliseconds
private Driver m_driver = null;
private HashMap<String, DeviceFeature> m_features = new HashMap<String, DeviceFeature>();
private String m_productKey = null;
private Long m_lastTimePolled = 0L;
private Long m_lastMsgReceived = 0L;
private boolean m_isModem = false;
private PriorityQueue<QEntry> m_requestQueue = new PriorityQueue<QEntry>();
private DeviceFeature m_featureQueried = null;
/** need to wait after query to avoid misinterpretation of duplicate replies */
private static final int QUIET_TIME_DIRECT_MESSAGE = 2000;
/** how far to space out poll messages */
private static final int TIME_BETWEEN_POLL_MESSAGES = 1500;
private long m_lastQueryTime = 0L;
private boolean m_hasModemDBEntry = false;
private DeviceStatus m_status = DeviceStatus.INITIALIZED;
/**
* Constructor
*/
public InsteonDevice() {
m_lastMsgReceived = System.currentTimeMillis();
}
// --------------------- simple getters -----------------------------
public boolean hasProductKey() {
return m_productKey != null;
}
public String getProductKey() {
return m_productKey;
}
public boolean hasModemDBEntry() {
return m_hasModemDBEntry;
}
public DeviceStatus getStatus() {
return m_status;
}
public InsteonAddress getAddress() {
return (m_address);
}
public Driver getDriver() {
return m_driver;
}
public boolean hasValidPorts() {
return (!m_ports.isEmpty());
}
public long getPollInterval() {
return m_pollInterval;
}
public boolean isModem() {
return m_isModem;
}
public DeviceFeature getFeature(String f) {
return m_features.get(f);
}
public HashMap<String, DeviceFeature> getFeatures() {
return m_features;
}
public byte getX10HouseCode() {
return (m_address.getX10HouseCode());
}
public byte getX10UnitCode() {
return (m_address.getX10UnitCode());
}
public boolean hasProductKey(String key) {
return m_productKey != null && m_productKey.equals(key);
}
public boolean hasValidPollingInterval() {
return (m_pollInterval > 0);
}
public long getPollOverDueTime() {
return (m_lastTimePolled - m_lastMsgReceived);
}
public String getPort() throws IOException {
if (m_ports.isEmpty()) {
throw new IOException("no ports configured for instrument " + getAddress());
}
return (m_ports.iterator().next());
}
public boolean hasAnyListeners() {
synchronized (m_features) {
for (DeviceFeature f : m_features.values()) {
if (f.hasListeners()) {
return true;
}
}
}
return false;
}
// --------------------- simple setters -----------------------------
public void setStatus(DeviceStatus aI) {
m_status = aI;
}
public void setHasModemDBEntry(boolean b) {
m_hasModemDBEntry = b;
}
public void setAddress(InsteonAddress ia) {
m_address = ia;
}
public void setDriver(Driver d) {
m_driver = d;
}
public void setIsModem(boolean f) {
m_isModem = f;
}
public void setProductKey(String pk) {
m_productKey = pk;
}
public void setPollInterval(long pi) {
logger.trace("setting poll interval for {} to {} ", m_address, pi);
if (pi > 0) {
m_pollInterval = pi;
}
}
public void setFeatureQueried(DeviceFeature f) {
synchronized (m_requestQueue) {
m_featureQueried = f;
}
};
public DeviceFeature getFeatureQueried() {
synchronized (m_requestQueue) {
return (m_featureQueried);
}
};
/**
* Add a port. Currently only a single port is being used.
*
* @param p the port to add
*/
public void addPort(String p) {
if (p == null) {
return;
}
if (!m_ports.contains(p)) {
m_ports.add(p);
}
}
/**
* Removes feature listener from this device
*
* @param aItemName name of the feature listener to remove
* @return true if a feature listener was successfully removed
*/
public boolean removeFeatureListener(String aItemName) {
boolean removedListener = false;
synchronized (m_features) {
for (Iterator<Entry<String, DeviceFeature>> it = m_features.entrySet().iterator(); it.hasNext();) {
DeviceFeature f = it.next().getValue();
if (f.removeListener(aItemName)) {
removedListener = true;
}
}
}
return removedListener;
}
/**
* Invoked to process an openHAB command
*
* @param driver The driver to use
* @param c The item configuration
* @param command The actual command to execute
*/
public void processCommand(Driver driver, InsteonPLMBindingConfig c, Command command) {
logger.debug("processing command {} features: {}", command, m_features.size());
synchronized (m_features) {
for (DeviceFeature i : m_features.values()) {
if (i.isReferencedByItem(c.getItemName())) {
i.handleCommand(c, command);
}
}
}
}
/**
* Execute poll on this device: create an array of messages,
* add them to the request queue, and schedule the queue
* for processing.
*
* @param delay scheduling delay (in milliseconds)
*/
public void doPoll(long delay) {
long now = System.currentTimeMillis();
ArrayList<QEntry> l = new ArrayList<QEntry>();
synchronized (m_features) {
int spacing = 0;
for (DeviceFeature i : m_features.values()) {
if (i.hasListeners()) {
Msg m = i.makePollMsg();
if (m != null) {
l.add(new QEntry(i, m, now + delay + spacing));
spacing += TIME_BETWEEN_POLL_MESSAGES;
}
}
}
}
if (l.isEmpty()) {
return;
}
synchronized (m_requestQueue) {
for (QEntry e : l) {
m_requestQueue.add(e);
}
}
RequestQueueManager.s_instance().addQueue(this, now + delay);
if (!l.isEmpty()) {
synchronized (m_lastTimePolled) {
m_lastTimePolled = now;
}
}
}
/**
* Handle incoming message for this device by forwarding
* it to all features that this device supports
*
* @param fromPort port from which the message come in
* @param msg the incoming message
*/
public void handleMessage(String fromPort, Msg msg) {
synchronized (m_lastMsgReceived) {
m_lastMsgReceived = System.currentTimeMillis();
}
synchronized (m_features) {
// first update all features that are
// not status features
for (DeviceFeature f : m_features.values()) {
if (!f.isStatusFeature()) {
logger.debug("----- applying message to feature: {}", f.getName());
if (f.handleMessage(msg, fromPort)) {
// handled a reply to a query,
// mark it as processed
logger.trace("handled reply of direct: {}", f);
setFeatureQueried(null);
break;
}
}
}
// then update all the status features,
// e.g. when the device was last updated
for (DeviceFeature f : m_features.values()) {
if (f.isStatusFeature()) {
f.handleMessage(msg, fromPort);
}
}
}
}
/**
* Helper method to make standard message
*
* @param flags
* @param cmd1
* @param cmd2
* @return standard message
* @throws FieldException
* @throws IOException
*/
public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2) throws FieldException, IOException {
return (makeStandardMessage(flags, cmd1, cmd2, -1));
}
/**
* Helper method to make standard message, possibly with group
*
* @param flags
* @param cmd1
* @param cmd2
* @param group (-1 if not a group message)
* @return standard message
* @throws FieldException
* @throws IOException
*/
public Msg makeStandardMessage(byte flags, byte cmd1, byte cmd2, int group) throws FieldException, IOException {
Msg m = Msg.s_makeMessage("SendStandardMessage");
InsteonAddress addr = null;
if (group != -1) {
flags |= 0xc0; // mark message as group message
// and stash the group number into the address
addr = new InsteonAddress((byte) 0, (byte) 0, (byte) (group & 0xff));
} else {
addr = getAddress();
}
m.setAddress("toAddress", addr);
m.setByte("messageFlags", flags);
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
return m;
}
public Msg makeX10Message(byte rawX10, byte X10Flag) throws FieldException, IOException {
Msg m = Msg.s_makeMessage("SendX10Message");
m.setByte("rawX10", rawX10);
m.setByte("X10Flag", X10Flag);
m.setQuietTime(300L);
return m;
}
/**
* Helper method to make extended message
*
* @param flags
* @param cmd1
* @param cmd2
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2) throws FieldException, IOException {
return makeExtendedMessage(flags, cmd1, cmd2, new byte[] {});
}
/**
* Helper method to make extended message
*
* @param flags
* @param cmd1
* @param cmd2
* @param data array with userdata
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessage(byte flags, byte cmd1, byte cmd2, byte[] data) throws FieldException, IOException {
Msg m = Msg.s_makeMessage("SendExtendedMessage");
m.setAddress("toAddress", getAddress());
m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
m.setUserData(data);
m.setCRC();
return m;
}
/**
* Helper method to make extended message, but with different CRC calculation
*
* @param flags
* @param cmd1
* @param cmd2
* @param data array with user data
* @return extended message
* @throws FieldException
* @throws IOException
*/
public Msg makeExtendedMessageCRC2(byte flags, byte cmd1, byte cmd2, byte[] data)
throws FieldException, IOException {
Msg m = Msg.s_makeMessage("SendExtendedMessage");
m.setAddress("toAddress", getAddress());
m.setByte("messageFlags", (byte) (((flags & 0xff) | 0x10) & 0xff));
m.setByte("command1", cmd1);
m.setByte("command2", cmd2);
m.setUserData(data);
m.setCRC2();
return m;
}
/**
* Called by the RequestQueueManager when the queue has expired
*
* @param timeNow
* @return time when to schedule the next message (timeNow + quietTime)
*/
public long processRequestQueue(long timeNow) {
synchronized (m_requestQueue) {
if (m_requestQueue.isEmpty()) {
return 0L;
}
if (m_featureQueried != null) {
// A feature has been queried, but
// the response has not been digested yet.
// Must wait for the query to be processed.
long dt = timeNow - (m_lastQueryTime + m_featureQueried.getDirectAckTimeout());
if (dt < 0) {
logger.debug("still waiting for query reply from {} for another {} usec", m_address, -dt);
return (timeNow + 2000L); // retry soon
} else {
logger.debug("gave up waiting for query reply from device {}", m_address);
}
}
QEntry qe = m_requestQueue.poll(); // take it off the queue!
if (!qe.getMsg().isBroadcast()) {
logger.debug("qe taken off direct: {} {}", qe.getFeature(), qe.getMsg());
m_lastQueryTime = timeNow;
// mark feature as pending
qe.getFeature().setQueryStatus(DeviceFeature.QueryStatus.QUERY_PENDING);
// also mark this queue as pending so there is no doubt
m_featureQueried = qe.getFeature();
} else {
logger.debug("qe taken off bcast: {} {}", qe.getFeature(), qe.getMsg());
}
long quietTime = qe.getMsg().getQuietTime();
qe.getMsg().setQuietTime(500L); // rate limiting downstream!
try {
writeMessage(qe.getMsg());
} catch (IOException e) {
logger.error("message write failed for msg {}", qe.getMsg(), e);
}
// figure out when the request queue should be checked next
QEntry qnext = m_requestQueue.peek();
long nextExpTime = (qnext == null ? 0L : qnext.getExpirationTime());
long nextTime = Math.max(timeNow + quietTime, nextExpTime);
logger.debug("next request queue processed in {} msec, quiettime = {}", nextTime - timeNow, quietTime);
return (nextTime);
}
}
/**
* Enqueues message to be sent at the next possible time
*
* @param m message to be sent
* @param f device feature that sent this message (so we can associate the response message with it)
*/
public void enqueueMessage(Msg m, DeviceFeature f) {
enqueueDelayedMessage(m, f, 0);
}
/**
* Enqueues message to be sent after a delay
*
* @param m message to be sent
* @param f device feature that sent this message (so we can associate the response message with it)
* @param d time (in milliseconds)to delay before enqueuing message
*/
public void enqueueDelayedMessage(Msg m, DeviceFeature f, long delay) {
long now = System.currentTimeMillis();
synchronized (m_requestQueue) {
m_requestQueue.add(new QEntry(f, m, now + delay));
}
if (!m.isBroadcast()) {
m.setQuietTime(QUIET_TIME_DIRECT_MESSAGE);
}
logger.trace("enqueing direct message with delay {}", delay);
RequestQueueManager.s_instance().addQueue(this, now + delay);
}
private void writeMessage(Msg m) throws IOException {
m_driver.writeMessage(getPort(), m);
}
private void instantiateFeatures(DeviceType dt) {
for (Entry<String, String> fe : dt.getFeatures().entrySet()) {
DeviceFeature f = DeviceFeature.s_makeDeviceFeature(fe.getValue());
if (f == null) {
logger.error("device type {} references unknown feature: {}", dt, fe.getValue());
} else {
addFeature(fe.getKey(), f);
}
}
for (Entry<String, FeatureGroup> fe : dt.getFeatureGroups().entrySet()) {
FeatureGroup fg = fe.getValue();
DeviceFeature f = DeviceFeature.s_makeDeviceFeature(fg.getType());
if (f == null) {
logger.error("device type {} references unknown feature group: {}", dt, fg.getType());
} else {
addFeature(fe.getKey(), f);
}
connectFeatures(fe.getKey(), f, fg.getFeatures());
}
}
private void connectFeatures(String gn, DeviceFeature fg, ArrayList<String> features) {
for (String fs : features) {
DeviceFeature f = m_features.get(fs);
if (f == null) {
logger.error("feature group {} references unknown feature {}", gn, fs);
} else {
logger.debug("{} connected feature: {}", gn, f);
fg.addConnectedFeature(f);
}
}
}
private void addFeature(String name, DeviceFeature f) {
f.setDevice(this);
synchronized (m_features) {
m_features.put(name, f);
}
}
@Override
public String toString() {
String s = m_address.toString();
for (Entry<String, DeviceFeature> f : m_features.entrySet()) {
s += "|" + f.getKey() + "->" + f.getValue().toString();
}
return s;
}
/**
* Factory method
*
* @param dt device type after which to model the device
* @return newly created device
*/
public static InsteonDevice s_makeDevice(DeviceType dt) {
InsteonDevice dev = new InsteonDevice();
dev.instantiateFeatures(dt);
return dev;
}
/**
* Queue entry helper class
*
* @author Bernd Pfrommer
*/
public static class QEntry implements Comparable<QEntry> {
private DeviceFeature m_feature = null;
private Msg m_msg = null;
private long m_expirationTime = 0L;
public DeviceFeature getFeature() {
return m_feature;
}
public Msg getMsg() {
return m_msg;
}
public long getExpirationTime() {
return m_expirationTime;
}
QEntry(DeviceFeature f, Msg m, long t) {
m_feature = f;
m_msg = m;
m_expirationTime = t;
}
@Override
public int compareTo(QEntry a) {
return (int) (m_expirationTime - a.m_expirationTime);
}
}
}