/* ================================================================== * RFXCOMTransceiver.java - Jul 9, 2012 12:05:16 PM * * Copyright 2007-2012 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== * $Id$ * ================================================================== */ package net.solarnetwork.node.rfxcom; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import net.solarnetwork.node.ConversationalDataCollector; import net.solarnetwork.node.DataCollectorFactory; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier; import net.solarnetwork.node.settings.support.BasicToggleSettingSpecifier; import net.solarnetwork.node.support.SerialPortBeanParameters; import net.solarnetwork.node.util.PrefixedMessageSource; import net.solarnetwork.util.DynamicServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; /** * {@link SettingSpecifierProvider} for RFXCOM transceiver, allowing * for the configuration of the transceiver via settings. * * @author matt * @version $Revision$ */ public class RFXCOMTransceiver implements RFXCOM, SettingSpecifierProvider { private static final SerialPortBeanParameters DEFAULT_SERIAL_PARAMS = new SerialPortBeanParameters(); static { DEFAULT_SERIAL_PARAMS.setBaud(38400); DEFAULT_SERIAL_PARAMS.setDataBits(8); DEFAULT_SERIAL_PARAMS.setStopBits(1); DEFAULT_SERIAL_PARAMS.setParity(0); DEFAULT_SERIAL_PARAMS.setDtrFlag(1); DEFAULT_SERIAL_PARAMS.setRtsFlag(1); DEFAULT_SERIAL_PARAMS.setReceiveThreshold(-1); DEFAULT_SERIAL_PARAMS.setReceiveTimeout(60000); DEFAULT_SERIAL_PARAMS.setMaxWait(65000); } private static final Object MONITOR = new Object(); private static MessageSource MESSAGE_SOURCE; private DynamicServiceTracker<DataCollectorFactory<SerialPortBeanParameters>> dataCollectorFactory; private SerialPortBeanParameters serialParams = getDefaultSerialParameters(); private final MessageFactory mf = new MessageFactory(); private StatusMessage status = null; private final Logger log = LoggerFactory.getLogger(getClass()); /** * Get the default serial parameters used for RFXCOM transceivers. * @return */ public static final SerialPortBeanParameters getDefaultSerialParameters() { return (SerialPortBeanParameters)DEFAULT_SERIAL_PARAMS.clone(); } @Override public String getUID() { return (dataCollectorFactory == null ? null : dataCollectorFactory.getPropertyFilters() == null ? null : (String)dataCollectorFactory.getPropertyFilters().get("UID")); } @Override public ConversationalDataCollector getDataCollectorInstance() { final DataCollectorFactory<SerialPortBeanParameters> df = getDataCollectorFactory().service(); if ( df == null ) { return null; } ConversationalDataCollector dc = df.getConversationalDataCollectorInstance(getSerialParams()); if ( status == null ) { status = dc.collectData(new ConversationalDataCollector.Moderator<StatusMessage>() { @Override public StatusMessage conductConversation(ConversationalDataCollector dataCollector) { return getStatus(dataCollector); } }); } return dc; } @Override public String getSettingUID() { return "net.solarnetwork.node.rfxcom"; } @Override public String getDisplayName() { return "RFXCOM transceiver"; } @Override public MessageSource getMessageSource() { synchronized (MONITOR) { if ( MESSAGE_SOURCE == null ) { ResourceBundleMessageSource serial = new ResourceBundleMessageSource(); serial.setBundleClassLoader(SerialPortBeanParameters.class.getClassLoader()); serial.setBasename(SerialPortBeanParameters.class.getName()); PrefixedMessageSource serialSource = new PrefixedMessageSource(); serialSource.setDelegate(serial); serialSource.setPrefix("serialParams."); ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBundleClassLoader(RFXCOMTransceiver.class.getClassLoader()); source.setBasename(RFXCOMTransceiver.class.getName()); source.setParentMessageSource(serialSource); MESSAGE_SOURCE = source; } } return MESSAGE_SOURCE; } private void addToggleSetting(List<SettingSpecifier> results, PropertyAccessor bean, String name) { results.add(new BasicToggleSettingSpecifier(name, (bean == null ? Boolean.FALSE : bean.getPropertyValue(name)), true)); } @Override public List<SettingSpecifier> getSettingSpecifiers() { List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20); results.add(new BasicTextFieldSettingSpecifier( "dataCollectorFactory.propertyFilters['UID']", "/dev/ttyUSB0")); if ( status == null ) { try { updateStatus(); } catch ( RuntimeException e ) { log.warn("Unable to update RFXCOM status", e.getCause()); } } if ( status != null ) { log.debug("RFXCOM status: firmware {}, product {}, Oregon {}", new Object[] { status.getFirmwareVersion(), status.getTransceiverType().getDescription(), status.isOregonEnabled() }); results.add(new BasicTitleSettingSpecifier("firmwareVersion", (status == null ? "N/A" : String.valueOf(status.getFirmwareVersion())), true)); results.add(new BasicTitleSettingSpecifier("transceiverType", (status == null ? "N/A" : status.getTransceiverType().getDescription()), true)); PropertyAccessor bean = (status == null ? null : PropertyAccessorFactory.forBeanPropertyAccess(status)); addToggleSetting(results, bean, "ACEnabled"); addToggleSetting(results, bean, "ADEnabled"); addToggleSetting(results, bean, "ARCEnabled"); addToggleSetting(results, bean, "ATIEnabled"); addToggleSetting(results, bean, "FS20Enabled"); addToggleSetting(results, bean, "hidekiEnabled"); addToggleSetting(results, bean, "homeEasyEUEnabled"); addToggleSetting(results, bean, "ikeaKopplaEnabled"); addToggleSetting(results, bean, "laCrosseEnabled"); addToggleSetting(results, bean, "mertikEnabled"); addToggleSetting(results, bean, "oregonEnabled"); addToggleSetting(results, bean, "proGuardEnabled"); addToggleSetting(results, bean, "visonicEnabled"); addToggleSetting(results, bean, "x10Enabled"); addToggleSetting(results, bean, "undecodedMode"); } results.addAll(SerialPortBeanParameters.getDefaultSettingSpecifiers( RFXCOMTransceiver.getDefaultSerialParameters(), "serialParams.")); return results; } public void updateModeSetting(String name, Object value) { if ( this.status == null ) { updateStatus(); } if ( this.status != null ) { SetModeMessage msg = new SetModeMessage(mf.incrementAndGetSequenceNumber(), this.status); PropertyAccessor bean = PropertyAccessorFactory.forBeanPropertyAccess(msg); Object currValue = bean.getPropertyValue(name); if ( value != null && !value.equals(currValue) ) { bean.setPropertyValue(name, value); setMode(msg); } } } /** * Update the settings of this class. * * <p>This method is designed to work with Spring's bean-managed OSGi Configuration * Admin service, rather than the container-managed approach of setting properties * directly. This is because many of the supported properties require communicating * with the RFXCOM device, but those can all be set via a single call. Thus the * supported properties of this method are those properties directly available on * this class itself, and those available on the {@link SetModeMessage} class. * * @param properties the properties to change */ public void updateConfiguration(Map<String, ?> properties) { Map<String, Object> setModeProperties = new HashMap<String, Object>(properties); PropertyAccessor bean = PropertyAccessorFactory.forBeanPropertyAccess(this); // if this is NOT something that must be handled via a SetMode command, apply those directly... for ( Map.Entry<String, ?> me : properties.entrySet() ) { if ( bean.isWritableProperty(me.getKey()) ) { bean.setPropertyValue(me.getKey(), me.getValue()); } else { setModeProperties.put(me.getKey(), me.getValue()); } } // and now apply remaining properties via single SetMode, so we only have to talk to // device one time if ( this.status == null ) { updateStatus(); } if ( this.status != null ) { SetModeMessage msg = new SetModeMessage(mf.incrementAndGetSequenceNumber(), this.status); bean = PropertyAccessorFactory.forBeanPropertyAccess(msg); boolean changed = false; for ( Map.Entry<String, Object> me : setModeProperties.entrySet() ) { if ( bean.isReadableProperty(me.getKey()) ) { Object currValue = bean.getPropertyValue(me.getKey()); if ( me.getValue() != null && me.getValue().equals(currValue) ) { continue; } } if ( bean.isWritableProperty(me.getKey()) ) { bean.setPropertyValue(me.getKey(), me.getValue()); changed = true; } } if ( changed ) { log.debug("Updating RFXCOM settings to {}", msg); setMode(msg); } } } private void updateStatus() { final ConversationalDataCollector dc = getDataCollectorInstance(); if ( dc == null ) { return; } try { status = dc.collectData(new ConversationalDataCollector.Moderator<StatusMessage>() { @Override public StatusMessage conductConversation(ConversationalDataCollector dataCollector) { return getStatus(dataCollector); } }); } finally { dc.stopCollecting(); } } private void setMode(final SetModeMessage msg) { final MessageListener listener = new MessageListener(); ConversationalDataCollector dc = null; try { dc = getDataCollectorInstance(); if ( dc == null ) { return; } StatusMessage result = dc.collectData(new ConversationalDataCollector.Moderator<StatusMessage>() { @Override public StatusMessage conductConversation(ConversationalDataCollector dc) { dc.speakAndListen(msg.getMessagePacket(), listener); Message msg = mf.parseMessage(dc.getCollectedData(), 0); StatusMessage result = null; if ( msg instanceof StatusMessage ) { result = (StatusMessage)msg; } return result; } }); if ( result != null ) { if ( log.isDebugEnabled() ) { log.debug("RFXCOM status: firmware {}, product {}, Oregon {}", new Object[] { status.getFirmwareVersion(), status.getTransceiverType().getDescription(), status.isOregonEnabled() }); } status = result; } } finally { if ( dc != null ) { dc.stopCollecting(); } } } private StatusMessage getStatus(ConversationalDataCollector dc) { final MessageListener listener = new MessageListener(); // send reset, followed by status to see how rfxcom is configured dc.speak(new CommandMessage(Command.Reset).getMessagePacket()); // wait at least 50ms try { Thread.sleep(100); } catch ( InterruptedException e ) { // ignore } dc.speakAndListen(new CommandMessage(Command.Status, mf.incrementAndGetSequenceNumber()).getMessagePacket(), listener); Message msg = mf.parseMessage(dc.getCollectedData(), 0); StatusMessage result = null; if ( msg instanceof StatusMessage ) { result = (StatusMessage)msg; if ( log.isDebugEnabled() ) { log.debug("RFXCOM status: firmware {}, product {}, Oregon {}", new Object[] { result.getFirmwareVersion(), result.getTransceiverType().getDescription(), result.isOregonEnabled() }); } } return result; } public DynamicServiceTracker<DataCollectorFactory<SerialPortBeanParameters>> getDataCollectorFactory() { return dataCollectorFactory; } public void setDataCollectorFactory( DynamicServiceTracker<DataCollectorFactory<SerialPortBeanParameters>> dataCollectorFactory) { this.dataCollectorFactory = dataCollectorFactory; } public SerialPortBeanParameters getSerialParams() { return serialParams; } public void setSerialParams(SerialPortBeanParameters serialParams) { this.serialParams = serialParams; } }