/* ================================================================== * CCSupport.java - Apr 23, 2013 3:30:30 PM * * Copyright 2007-2013 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.hw.currentcost; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; 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.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentSkipListSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import net.solarnetwork.domain.GeneralDatumMetadata; import net.solarnetwork.node.DatumMetadataService; import net.solarnetwork.node.io.serial.SerialConnection; import net.solarnetwork.node.io.serial.SerialDeviceSupport; import net.solarnetwork.node.io.serial.SerialNetwork; 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.util.OptionalService; /** * Support class for reading CurrentCost watt meter data from a serial * connection. * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>serialNetwork</dt> * <dd>The {@link SerialNetwork} to use.</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 device, since it only measures * current. Defaults to {@link #DEFAULT_VOLTAGE}.</dd> * * <dt>ampSensorIndex</dt> * <dd>The device 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 Datum instances. This format will be passed the device address (as a * <em>string</em>) and the device sensor index (as an <em>integer</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 device address ID values to Datum sourceId * values. This can be used to consistently collect data from devices, even * after the device 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 * device 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 device 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 2.1 */ public class CCSupport extends SerialDeviceSupport { /** The data byte index for the device's address ID. */ public static final int DEVICE_ADDRESS_IDX = 2; /** The default value for the {@code voltage} property. */ public static final float DEFAULT_VOLTAGE = 240.0F; /** The default value for the {@code sourceIdFormat} property. */ public static final String DEFAULT_SOURCE_ID_FORMAT = "%s.%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; /** The starting message marker, which is the opening XML element. */ public static final String MESSAGE_START_MARKER = "<msg>"; /** The ending message marker, which is the closing XML element. */ public static final String MESSAGE_END_MARKER = "</msg>"; /** A class-level logger. */ protected final Logger log = LoggerFactory.getLogger(getClass()); /** A CCMessageParser instance. */ protected final CCMessageParser messageParser = new CCMessageParser(); private final SortedSet<CCDatum> knownAddresses = new ConcurrentSkipListSet<CCDatum>(); 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 long sampleCacheMs = 5000; private MessageSource messageSource; private OptionalService<DatumMetadataService> datumMetadataService; private final ConcurrentMap<String, GeneralDatumMetadata> sourceMetadataCache = new ConcurrentHashMap<String, GeneralDatumMetadata>( 4); /** * 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 device address, as long as the {@link CCDatum#getDeviceAddress()} * value is not <em>null</em>. * </p> * * @param datum * the datum to add */ protected void addKnownAddress(CCDatum datum) { if ( datum != null && datum.getDeviceAddress() != null ) { knownAddresses.remove(datum); // remove old copy, if present 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<CCDatum> 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<CCDatum> toRemove) { knownAddresses.removeAll(toRemove); } /** * Get an address value for a given sample and sensor index. * * @param datum * the sample data * @param ampIndex * the sensor index * @return the address value to use */ protected String addressValue(CCDatum datum, int ampIndex) { return String.format(sourceIdFormat, datum.getDeviceAddress(), ampIndex); } /** * Return all cached data from the {@code knownAddresses} Map whose address * is currently configured in the {@link #getAddressSourceMapping()} map. * The data is only returned if it is not older than * {@link #getSampleCacheMs()} (if that is configured as anything greater * than zero). * * @return set of cached {@link CCDatum}, or an empty Set if none available */ protected Set<CCDatum> allCachedDataForConfiguredAddresses() { Set<String> captureAddresses = (addressSourceMapping == null ? null : addressSourceMapping.keySet()); if ( captureAddresses == null ) { return Collections.emptySet(); } Set<CCDatum> result = new HashSet<CCDatum>(4); final long now = System.currentTimeMillis(); for ( CCDatum datum : knownAddresses ) { for ( int i = 1; i <= 3; i++ ) { String anAddress = addressValue(datum, i); if ( captureAddresses.contains(anAddress) ) { if ( sampleCacheMs < 1 || (now - datum.getCreated()) <= sampleCacheMs ) { result.add(datum); break; } } } } if ( log.isDebugEnabled() && result.size() > 0 ) { log.debug("Returning cached CCDatum samples: {}", result); } return result; } /** * 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 ( CCDatum datum : knownAddresses ) { if ( status.length() > 0 ) { status.append(",\n"); } status.append(datum.getStatusMessage()); } CCSupport defaults = new CCSupport(); results.add(new BasicTitleSettingSpecifier("knownAddresses", status.toString(), true)); results.add(new BasicTextFieldSettingSpecifier("uid", null)); results.add(new BasicTextFieldSettingSpecifier("groupUID", null)); results.add(new BasicTextFieldSettingSpecifier("serialNetwork.propertyFilters['UID']", "Serial Port")); results.add(new BasicTextFieldSettingSpecifier("sampleCacheMs", String.valueOf(defaults.getSampleCacheMs()))); results.add(new BasicTextFieldSettingSpecifier("voltage", String.valueOf(DEFAULT_VOLTAGE))); results.add(new BasicToggleSettingSpecifier("multiCollectSensor1", defaults.isMultiCollectSensor1())); results.add(new BasicToggleSettingSpecifier("multiCollectSensor2", defaults.isMultiCollectSensor2())); results.add(new BasicToggleSettingSpecifier("multiCollectSensor3", defaults.isMultiCollectSensor3())); 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))); return results; } /** * Returns an empty Map. Extending classes can override as appropriate. * * @param conn * the serial connection * @return empty map */ @Override protected Map<String, Object> readDeviceInfo(SerialConnection conn) { return Collections.emptyMap(); } /** * Add source metadata using the configured {@link DatumMetadataService} (if * available). The metadata will be cached so that subseqent calls to this * method with the same metadata value will not try to re-save the unchanged * value. This method will catch all exceptions and silently discard them. * * @param sourceId * the source ID to add metadata to * @param meta * the metadata to add * @param returns * <em>true</em> if the metadata was saved successfully, or does not * need to be updated */ protected boolean addSourceMetadata(final String sourceId, final GeneralDatumMetadata meta) { GeneralDatumMetadata cached = sourceMetadataCache.get(sourceId); if ( cached != null && meta.equals(cached) ) { // we've already posted this metadata... don't bother doing it again log.debug("Source {} metadata already added, not posting again", sourceId); return true; } DatumMetadataService service = null; if ( datumMetadataService != null ) { service = datumMetadataService.service(); } if ( service == null ) { return false; } try { service.addSourceMetadata(sourceId, meta); sourceMetadataCache.put(sourceId, meta); return true; } catch ( Exception e ) { log.debug("Error saving source {} metadata: {}", sourceId, e.getMessage()); } return false; } 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; } /** * Get the bitmask flag of amp sensor index values to return when requesting * multiple datum samples. * * @return The current bitmask value. */ public int getMultiAmpSensorIndexFlags() { return multiAmpSensorIndexFlags; } /** * Set the bitmask flag for which amp sensor index readings to return when * requesting multiple datum samples. * * 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 * {@code 7}. * * @param multiAmpSensorIndexFlags * The bitmask to set. */ public void setMultiAmpSensorIndexFlags(int multiAmpSensorIndexFlags) { this.multiAmpSensorIndexFlags = multiAmpSensorIndexFlags; } private boolean isMultiCollectSensor(int index) { return (this.multiAmpSensorIndexFlags & index) == index; } private void setMultiCollectSensor(int index, boolean value) { if ( value ) { this.multiAmpSensorIndexFlags |= index; } else { this.multiAmpSensorIndexFlags &= ~index; } } /** * Test if sensor 1 should be collected when requesting multiple datum * samples. * * @return <em>true</em> if sensor 1 should be collected * @see #getMultiAmpSensorIndexFlags() * @since 2.1 */ public boolean isMultiCollectSensor1() { return isMultiCollectSensor(1); } /** * Set if sensor 1 should be collected when requesting multiple datum * samples. * * @param value * <em>true</em> if sensor 1 should be collected * @since 2.1 */ public void setMultiCollectSensor1(boolean value) { setMultiCollectSensor(1, value); } /** * Test if sensor 2 should be collected when requesting multiple datum * samples. * * @return <em>true</em> if sensor 2 should be collected * @see #getMultiAmpSensorIndexFlags() * @since 2.1 */ public boolean isMultiCollectSensor2() { return isMultiCollectSensor(2); } /** * Set if sensor 2 should be collected when requesting multiple datum * samples. * * @param value * <em>true</em> if sensor 2 should be collected * @since 2.1 */ public void setMultiCollectSensor2(boolean value) { setMultiCollectSensor(2, value); } /** * Test if sensor 3 should be collected when requesting multiple datum * samples. * * @return <em>true</em> if sensor 3 should be collected * @see #getMultiAmpSensorIndexFlags() * @since 2.1 */ public boolean isMultiCollectSensor3() { return isMultiCollectSensor(3); } /** * Set if sensor 3 should be collected when requesting multiple datum * samples. * * @param value * <em>true</em> if sensor 3 should be collected * @since 2.1 */ public void setMultiCollectSensor3(boolean value) { setMultiCollectSensor(3, value); } 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; } @Override public String getUID() { return getUid(); } public long getSampleCacheMs() { return sampleCacheMs; } public void setSampleCacheMs(long sampleCacheMs) { this.sampleCacheMs = sampleCacheMs; } public MessageSource getMessageSource() { return messageSource; } public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public OptionalService<DatumMetadataService> getDatumMetadataService() { return datumMetadataService; } public void setDatumMetadataService(OptionalService<DatumMetadataService> datumMetadataService) { this.datumMetadataService = datumMetadataService; } }