/* ==================================================================
* CentameterSupport.java - Sep 20, 2010 9:24:54 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.centameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;
import net.solarnetwork.node.DataCollector;
import net.solarnetwork.node.DataCollectorFactory;
import net.solarnetwork.node.settings.SettingSpecifier;
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.DataCollectorSerialPortBeanParameters;
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.context.MessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
/**
* Base class for reading Centameter sensor data.
*
* <p>
* The Centameter packet contains a "magic" byte prefix (0x78) followed by 15
* message bytes.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>dataCollectorFactory</dt>
* <dd>The factory for creating {@link DataCollector} instances with.</dd>
*
* <dt>serialParams</dt>
* <dd>The serial port parameters to use.</dd>
*
* <dt>voltage</dt>
* <dd>A hard-coded voltage value to use for the Cent-a-meter, since it only
* measures current. Defaults to {@link #DEFAULT_VOLTAGE}.</dd>
*
* <dt>ampSensorIndex</dt>
* <dd>The Cent-a-meter can report on 3 different currents. This index value is
* the desired current to read. Possible values for this property are 1, 2, or
* 3. Defaults to {@code 1}.</dd>
*
* <dt>sourceIdFormat</dt>
* <dd>A string format pattern for generating the {@code sourceId} value in
* returned {@link PowerDatum} instances. This format will be passed the
* Centameter address (as a <em>short</em>) and the Centameter amp sensor index
* (as a <em>int</em>). Defaults to {@link #DEFAULT_SOURCE_ID_FORMAT}.</dd>
*
* <dt>multiAmpSensorIndexFlags</dt>
* <dd>A bitmask flag for which amp sensor index readings to return from
* {@link #readMultipleDatum()}. The amp sensors number 1 - 3. Enable reading
* each index by adding together each index as 2 ^ (index - 1). Thus to enable
* reading from all 3 indexes set this value to <em>7</em> (2^0 + 2^1 + 2^2) =
* 7). Defaults to 7.</dd>
*
* <dt>addressSourceMapping</dt>
* <dd>If configured, a mapping of Centameter address ID values to PowerDatum
* sourceId values. This can be used to consistently collect data from
* Centameters, even after the Centameter has been reset and it generates a new
* random address ID value for itself.</dd>
*
* <dt>sourceIdFilter</dt>
* <dd>If configured, a set of PowerDatum sourceId values to accept data for,
* rejecting all others. Sometimes bogus data can be received or some other
* Centameter not part of this node might be received. Configuring this field
* prevents data from sources other than those configured here from being
* collected. Note the source values configured here should be the values
* <em>after</em> any {@code addressSourceMapping} translation has occurred.</dd>
*
* <dt>collectAllSourceIds</dt>
* <dd>If <em>true</em> and the
* {@link net.solarnetwork.node.MultiDatumDataSource} API is used, then attempt
* to read values for all sources configured in the {@code sourceIdFilter}
* property and return all the data collected. The
* {@code collectAllSourceIdsTimeout} property is used to limit the amount of
* time spent collecting data, as there is no guarantee the application can read
* from all sources: the Centamter data is captured somewhat randomly. Defaults
* to <em>true</em>.</dd>
*
* <dt>collectAllSourceIdsTimeout</dt>
* <dd>When {@code collectAllSourceIds} is configured as <em>true</em> this is a
* timeout value, in seconds, the application should spend attempting to collect
* data from all configured sources. If this amount of time is passed before
* data for all sources has been collected, the application will give up and
* just return whatever data it has collected at that point. Defaults to
* {@link #DEFAULT_COLLECT_ALL_SOURCE_IDS_TIMEOUT}.</dd>
* </dl>
*
* @author matt
* @version 1.1
*/
public class CentameterSupport {
/** The data byte index for the Centameter's address ID. */
public static final int CENTAMETER_ADDRESS_IDX = 2;
/**
* The data byte index for the Centameter's amp reading, as integer (amps *
* 10) value.
*/
public static final int CENTAMETER_AMPS_IDX = 7;
/** The default value for the {@code voltage} property. */
public static final float DEFAULT_VOLTAGE = 230.0F;
/** The default value for the {@code sourceIdFormat} property. */
public static final String DEFAULT_SOURCE_ID_FORMAT = "%X.%d";
/** The default value for the {@code collectAllSourceIdsTimeout} property. */
public static final int DEFAULT_COLLECT_ALL_SOURCE_IDS_TIMEOUT = 30;
/** The default value for the {@code multiAmpSensorIndexFlags} property. */
public static final int DEFAULT_MULTI_AMP_SENSOR_INDEX_FLAGS = (1 | 2 | 4);
/** The default value for the {@code ampSensorIndex} property. */
public static final int DEFAULT_AMP_SENSOR_INDEX = 1;
/** A class-level logger. */
protected final Logger log = LoggerFactory.getLogger(getClass());
private static final Object MONITOR = new Object();
private static MessageSource MESSAGE_SOURCE;
private DynamicServiceTracker<DataCollectorFactory<DataCollectorSerialPortBeanParameters>> dataCollectorFactory;
private DataCollectorSerialPortBeanParameters serialParams = getDefaultSerialParams();
private float voltage = DEFAULT_VOLTAGE;
private int ampSensorIndex = DEFAULT_AMP_SENSOR_INDEX;
private int multiAmpSensorIndexFlags = DEFAULT_MULTI_AMP_SENSOR_INDEX_FLAGS;
private String sourceIdFormat = DEFAULT_SOURCE_ID_FORMAT;
private Map<String, String> addressSourceMapping = null;
private Set<String> sourceIdFilter = null;
private boolean collectAllSourceIds = true;
private int collectAllSourceIdsTimeout = DEFAULT_COLLECT_ALL_SOURCE_IDS_TIMEOUT;
private final SortedSet<CentameterDatum> knownAddresses = new ConcurrentSkipListSet<CentameterDatum>();
private String uid = null;
private String groupUID = null;
protected static final DataCollectorSerialPortBeanParameters getDefaultSerialParams() {
DataCollectorSerialPortBeanParameters defaults = new DataCollectorSerialPortBeanParameters();
defaults.setMagic(new byte[] { 120 });
defaults.setBaud(4800);
defaults.setBufferSize(16);
defaults.setReadSize(15);
defaults.setReceiveThreshold(-1);
defaults.setMaxWait(90000);
return defaults;
}
/**
* Add a new cached "known" address value.
*
* <p>
* This adds the address to the cached set of <em>known</em> addresses,
* which are shown as a read-only setting property to aid in mapping the
* right Cent-a-meter address.
* </p>
*
* @param datum
* the datum to add
*/
protected void addKnownAddress(CentameterDatum datum) {
knownAddresses.add(datum);
}
/**
* Get a read-only set of known addresses.
*
* <p>
* This will contain all the addresses previously passed to
* {@link #addKnownAddress(String)} and that have not been removed via
* {@link #clearKnownAddresses(Collection)}.
* </p>
*
* @return a read-only set of known addresses
*/
protected SortedSet<CentameterDatum> getKnownAddresses() {
return Collections.unmodifiableSortedSet(knownAddresses);
}
/**
* Remove known address values from the known address cache.
*
* <p>
* You can clear out the entire cache by passing in the result of
* {@link #getKnownAddresses()}.
* </p>
*
* @param toRemove
* the collection of addresses to remove
*/
protected void clearKnownAddresses(Collection<CentameterDatum> toRemove) {
knownAddresses.removeAll(toRemove);
}
/**
* Set a {@code addressSourceMapping} Map via an encoded String value.
*
* <p>
* The format of the {@code mapping} String should be:
* </p>
*
* <pre>
* key=val[,key=val,...]
* </pre>
*
* <p>
* Whitespace is permitted around all delimiters, and will be stripped from
* the keys and values.
* </p>
*
* @param mapping
*/
public void setAddressSourceMappingValue(String mapping) {
if ( mapping == null || mapping.length() < 1 ) {
setAddressSourceMapping(null);
return;
}
String[] pairs = mapping.split("\\s*,\\s*");
Map<String, String> map = new LinkedHashMap<String, String>();
for ( String pair : pairs ) {
String[] kv = pair.split("\\s*=\\s*");
if ( kv == null || kv.length != 2 ) {
continue;
}
map.put(kv[0], kv[1]);
}
setAddressSourceMapping(map);
}
/**
* Set a {@link sourceIdFilter} List via an encoded String value.
*
* <p>
* The format of the {@code filters} String should be a comma-delimited list
* of values. Whitespace is permitted around the commas, and will be
* stripped from the values.
* </p>
*
* @param filters
*/
public void setSourceIdFilterValue(String filters) {
if ( filters == null || filters.length() < 1 ) {
setSourceIdFilter(null);
return;
}
String[] data = filters.split("\\s*,\\s*");
Set<String> s = new LinkedHashSet<String>(data.length);
for ( String d : data ) {
s.add(d);
}
setSourceIdFilter(s);
}
public List<SettingSpecifier> getDefaultSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20);
StringBuilder status = new StringBuilder();
for ( CentameterDatum datum : knownAddresses ) {
if ( status.length() > 0 ) {
status.append(",\n");
}
status.append(datum.getStatusMessage());
}
results.add(new BasicTitleSettingSpecifier("knownAddresses", status.toString(), true));
results.add(new BasicTextFieldSettingSpecifier("dataCollectorFactory.propertyFilters['UID']",
"/dev/ttyUSB0"));
results.add(new BasicTextFieldSettingSpecifier("uid", null));
results.add(new BasicTextFieldSettingSpecifier("groupUID", null));
results.add(new BasicTextFieldSettingSpecifier("voltage", String.valueOf(DEFAULT_VOLTAGE)));
// the multiAmpSensorIndexFlags override this settings, so let's not expose it
// results.add(new BasicTextFieldSettingSpecifier("ampSensorIndex",
// String.valueOf(DEFAULT_AMP_SENSOR_INDEX)));
results.add(new BasicTextFieldSettingSpecifier("multiAmpSensorIndexFlags", String
.valueOf(DEFAULT_MULTI_AMP_SENSOR_INDEX_FLAGS)));
results.add(new BasicTextFieldSettingSpecifier("sourceIdFormat", DEFAULT_SOURCE_ID_FORMAT));
results.add(new BasicTextFieldSettingSpecifier("addressSourceMappingValue", ""));
results.add(new BasicTextFieldSettingSpecifier("sourceIdFilterValue", ""));
results.add(new BasicToggleSettingSpecifier("collectAllSourceIds", Boolean.TRUE));
results.add(new BasicTextFieldSettingSpecifier("collectAllSourceIdsTimeout", String
.valueOf(DEFAULT_COLLECT_ALL_SOURCE_IDS_TIMEOUT)));
results.addAll(DataCollectorSerialPortBeanParameters.getDefaultSettingSpecifiers(
getDefaultSerialParams(), "serialParams."));
return results;
}
public MessageSource getDefaultSettingsMessageSource() {
synchronized ( MONITOR ) {
if ( MESSAGE_SOURCE == null ) {
ResourceBundleMessageSource serial = new ResourceBundleMessageSource();
serial.setBundleClassLoader(SerialPortBeanParameters.class.getClassLoader());
serial.setBasenames(new String[] { SerialPortBeanParameters.class.getName(),
DataCollectorSerialPortBeanParameters.class.getName() });
PrefixedMessageSource serialSource = new PrefixedMessageSource();
serialSource.setDelegate(serial);
serialSource.setPrefix("serialParams.");
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBundleClassLoader(CentameterSupport.class.getClassLoader());
source.setBasename(CentameterSupport.class.getName());
source.setParentMessageSource(serialSource);
MESSAGE_SOURCE = source;
}
}
return MESSAGE_SOURCE;
}
public DynamicServiceTracker<DataCollectorFactory<DataCollectorSerialPortBeanParameters>> getDataCollectorFactory() {
return dataCollectorFactory;
}
public void setDataCollectorFactory(
DynamicServiceTracker<DataCollectorFactory<DataCollectorSerialPortBeanParameters>> dataCollectorFactory) {
this.dataCollectorFactory = dataCollectorFactory;
}
public DataCollectorSerialPortBeanParameters getSerialParams() {
return serialParams;
}
public void setSerialParams(DataCollectorSerialPortBeanParameters serialParams) {
this.serialParams = serialParams;
}
public float getVoltage() {
return voltage;
}
public void setVoltage(float voltage) {
this.voltage = voltage;
}
public int getAmpSensorIndex() {
return ampSensorIndex;
}
public void setAmpSensorIndex(int ampSensorIndex) {
this.ampSensorIndex = ampSensorIndex;
}
public int getMultiAmpSensorIndexFlags() {
return multiAmpSensorIndexFlags;
}
public void setMultiAmpSensorIndexFlags(int multiAmpSensorIndexFlags) {
this.multiAmpSensorIndexFlags = multiAmpSensorIndexFlags;
}
public String getSourceIdFormat() {
return sourceIdFormat;
}
public void setSourceIdFormat(String sourceIdFormat) {
this.sourceIdFormat = sourceIdFormat;
}
public Map<String, String> getAddressSourceMapping() {
return addressSourceMapping;
}
public void setAddressSourceMapping(Map<String, String> addressSourceMapping) {
this.addressSourceMapping = addressSourceMapping;
}
public Set<String> getSourceIdFilter() {
return sourceIdFilter;
}
public void setSourceIdFilter(Set<String> sourceIdFilter) {
this.sourceIdFilter = sourceIdFilter;
}
public boolean isCollectAllSourceIds() {
return collectAllSourceIds;
}
public void setCollectAllSourceIds(boolean collectAllSourceIds) {
this.collectAllSourceIds = collectAllSourceIds;
}
public int getCollectAllSourceIdsTimeout() {
return collectAllSourceIdsTimeout;
}
public void setCollectAllSourceIdsTimeout(int collectAllSourceIdsTimeout) {
this.collectAllSourceIdsTimeout = collectAllSourceIdsTimeout;
}
public String getUID() {
return getUid();
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getGroupUID() {
return groupUID;
}
public void setGroupUID(String groupUID) {
this.groupUID = groupUID;
}
}