/* ==================================================================
* CentameterPowerDatumDataSource.java - Apr 25, 2010 12:57:01 PM
*
* Copyright 2007-2010 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.power.impl.centameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.power.PowerDatum;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.support.DataCollectorSerialPortBeanParameters;
import net.solarnetwork.node.util.ClassUtils;
import net.solarnetwork.node.util.DataUtils;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
/**
* Implementation of {@link DatumDataSource} {@link PowerDatum} objects, using a
* Centameter amp sensor.
*
* <p>
* Normally Centameters are used to monitor consumption, but in some situations
* they can be used as a low-cost montior for generation, especially if the
* generation device cannot be communicated with.
* </p>
*
* <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>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>ampsFieldName</dt>
* <dd>The bean property on {@link PowerDatum} to set the amp reading value
* collected from the Centameter. Defaults to {@link #DEFAULT_AMPS_FIELD_NAME}.</dd>
*
* <dt>voltsFieldName</dt>
* <dd>The bean property on {@link PowerDatum} to set the {@code voltage} value.
* Defaults to {@link #DEFAULT_AMPS_FIELD_NAME}.</dd>
* </dl>
*
* @author matt
* @version 1.0
*/
public class CentameterPowerDatumDataSource extends CentameterSupport implements
DatumDataSource<PowerDatum>, MultiDatumDataSource<PowerDatum>, SettingSpecifierProvider {
/** The default value for the {@code ampsFieldName} property. */
public static final String DEFAULT_AMPS_FIELD_NAME = "pvAmps";
/** The default value for the {@code voltsFieldName} property. */
public static final String DEFAULT_VOLTS_FIELD_NAME = "pvVolts";
private static final Object MONITOR = new Object();
private static MessageSource MESSAGE_SOURCE;
private String ampsFieldName = DEFAULT_AMPS_FIELD_NAME;
private String voltsFieldName = DEFAULT_VOLTS_FIELD_NAME;
@Override
public Class<? extends PowerDatum> getDatumType() {
return PowerDatum.class;
}
@Override
public PowerDatum readCurrentDatum() {
DataCollectorFactory<DataCollectorSerialPortBeanParameters> df = getDataCollectorFactory()
.service();
if ( df == null ) {
log.debug("No DataCollectorFactory available");
return null;
}
DataCollector dataCollector = df.getDataCollectorInstance(getSerialParams());
byte[] data = null;
try {
dataCollector.collectData();
data = dataCollector.getCollectedData();
} finally {
if ( dataCollector != null ) {
dataCollector.stopCollecting();
}
}
if ( data == null ) {
log.warn("Null serial data received, serial communications problem");
return null;
}
return getPowerDatumInstance(DataUtils.getUnsignedValues(data), getAmpSensorIndex());
}
@Override
public Class<? extends PowerDatum> getMultiDatumType() {
return PowerDatum.class;
}
@Override
public Collection<PowerDatum> readMultipleDatum() {
DataCollectorFactory<DataCollectorSerialPortBeanParameters> df = getDataCollectorFactory()
.service();
if ( df == null ) {
return null;
}
List<PowerDatum> result = new ArrayList<PowerDatum>(3);
long endTime = isCollectAllSourceIds() && getSourceIdFilter().size() > 1 ? System
.currentTimeMillis() + (getCollectAllSourceIdsTimeout() * 1000) : 0;
Set<String> sourceIdSet = new HashSet<String>(getSourceIdFilter().size());
DataCollector dataCollector = null;
try {
dataCollector = df.getDataCollectorInstance(getSerialParams());
do {
dataCollector.collectData();
byte[] data = dataCollector.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++ ) {
PowerDatum datum = getPowerDatumInstance(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().size() );
} finally {
if ( dataCollector != null ) {
dataCollector.stopCollecting();
}
}
return result.size() < 1 ? null : result;
}
private PowerDatum getPowerDatumInstance(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);
PowerDatum datum = new PowerDatum();
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;
}
datum.setSourceId(addr);
datum.setCreated(new Date());
Map<String, Object> props = new HashMap<String, Object>();
props.put(ampsFieldName, amps);
props.put(voltsFieldName, getVoltage());
ClassUtils.setBeanProperties(datum, props);
return datum;
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.power.centameter";
}
@Override
public String getDisplayName() {
return "Cent-a-meter power meter";
}
@Override
public MessageSource getMessageSource() {
synchronized ( MONITOR ) {
if ( MESSAGE_SOURCE == null ) {
MessageSource parent = getDefaultSettingsMessageSource();
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBundleClassLoader(CentameterPowerDatumDataSource.class.getClassLoader());
source.setBasename(CentameterPowerDatumDataSource.class.getName());
source.setParentMessageSource(parent);
MESSAGE_SOURCE = source;
}
}
return MESSAGE_SOURCE;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> results = getDefaultSettingSpecifiers();
results.add(new BasicTextFieldSettingSpecifier("ampsFieldName", DEFAULT_AMPS_FIELD_NAME));
results.add(new BasicTextFieldSettingSpecifier("voltsFieldName", DEFAULT_VOLTS_FIELD_NAME));
return results;
}
public String getAmpsFieldName() {
return ampsFieldName;
}
public void setAmpsFieldName(String ampsFieldName) {
this.ampsFieldName = ampsFieldName;
}
public String getVoltsFieldName() {
return voltsFieldName;
}
public void setVoltsFieldName(String voltsFieldName) {
this.voltsFieldName = voltsFieldName;
}
}