/* =================================================================== * CentameterConsumptionDatumDataSource.java * * Created Jul 23, 2009 9:18:41 AM * * Copyright (c) 2009 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 * =================================================================== */ package net.solarnetwork.node.consumption.impl; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import net.solarnetwork.node.DataCollector; import net.solarnetwork.node.DataCollectorFactory; import net.solarnetwork.node.DatumDataSource; import net.solarnetwork.node.MultiDatumDataSource; import net.solarnetwork.node.centameter.CentameterDatum; import net.solarnetwork.node.centameter.CentameterSupport; import net.solarnetwork.node.centameter.CentameterUtils; import net.solarnetwork.node.consumption.ConsumptionDatum; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.support.DataCollectorSerialPortBeanParameters; import net.solarnetwork.node.util.DataUtils; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; /** * {@link ConsumptionDataSource} implementation for Cent-a-meter monitors, * reading data via a serial port. * * <p> * This implementation relies on a device that can listen to the radio signal * broadcast by a Cent-a-meter monitor and write that data to a local serial * port. This class will read the Cent-a-meter data from the serial port to * generate consumption data. * </p> * * <p> * It assumes the {@link DataCollector} implementation blocks until appropriate * data is available when the {@link DataCollector#collectData()} method is * called. * </p> * * <p> * Serial parameters that are known to work are: * </p> * * <pre> * magicBytes = x (0x78) * baud = 4800 * bufferSize = 16 * readSize = 15 * receiveThreshold = -1 * maxWait = 60000 * </pre> * * @author matt * @version 1.1 */ public class CentameterConsumptionDatumDataSource extends CentameterSupport implements DatumDataSource<ConsumptionDatum>, MultiDatumDataSource<ConsumptionDatum>, SettingSpecifierProvider { private static final Object MONITOR = new Object(); private static MessageSource MESSAGE_SOURCE; @Override public Class<? extends ConsumptionDatum> getDatumType() { return ConsumptionDatum.class; } @Override public ConsumptionDatum readCurrentDatum() { DataCollectorFactory<DataCollectorSerialPortBeanParameters> df = getDataCollectorFactory() .service(); if ( df == null ) { log.debug("No DataCollectorFactory available"); return null; } byte[] data = null; DataCollector dc = df.getDataCollectorInstance(getSerialParams()); try { dc.collectData(); data = dc.getCollectedData(); } finally { if ( dc != null ) { dc.stopCollecting(); } } if ( data == null || data.length == 0 ) { log.warn("No serial data received, serial communications problem"); return null; } return getConsumptionDatumInstance(DataUtils.getUnsignedValues(data), getAmpSensorIndex()); } @Override public Class<? extends ConsumptionDatum> getMultiDatumType() { return ConsumptionDatum.class; } @Override public Collection<ConsumptionDatum> readMultipleDatum() { DataCollectorFactory<DataCollectorSerialPortBeanParameters> df = getDataCollectorFactory() .service(); if ( df == null ) { return null; } List<ConsumptionDatum> result = new ArrayList<ConsumptionDatum>(3); long endTime = isCollectAllSourceIds() && getSourceIdFilter() != null && getSourceIdFilter().size() > 1 ? System.currentTimeMillis() + (getCollectAllSourceIdsTimeout() * 1000) : 0; Set<String> sourceIdSet = new HashSet<String>(getSourceIdFilter() == null ? 0 : getSourceIdFilter().size()); DataCollector dc = null; try { dc = df.getDataCollectorInstance(getSerialParams()); do { dc.collectData(); byte[] data = dc.getCollectedData(); if ( data == null ) { log.warn("Null serial data received, serial communications problem"); return null; } short[] unsigned = DataUtils.getUnsignedValues(data); // add a known address for this reading addKnownAddress(new CentameterDatum( String.format("%X", unsigned[CENTAMETER_ADDRESS_IDX]), (float) CentameterUtils.getAmpReading(unsigned, 1), (float) CentameterUtils.getAmpReading(unsigned, 2), (float) CentameterUtils.getAmpReading(unsigned, 3))); if ( log.isDebugEnabled() ) { log.debug(String.format( "Centameter address %X, count %d, amp1 %.1f, amp2 %.1f, amp3 %.1f", unsigned[CENTAMETER_ADDRESS_IDX], (unsigned[1] & 0xF), CentameterUtils.getAmpReading(unsigned, 1), CentameterUtils.getAmpReading(unsigned, 2), CentameterUtils.getAmpReading(unsigned, 3))); } for ( int ampIndex = 1; ampIndex <= 3; ampIndex++ ) { ConsumptionDatum datum = getConsumptionDatumInstance(unsigned, ampIndex); if ( (ampIndex & getMultiAmpSensorIndexFlags()) != ampIndex ) { continue; } if ( datum != null ) { if ( !sourceIdSet.contains(datum.getSourceId()) ) { result.add(datum); sourceIdSet.add(datum.getSourceId()); } } } } while ( System.currentTimeMillis() < endTime && sourceIdSet.size() < (getSourceIdFilter() == null ? 0 : getSourceIdFilter() .size()) ); } finally { if ( dc != null ) { dc.stopCollecting(); } } return result.size() < 1 ? null : result; } private ConsumptionDatum getConsumptionDatumInstance(short[] unsigned, int ampIndex) { // report the Centameter address as upper-case hex value String addr = String.format(getSourceIdFormat(), unsigned[CENTAMETER_ADDRESS_IDX], ampIndex); float amps = (float) CentameterUtils.getAmpReading(unsigned, ampIndex); if ( getAddressSourceMapping() != null && getAddressSourceMapping().containsKey(addr) ) { addr = getAddressSourceMapping().get(addr); } if ( getSourceIdFilter() != null && !getSourceIdFilter().contains(addr) ) { if ( log.isInfoEnabled() ) { log.info("Rejecting source [" + addr + "] not in source ID filter set"); } return null; } ConsumptionDatum datum = new ConsumptionDatum(addr, amps, getVoltage()); datum.setCreated(new Date()); return datum; } @Override public String getSettingUID() { return "net.solarnetwork.node.consumption.centameter"; } @Override public String getDisplayName() { return "Cent-a-meter consumption meter"; } @Override public MessageSource getMessageSource() { synchronized ( MONITOR ) { if ( MESSAGE_SOURCE == null ) { MessageSource parent = getDefaultSettingsMessageSource(); ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBundleClassLoader(CentameterConsumptionDatumDataSource.class.getClassLoader()); source.setBasename(CentameterConsumptionDatumDataSource.class.getName()); source.setParentMessageSource(parent); MESSAGE_SOURCE = source; } } return MESSAGE_SOURCE; } @Override public List<SettingSpecifier> getSettingSpecifiers() { return getDefaultSettingSpecifiers(); } }