/** * 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.maxcul.internal.message.sequencers; import java.util.HashSet; import java.util.Iterator; import org.openhab.binding.maxcul.internal.MaxCulBindingConfig; import org.openhab.binding.maxcul.internal.MaxCulDevice; import org.openhab.binding.maxcul.internal.MaxCulMsgHandler; import org.openhab.binding.maxcul.internal.messages.AckMsg; import org.openhab.binding.maxcul.internal.messages.BaseMsg; import org.openhab.binding.maxcul.internal.messages.MaxCulMsgType; import org.openhab.binding.maxcul.internal.messages.PairPingMsg; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Handle the pairing and initialisation sequence of a new device. This should * be called when the device has been verified etc as this will just send the * pong without verifying whether we should or not. * * @author Paul Hampson (cyclingengineer) * @since 1.6.0 */ public class PairingInitialisationSequence implements MessageSequencer { private enum PairingInitialisationState { INITIAL_PING, PONG_ACKED, GROUP_ID_ACKED, CONFIG_TEMPS_ACKED, SENDING_ASSOCIATIONS, SENDING_ASSOCIATIONS_ACKED, SENDING_WEEK_PROFILE, RETX_WAKEUP_ACK, FINISHED; } private static final Logger logger = LoggerFactory.getLogger(PairingInitialisationSequence.class); private PairingInitialisationState state = PairingInitialisationState.INITIAL_PING; private String devAddr; private byte group_id; private MaxCulMsgHandler messageHandler; private int pktLostCount = 0; private MaxCulDevice deviceType = MaxCulDevice.UNKNOWN; private MaxCulBindingConfig config; private HashSet<MaxCulBindingConfig> associations; private Iterator<MaxCulBindingConfig> assocIter; private boolean useFast = true; /* place to keep stuff when going through ReTx */ private BaseMsg reTxMsg; private PairingInitialisationState reTxState; public PairingInitialisationSequence(byte group_id, MaxCulMsgHandler messageHandler, MaxCulBindingConfig cfg, HashSet<MaxCulBindingConfig> associations) { this.group_id = group_id; this.messageHandler = messageHandler; this.config = cfg; this.associations = associations; } @Override public void runSequencer(BaseMsg msg) { /* * This sequence is taken from observations of activity between the MAX! * Cube and a wall thermostat and refined using some experimentation :) */ if (state != PairingInitialisationState.RETX_WAKEUP_ACK) { pktLostCount = 0; // reset counter - ack received } logger.debug("Sequence State: " + state); switch (state) { case INITIAL_PING: /* get device type */ PairPingMsg ppMsg = new PairPingMsg(msg.rawMsg); this.deviceType = MaxCulDevice.getDeviceTypeFromInt(ppMsg.type); /* Send PONG - assumes PING is checked */ logger.debug("Sending PONG"); this.devAddr = msg.srcAddrStr; messageHandler.sendPairPong(devAddr, this); state = PairingInitialisationState.PONG_ACKED; break; case PONG_ACKED: if (msg.msgType == MaxCulMsgType.ACK) { AckMsg ack = new AckMsg(msg.rawMsg); if (!ack.getIsNack()) { if (this.deviceType == MaxCulDevice.PUSH_BUTTON) { /* for a push button we're done now */ state = PairingInitialisationState.FINISHED; } else { /* send group id information */ logger.debug("Sending GROUP_ID"); messageHandler.sendSetGroupId(devAddr, group_id, this); state = PairingInitialisationState.GROUP_ID_ACKED; } } else { logger.error("PAIR_PONG was nacked. Ending sequence"); state = PairingInitialisationState.FINISHED; } } else { logger.error("Received " + msg.msgType + " when expecting ACK"); } break; case GROUP_ID_ACKED: if (msg.msgType == MaxCulMsgType.ACK) { AckMsg ack = new AckMsg(msg.rawMsg); if (!ack.getIsNack() && (this.deviceType == MaxCulDevice.RADIATOR_THERMOSTAT || this.deviceType == MaxCulDevice.WALL_THERMOSTAT || this.deviceType == MaxCulDevice.RADIATOR_THERMOSTAT_PLUS)) { // send temps for comfort/eco etc messageHandler.sendConfigTemperatures(devAddr, this, config.getComfortTemp(), config.getEcoTemp(), config.getMaxTemp(), config.getMinTemp(), config.getMeasurementOffset(), config.getWindowOpenTemperature(), config.getWindowOpenDuration()); state = PairingInitialisationState.CONFIG_TEMPS_ACKED; } else { logger.error("SET_GROUP_ID was nacked. Ending sequence"); state = PairingInitialisationState.FINISHED; } } else { logger.error("Received " + msg.msgType + " when expecting ACK"); } break; case CONFIG_TEMPS_ACKED: if (msg.msgType == MaxCulMsgType.ACK) { AckMsg ack = new AckMsg(msg.rawMsg); if (!ack.getIsNack()) { /* * associate device with us so we get updates - we pretend * to be the MAX! Cube */ messageHandler.sendAddLinkPartner(devAddr, this, msg.dstAddrStr, MaxCulDevice.CUBE); /* * if there are more associations to come then set up * iterator and goto state to transmit more associations */ if (associations != null && associations.isEmpty() == false) { assocIter = associations.iterator(); state = PairingInitialisationState.SENDING_ASSOCIATIONS; } else { logger.debug("No user configured associations"); state = PairingInitialisationState.SENDING_ASSOCIATIONS_ACKED; } } else { logger.error("CONFIG_TEMPERATURES was nacked. Ending sequence"); state = PairingInitialisationState.FINISHED; } } else { logger.error("Received " + msg.msgType + " when expecting ACK"); } break; case SENDING_ASSOCIATIONS: if (msg.msgType == MaxCulMsgType.ACK) { AckMsg ack = new AckMsg(msg.rawMsg); if (!ack.getIsNack()) { if (assocIter.hasNext()) /* * this should always be true, but * good to check */ { MaxCulBindingConfig partnerCfg = assocIter.next(); messageHandler.sendAddLinkPartner(this.devAddr, this, partnerCfg.getDevAddr(), partnerCfg.getDeviceType()); /* * if it's the last association message then wait for * last ACK */ if (assocIter.hasNext()) { state = PairingInitialisationState.SENDING_ASSOCIATIONS; } else { state = PairingInitialisationState.SENDING_ASSOCIATIONS_ACKED; } } else { // TODO NOTE: if further states are added then ensure // you go to the right state. I.e. when all associations // are done state = PairingInitialisationState.FINISHED; } } else { logger.error("SENDING_ASSOCIATIONS was nacked. Ending sequence"); state = PairingInitialisationState.FINISHED; } } else { logger.error("Received " + msg.msgType + " when expecting ACK"); } break; case SENDING_ASSOCIATIONS_ACKED: state = PairingInitialisationState.FINISHED; break; case SENDING_WEEK_PROFILE: // TODO implement this - but where to get a week profile from. // Meaningless at the moment! state = PairingInitialisationState.FINISHED; break; case FINISHED: /* done, do nothing */ break; case RETX_WAKEUP_ACK: /* here are waiting for an ACK after sending a wakeup message */ if (msg.msgType == MaxCulMsgType.ACK) { AckMsg ack = new AckMsg(msg.rawMsg); if (!ack.getIsNack()) { logger.debug("Attempt retransmission - resuming"); this.useFast = true; messageHandler.sendMessage(reTxMsg); state = reTxState; // resume back to previous state } else { logger.error("WAKEUP for ReTx was nacked. Ending sequence"); state = PairingInitialisationState.FINISHED; } } else { logger.error("Received " + msg.msgType + " when expecting ACK"); } break; default: logger.error("Invalid state for PairingInitialisation Message Sequence!"); break; } } @Override public boolean isComplete() { return state == PairingInitialisationState.FINISHED; } @Override public void packetLost(BaseMsg msg) { pktLostCount++; logger.debug("Lost " + pktLostCount + " packets"); if (pktLostCount < 3) { /* send WAKEUP to allow us to send messages in fast mode */ logger.debug("Attempt retransmission - first wakeup device"); this.useFast = false; messageHandler.sendWakeup(msg.dstAddrStr, this); /* save current state, but avoid overwriting on second attempt */ if (this.state != PairingInitialisationState.RETX_WAKEUP_ACK) { this.reTxMsg = msg; this.reTxState = state; } state = PairingInitialisationState.RETX_WAKEUP_ACK; } else { logger.error("Lost " + pktLostCount + " packets. Ending Sequence in state " + this.state); state = PairingInitialisationState.FINISHED; } } @Override public boolean useFastSend() { // only use fast send when not sending the wakup msg in PONG_ACKED return (useFast); } }