/** * 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; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.apache.commons.lang.StringUtils; import org.openhab.binding.maxcul.MaxCulBindingProvider; import org.openhab.binding.maxcul.internal.message.sequencers.PairingInitialisationSequence; import org.openhab.binding.maxcul.internal.message.sequencers.TimeUpdateRequestSequence; import org.openhab.binding.maxcul.internal.messages.BaseMsg; import org.openhab.binding.maxcul.internal.messages.MaxCulBindingMessageProcessor; import org.openhab.binding.maxcul.internal.messages.MaxCulMsgType; import org.openhab.binding.maxcul.internal.messages.PairPingMsg; import org.openhab.binding.maxcul.internal.messages.PushButtonMsg; import org.openhab.binding.maxcul.internal.messages.PushButtonMsg.PushButtonMode; import org.openhab.binding.maxcul.internal.messages.SetTemperatureMsg; import org.openhab.binding.maxcul.internal.messages.ShutterContactStateMsg; import org.openhab.binding.maxcul.internal.messages.ShutterContactStateMsg.ShutterContactState; import org.openhab.binding.maxcul.internal.messages.ThermostatStateMsg; import org.openhab.binding.maxcul.internal.messages.TimeInfoMsg; import org.openhab.binding.maxcul.internal.messages.WallThermostatControlMsg; import org.openhab.binding.maxcul.internal.messages.WallThermostatStateMsg; import org.openhab.core.binding.AbstractBinding; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.types.Command; import org.openhab.io.transport.cul.CULCommunicationException; import org.openhab.io.transport.cul.CULHandler; import org.openhab.io.transport.cul.CULLifecycleListener; import org.openhab.io.transport.cul.CULLifecycleManager; import org.openhab.io.transport.cul.CULMode; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This binding allows integration of the MAX! devices via the CUL device - so * without the need for the Max!Cube device. * * @author Paul Hampson (cyclingengineer) * @since 1.6.0 */ public class MaxCulBinding extends AbstractBinding<MaxCulBindingProvider> implements ManagedService, MaxCulBindingMessageProcessor { private static final Logger logger = LoggerFactory.getLogger(MaxCulBinding.class); /** * This provides access to the CULFW device (e.g. USB stick) */ private final CULLifecycleManager culHandlerLifecycle; /** * This sets the address of the controller i.e. us! */ private final String srcAddr = "010203"; /** * Set default group ID */ private final byte DEFAULT_GROUP_ID = 0x1; /** * Flag to indicate if we are in pairing mode. Default timeout is 60 * seconds. */ private boolean pairMode = false; private int pairModeTimeout = 60000; private int PACED_TRANSMIT_TIME = 10000; private Map<String, Timer> timers = new HashMap<String, Timer>(); private Map<MaxCulBindingConfig, Timer> pacedBindingTransmitTimers = new HashMap<MaxCulBindingConfig, Timer>(); MaxCulMsgHandler messageHandler; private String tzStr; public MaxCulBinding() { culHandlerLifecycle = new CULLifecycleManager(CULMode.MAX, new CULLifecycleListener() { @Override public void open(CULHandler cul) throws CULCommunicationException { messageHandler = new MaxCulMsgHandler(srcAddr, cul, providers); messageHandler.registerMaxCulBindingMessageProcessor(MaxCulBinding.this); } @Override public void close(CULHandler cul) { cul.unregisterListener(messageHandler); } }); } @Override public void activate() { logger.debug("Activating MaxCul binding"); culHandlerLifecycle.open(); } @Override public void deactivate() { logger.debug("De-Activating MaxCul binding"); culHandlerLifecycle.close(); } /** * @{inheritDoc */ @Override protected void internalReceiveCommand(final String itemName, Command command) { Timer pairModeTimer = null; MaxCulBindingConfig bindingConfig = null; for (MaxCulBindingProvider provider : super.providers) { bindingConfig = provider.getConfigForItemName(itemName); if (bindingConfig != null) { break; } } logger.debug("Received command " + command.toString() + " for item " + itemName); if (bindingConfig != null) { logger.debug("Found config for " + itemName); switch (bindingConfig.getDeviceType()) { case PAIR_MODE: if ((command instanceof OnOffType)) { switch ((OnOffType) command) { case ON: /* * turn on pair mode and schedule disabling of pairing * mode */ pairMode = true; TimerTask task = new TimerTask() { @Override public void run() { logger.debug(itemName + " pairMode timeout executed"); pairMode = false; eventPublisher.postUpdate(itemName, OnOffType.OFF); } }; pairModeTimer = timers.get(itemName); if (pairModeTimer != null) { pairModeTimer.cancel(); timers.remove(itemName); } pairModeTimer = new Timer(); timers.put(itemName, pairModeTimer); pairModeTimer.schedule(task, pairModeTimeout); logger.debug(itemName + " pairMode enabled & timeout scheduled"); break; case OFF: /* * we are manually disabling, so clear the timer and the * flag */ pairMode = false; pairModeTimer = timers.get(itemName); if (pairModeTimer != null) { logger.debug(itemName + " pairMode timer cancelled"); pairModeTimer.cancel(); timers.remove(itemName); } logger.debug(itemName + " pairMode cleared"); break; } } else { logger.warn( "Command not handled for " + bindingConfig.getDeviceType() + " that is not OnOffType"); } break; case LISTEN_MODE: if (command instanceof OnOffType) { this.messageHandler.setListenMode(((OnOffType) command == OnOffType.ON)); } else { logger.warn( "Command not handled for " + bindingConfig.getDeviceType() + " that is not OnOffType"); } break; case LED_MODE: if (command instanceof OnOffType) { this.messageHandler.setLedMode(((OnOffType) command == OnOffType.ON)); } else { logger.warn( "Command not handled for " + bindingConfig.getDeviceType() + " that is not OnOffType"); } break; case RADIATOR_THERMOSTAT: case RADIATOR_THERMOSTAT_PLUS: case WALL_THERMOSTAT: if (bindingConfig.getFeature() == MaxCulFeature.THERMOSTAT) { /* clear out old pacing timer */ if (pacedBindingTransmitTimers.containsKey(bindingConfig)) { pacedBindingTransmitTimers.get(bindingConfig).cancel(); pacedBindingTransmitTimers.remove(bindingConfig); } /* schedule new timer */ Timer pacingTimer = new Timer(); pacedBindingTransmitTimers.put(bindingConfig, pacingTimer); pacingTimer.schedule(new MaxCulPacedThermostatTransmitTask(command, bindingConfig, messageHandler, super.providers), PACED_TRANSMIT_TIME); } else if (bindingConfig.getFeature() == MaxCulFeature.DISPLAYSETTING) { messageHandler.sendSetDisplayActualTemp(bindingConfig.getDevAddr(), ((OnOffType) command == OnOffType.ON)); } else if (bindingConfig.getFeature() == MaxCulFeature.RESET) { messageHandler.sendReset(bindingConfig.getDevAddr()); } else { logger.warn("Command not handled for " + bindingConfig.getDeviceType() + " that is not OnOffType or DecimalType"); } break; case SHUTTER_CONTACT: if (bindingConfig.getFeature() == MaxCulFeature.RESET) { messageHandler.sendReset(bindingConfig.getDevAddr()); } break; default: logger.warn("Command not handled for " + bindingConfig.getDeviceType()); break; } } updateCreditMonitors(); } protected void addBindingProvider(MaxCulBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(MaxCulBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override public void updated(Dictionary<String, ?> config) throws ConfigurationException { logger.debug("MaxCUL Reading config"); if (config != null) { // handle timezone configuration // maxcul:timezone=Europe/London String timezoneString = (String) config.get("timezone"); if (StringUtils.isNotBlank(timezoneString)) { this.tzStr = timezoneString; } else { this.tzStr = "Europe/London"; } culHandlerLifecycle.config(config); } } private Collection<MaxCulBindingConfig> getBindingsBySerial(String serial) { Collection<MaxCulBindingConfig> bindingConfigs = null; for (MaxCulBindingProvider provider : super.providers) { bindingConfigs = provider.getConfigsForSerialNumber(serial); if (bindingConfigs != null) { break; } } if (bindingConfigs == null) { logger.error("Unable to find configuration for serial " + serial + ". Do you have a binding for it?"); return null; } return bindingConfigs; } private void updateCreditMonitors() { /* find and update credit monitor binding if it exists */ int credit10ms = messageHandler.getCreditStatus(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider.getCreditMonitorBindings(); for (MaxCulBindingConfig bc : bindingConfigs) { String itemName = provider.getItemNameForConfig(bc); eventPublisher.postUpdate(itemName, new DecimalType(credit10ms)); } } } @Override public void maxCulMsgReceived(String data, boolean isBroadcast) { logger.debug("Received data from CUL: " + data); MaxCulMsgType msgType = BaseMsg.getMsgType(data); /* * Check if it's broadcast and we're in pair mode or a PAIR_PING message * directly for us */ if (((pairMode && isBroadcast) || !isBroadcast) && msgType == MaxCulMsgType.PAIR_PING) { logger.debug("Got PAIR_PING message"); MaxCulBindingConfig configWithTempsConfig = null; /* process packet */ PairPingMsg pkt = new PairPingMsg(data); /* Match serial number to binding configuration */ Collection<MaxCulBindingConfig> bindingConfigs = getBindingsBySerial(pkt.serial); /* * only respond and set pairing info if we found at least one * binding config */ if (bindingConfigs != null) { logger.debug("Found " + bindingConfigs.size() + " configs for " + pkt.serial); for (MaxCulBindingConfig bc : bindingConfigs) { /* Set pairing information */ bc.setPairedInfo(pkt.srcAddrStr); /* * where it came from gives * the addr of the device */ if (bc.isTemperatureConfigSet() && configWithTempsConfig == null) { configWithTempsConfig = bc; } } /* if none have values set then send default from first config */ if (configWithTempsConfig == null) { logger.debug("Using default temperature configuration from config 0"); configWithTempsConfig = (MaxCulBindingConfig) bindingConfigs.toArray()[0]; } /* get device associations */ HashSet<MaxCulBindingConfig> associations = null; for (MaxCulBindingProvider provider : super.providers) { associations = provider.getAssociations(configWithTempsConfig.getSerialNumber()); if (associations != null && associations.isEmpty() == false) { logger.debug("Found associations"); break; } } /* start the initialisation sequence */ logger.debug("Creating pairing sequencer"); PairingInitialisationSequence ps = new PairingInitialisationSequence(this.DEFAULT_GROUP_ID, messageHandler, configWithTempsConfig, associations); messageHandler.startSequence(ps, pkt); } else { logger.error("Pairing failed: Unable to find binding config for device " + pkt.serial); } } else { switch (msgType) { /* * TODO there are other incoming messages that aren't handled that * could be */ case WALL_THERMOSTAT_CONTROL: WallThermostatControlMsg wallThermCtrlMsg = new WallThermostatControlMsg(data); wallThermCtrlMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(wallThermCtrlMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { if (bc.getFeature() == MaxCulFeature.THERMOSTAT && wallThermCtrlMsg.getDesiredTemperature() != null) { String itemName = provider.getItemNameForConfig(bc); eventPublisher.postUpdate(itemName, new DecimalType(wallThermCtrlMsg.getDesiredTemperature())); } else if (bc.getFeature() == MaxCulFeature.TEMPERATURE && wallThermCtrlMsg.getMeasuredTemperature() != null) { String itemName = provider.getItemNameForConfig(bc); eventPublisher.postUpdate(itemName, new DecimalType(wallThermCtrlMsg.getMeasuredTemperature())); } // TODO switch mode between manual/automatic? } } /* reply only if not broadcast */ if (isBroadcast == false) { this.messageHandler.sendAck(wallThermCtrlMsg); } break; case SET_TEMPERATURE: SetTemperatureMsg setTempMsg = new SetTemperatureMsg(data); setTempMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(setTempMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { if (bc.getFeature() == MaxCulFeature.THERMOSTAT) { String itemName = provider.getItemNameForConfig(bc); eventPublisher.postUpdate(itemName, new DecimalType(setTempMsg.getDesiredTemperature())); } // TODO switch mode between manual/automatic? } } /* respond to device */ if (isBroadcast == false) { this.messageHandler.sendAck(setTempMsg); } break; case THERMOSTAT_STATE: ThermostatStateMsg thermStateMsg = new ThermostatStateMsg(data); thermStateMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(thermStateMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { String itemName = provider.getItemNameForConfig(bc); if (bc.getFeature() == MaxCulFeature.THERMOSTAT && thermStateMsg.getDesiredTemperature() != null) { eventPublisher.postUpdate(itemName, new DecimalType(thermStateMsg.getDesiredTemperature())); } else if (bc.getFeature() == MaxCulFeature.TEMPERATURE && thermStateMsg.getMeasuredTemperature() != null) { eventPublisher.postUpdate(itemName, new DecimalType(thermStateMsg.getMeasuredTemperature())); } else if (bc.getFeature() == MaxCulFeature.BATTERY) { eventPublisher.postUpdate(itemName, thermStateMsg.getBatteryLow() ? OnOffType.ON : OnOffType.OFF); } else if (bc.getFeature() == MaxCulFeature.MODE) { eventPublisher.postUpdate(itemName, new DecimalType(thermStateMsg.getControlMode().toInt())); } else if (bc.getFeature() == MaxCulFeature.VALVE_POS) { eventPublisher.postUpdate(itemName, new DecimalType(thermStateMsg.getValvePos())); } // TODO switch mode between manual/automatic? } } /* respond to device */ if (isBroadcast == false) { this.messageHandler.sendAck(thermStateMsg); } break; case WALL_THERMOSTAT_STATE: WallThermostatStateMsg wallThermStateMsg = new WallThermostatStateMsg(data); wallThermStateMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(wallThermStateMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { String itemName = provider.getItemNameForConfig(bc); if (bc.getFeature() == MaxCulFeature.THERMOSTAT && wallThermStateMsg.getDesiredTemperature() != null) { eventPublisher.postUpdate(itemName, new DecimalType(wallThermStateMsg.getDesiredTemperature())); } else if (bc.getFeature() == MaxCulFeature.TEMPERATURE && wallThermStateMsg.getMeasuredTemperature() != null) { eventPublisher.postUpdate(itemName, new DecimalType(wallThermStateMsg.getMeasuredTemperature())); } else if (bc.getFeature() == MaxCulFeature.BATTERY) { eventPublisher.postUpdate(itemName, wallThermStateMsg.getBatteryLow() ? OnOffType.ON : OnOffType.OFF); } else if (bc.getFeature() == MaxCulFeature.MODE) { eventPublisher.postUpdate(itemName, new DecimalType(wallThermStateMsg.getControlMode().toInt())); } } } /* respond to device */ if (isBroadcast == false) { this.messageHandler.sendAck(wallThermStateMsg); } break; case TIME_INFO: TimeInfoMsg timeMsg = new TimeInfoMsg(data); timeMsg.printMessage(); TimeUpdateRequestSequence timeSeq = new TimeUpdateRequestSequence(this.tzStr, messageHandler); messageHandler.startSequence(timeSeq, timeMsg); break; case PUSH_BUTTON_STATE: PushButtonMsg pbMsg = new PushButtonMsg(data); pbMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(pbMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { String itemName = provider.getItemNameForConfig(bc); if (bc.getFeature() == MaxCulFeature.SWITCH) { // 'AUTO' maps to 'ON' and 'ECO' maps to 'OFF' eventPublisher.postUpdate(itemName, pbMsg.getMode() == PushButtonMode.AUTO ? OnOffType.ON : OnOffType.OFF); } else if (bc.getFeature() == MaxCulFeature.BATTERY) { eventPublisher.postUpdate(itemName, pbMsg.getBatteryLow() ? OnOffType.ON : OnOffType.OFF); } } } if (isBroadcast == false) { this.messageHandler.sendAck(pbMsg); } break; case SHUTTER_CONTACT_STATE: ShutterContactStateMsg shutterContactStateMsg = new ShutterContactStateMsg(data); shutterContactStateMsg.printMessage(); for (MaxCulBindingProvider provider : super.providers) { Collection<MaxCulBindingConfig> bindingConfigs = provider .getConfigsForRadioAddr(shutterContactStateMsg.srcAddrStr); for (MaxCulBindingConfig bc : bindingConfigs) { String itemName = provider.getItemNameForConfig(bc); if (bc.getFeature() == MaxCulFeature.CONTACT) { if (shutterContactStateMsg.getState() == ShutterContactState.CLOSED) { eventPublisher.postUpdate(itemName, OpenClosedType.CLOSED); } else if (shutterContactStateMsg.getState() == ShutterContactState.OPEN) { eventPublisher.postUpdate(itemName, OpenClosedType.OPEN); } } else if (bc.getFeature() == MaxCulFeature.BATTERY) { eventPublisher.postUpdate(itemName, shutterContactStateMsg.getBatteryLow() ? OnOffType.ON : OnOffType.OFF); } } } /* respond to device */ if (isBroadcast == false) { this.messageHandler.sendAck(shutterContactStateMsg); } break; default: logger.debug("Unhandled message type " + msgType.toString()); break; } } updateCreditMonitors(); } }