/* ================================================================== * CCDatumDataSource.java - Aug 26, 2014 10:19:02 AM * * Copyright 2007-2014 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.datum.currentcost; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import net.solarnetwork.domain.GeneralDatumMetadata; import net.solarnetwork.node.DataCollector; import net.solarnetwork.node.DatumDataSource; import net.solarnetwork.node.MultiDatumDataSource; import net.solarnetwork.node.domain.AtmosphericDatum; import net.solarnetwork.node.domain.GeneralNodeACEnergyDatum; import net.solarnetwork.node.domain.GeneralNodeDatum; import net.solarnetwork.node.hw.currentcost.CCDatum; import net.solarnetwork.node.hw.currentcost.CCSupport; import net.solarnetwork.node.io.serial.SerialConnection; import net.solarnetwork.node.io.serial.SerialConnectionAction; import net.solarnetwork.node.io.serial.SerialUtils; import net.solarnetwork.node.settings.KeyedSettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier; import net.solarnetwork.node.settings.support.BasicToggleSettingSpecifier; /** * {@link MultiDatumDataSource} implementation for CurrentCost watt monitors, * reading data via a serial port. * * <p> * This implementation relies on a device that can listen to the radio signal * broadcast by a CurrentCost watt meters and write that data to a local serial * port. This class will read the device 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> * * @author matt * @version 2.1 */ public class CCDatumDataSource extends CCSupport implements DatumDataSource<GeneralNodeDatum>, MultiDatumDataSource<GeneralNodeDatum>, SettingSpecifierProvider { private boolean tagConsumption = true; private boolean tagIndoor = true; @Override public Class<? extends GeneralNodeDatum> getDatumType() { return GeneralNodeDatum.class; } @Override public GeneralNodeDatum readCurrentDatum() { Set<CCDatum> datumSet = allCachedDataForConfiguredAddresses(); if ( !datumSet.isEmpty() ) { return getGeneralNodeACEnergyDatumInstance(datumSet.iterator().next(), getAmpSensorIndex()); } CCDatum sample = null; try { sample = performAction(new SerialConnectionAction<CCDatum>() { @Override public CCDatum doWithConnection(SerialConnection conn) throws IOException { byte[] data = conn.readMarkedMessage( MESSAGE_START_MARKER.getBytes(SerialUtils.ASCII_CHARSET), MESSAGE_END_MARKER.getBytes(SerialUtils.ASCII_CHARSET)); if ( data != null && data.length > 0 ) { return messageParser.parseMessage(data); } return null; } }); } catch ( IOException e ) { throw new RuntimeException( "Communication problem reading from serial device " + serialNetwork(), e); } if ( sample == null ) { log.warn("No serial data received for CurrentCost datum"); return null; } return getGeneralNodeACEnergyDatumInstance(sample, getAmpSensorIndex()); } @Override public Class<? extends GeneralNodeDatum> getMultiDatumType() { return GeneralNodeDatum.class; } @Override public Collection<GeneralNodeDatum> readMultipleDatum() { final Set<String> sourceIdSet = (new HashSet<String>( getSourceIdFilter() == null ? 0 : getSourceIdFilter().size())); final List<GeneralNodeDatum> result = new ArrayList<GeneralNodeDatum>(4); Set<CCDatum> datumSet = allCachedDataForConfiguredAddresses(); for ( CCDatum ccDatum : datumSet ) { processSample(result, sourceIdSet, ccDatum); } if ( !needMoreSamplesForSources(sourceIdSet) ) { return result; } final long endTime = (isCollectAllSourceIds() && getSourceIdFilter() != null && getSourceIdFilter().size() > 1 ? System.currentTimeMillis() + (getCollectAllSourceIdsTimeout() * 1000) : 0); try { performAction(new SerialConnectionAction<Object>() { @Override public Object doWithConnection(SerialConnection conn) throws IOException { do { byte[] data = conn.readMarkedMessage("<msg>".getBytes("US-ASCII"), "</msg>".getBytes("US-ASCII")); if ( data == null ) { log.warn("Null serial data received, serial communications problem"); return null; } CCDatum ccDatum = messageParser.parseMessage(data); if ( ccDatum == null || ccDatum.getDeviceAddress() == null ) { continue; } // add a known address for this reading addKnownAddress(ccDatum); processSample(result, sourceIdSet, ccDatum); } while ( System.currentTimeMillis() < endTime && needMoreSamplesForSources(sourceIdSet) ); return null; } }); } catch ( IOException e ) { throw new RuntimeException( "Communication problem reading from serial device " + serialNetwork(), e); } return result; } private boolean needMoreSamplesForSources(Set<String> sourceIdSet) { return (sourceIdSet.isEmpty() || sourceIdSet.size() < (getSourceIdFilter() == null ? 0 : getSourceIdFilter().size())); } private void processSample(List<GeneralNodeDatum> result, Set<String> sourceIdSet, CCDatum ccDatum) { if ( log.isDebugEnabled() ) { log.debug("Got CCDatum: {}", ccDatum.getStatusMessage()); } for ( int ampIndex = 1; ampIndex <= 3; ampIndex++ ) { if ( (ampIndex & getMultiAmpSensorIndexFlags()) != ampIndex ) { continue; } GeneralNodeDatum datum = getGeneralNodeACEnergyDatumInstance(ccDatum, ampIndex); if ( datum != null && !sourceIdSet.contains(datum.getSourceId()) ) { result.add(datum); sourceIdSet.add(datum.getSourceId()); } } GeneralNodeDatum datum = getGeneralNodeDatumTemperatureInstance(ccDatum); if ( datum != null && !sourceIdSet.contains(datum.getSourceId()) ) { result.add(datum); sourceIdSet.add(datum.getSourceId()); } } private GeneralNodeACEnergyDatum getGeneralNodeACEnergyDatumInstance(CCDatum datum, int ampIndex) { if ( datum == null ) { return null; } String addr = addressValue(datum, 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; } Integer wattReading = (ampIndex == 2 ? datum.getChannel2Watts() : ampIndex == 3 ? datum.getChannel3Watts() : datum.getChannel1Watts()); GeneralNodeACEnergyDatum result = new GeneralNodeACEnergyDatum(); result.setCreated(new Date(datum.getCreated())); result.setSourceId(addr); result.setWatts(wattReading); // associate consumption/generation tags with this source GeneralDatumMetadata sourceMeta = new GeneralDatumMetadata(); if ( isTagConsumption() ) { sourceMeta.addTag(net.solarnetwork.node.domain.EnergyDatum.TAG_CONSUMPTION); } else { sourceMeta.addTag(net.solarnetwork.node.domain.EnergyDatum.TAG_GENERATION); } addSourceMetadata(addr, sourceMeta); return result; } private GeneralNodeDatum getGeneralNodeDatumTemperatureInstance(CCDatum datum) { if ( datum == null ) { return null; } String addr = datum.getDeviceAddress() + ".T"; 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; } GeneralNodeDatum result = new GeneralNodeDatum(); result.setCreated(new Date(datum.getCreated())); result.setSourceId(addr); result.putInstantaneousSampleValue(AtmosphericDatum.TEMPERATURE_KEY, datum.getTemperature()); // associate indoor/outdoor tags with this source GeneralDatumMetadata sourceMeta = new GeneralDatumMetadata(); if ( isTagIndoor() ) { sourceMeta.addTag(AtmosphericDatum.TAG_ATMOSPHERE_INDOOR); } else { sourceMeta.addTag(AtmosphericDatum.TAG_ATMOSPHERE_OUTDOOR); } addSourceMetadata(addr, sourceMeta); return result; } @Override public String getSettingUID() { return "net.solarnetwork.node.datum.currentcost"; } @Override public String getDisplayName() { return "CurrentCost amp meter"; } private final Set<String> SPECS_FILTER = new HashSet<String>( Arrays.asList("sourceIdFormat", "voltage")); @Override public List<SettingSpecifier> getSettingSpecifiers() { CCDatumDataSource defaults = new CCDatumDataSource(); List<SettingSpecifier> specs = getDefaultSettingSpecifiers(); SettingSpecifier energyTag = new BasicToggleSettingSpecifier("tagConsumption", Boolean.valueOf(defaults.isTagConsumption())); SettingSpecifier atmosTag = new BasicToggleSettingSpecifier("tagIndoor", Boolean.valueOf(defaults.isTagIndoor())); if ( specs.size() > 4 ) { specs.add(4, atmosTag); specs.add(4, energyTag); } else { specs.add(energyTag); specs.add(atmosTag); } // remove some we don't want, insert addressSourceMappingValueExample for ( ListIterator<SettingSpecifier> itr = specs.listIterator(); itr.hasNext(); ) { SettingSpecifier spec = itr.next(); if ( spec instanceof KeyedSettingSpecifier<?> ) { KeyedSettingSpecifier<?> keyedSpec = (KeyedSettingSpecifier<?>) spec; if ( SPECS_FILTER.contains(keyedSpec.getKey()) ) { itr.remove(); } else if ( "addressSourceMappingValue".equals(keyedSpec.getKey()) ) { StringBuilder buf = new StringBuilder(); for ( CCDatum sample : getKnownAddresses() ) { if ( buf.length() > 0 ) { buf.append("<br>\n"); } String format = getSourceIdFormat(); for ( int sensor = 1; sensor <= 3; sensor += 1 ) { buf.append(String.format(format, sample.getDeviceAddress(), sensor)); buf.append(" = Phase").append(sensor).append(", "); } buf.append(sample.getDeviceAddress() + ".T = Temperature"); } if ( buf.length() > 0 ) { itr.add(new BasicTitleSettingSpecifier("addressSourceMappingValueExample", buf.toString(), true)); } } } } return specs; } public boolean isTagConsumption() { return tagConsumption; } public void setTagConsumption(boolean tagConsumption) { this.tagConsumption = tagConsumption; } public boolean isTagIndoor() { return tagIndoor; } public void setTagIndoor(boolean tagIndoor) { this.tagIndoor = tagIndoor; } }