/* ==================================================================
* SMAyasdi4jPowerDatumDataSource.java - Mar 7, 2013 11:57:06 AM
*
* 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.power.sma.yasdi4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.solarnetwork.domain.GeneralDatumMetadata;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.dao.SettingDao;
import net.solarnetwork.node.domain.ACEnergyDatum;
import net.solarnetwork.node.domain.GeneralNodeACEnergyDatum;
import net.solarnetwork.node.hw.sma.SMAInverterDataSourceSupport;
import net.solarnetwork.node.io.yasdi4j.YasdiMaster;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier;
import net.solarnetwork.util.DynamicServiceTracker;
import net.solarnetwork.util.StringUtils;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.context.MessageSource;
import de.michaeldenk.yasdi4j.YasdiChannel;
import de.michaeldenk.yasdi4j.YasdiDevice;
/**
* SMA {@link DatumDataSource} for {@link PowerDatum}, using the {@code yasdi4j}
* library.
*
* <p>
* This class is not generally not thread-safe. Only one thread should execute
* {@link #readCurrentDatum()} at a time.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>yasdi</dt>
* <dd>The dynamic service for the {@link YasdiMaster} instance to use.</dd>
*
* <dt>settingDao</dt>
* <dd>The {@link SettingDao} to use, required by the
* {@code channelNamesToResetDaily} property.</dd>
* </dl>
*
* @author matt
* @version 2.0
*/
public class SMAyasdi4jPowerDatumDataSource extends SMAInverterDataSourceSupport implements
DatumDataSource<GeneralNodeACEnergyDatum>, SettingSpecifierProvider {
/** The default value for the {@code sourceId} property. */
public static final String DEFAULT_SOURCE_ID = "Main";
/** The watts output channel name. */
public static final String CHANNEL_NAME_WATTS = "Pac";
/** The PV current channel name. */
public static final String CHANNEL_NAME_PV_AMPS = "Ipv";
/** The PV voltage channel name. */
public static final String CHANNEL_NAME_PV_VOLTS = "Upv-Ist";
/** The accumulative kWh channel name. */
public static final String CHANNEL_NAME_KWH = "E-Total";
/** The default device serial number value. */
public static final Long DEFAULT_SERIAL_NUMBER = 1000L;
/**
* Default value for the {@code channelNamesToMonitor} property.
*
* <p>
* Contains the PV voltage, PV current, and kWh channels.
* </p>
*/
public static final Set<String> DEFAULT_CHANNEL_NAMES_TO_MONITOR = Collections
.unmodifiableSet(new LinkedHashSet<String>(Arrays.asList(CHANNEL_NAME_PV_AMPS,
CHANNEL_NAME_PV_VOLTS, CHANNEL_NAME_KWH)));
private String pvVoltsChannelName = CHANNEL_NAME_PV_VOLTS;
private String pvAmpsChannelName = CHANNEL_NAME_PV_AMPS;
private Set<String> pvWattsChannelNames = Collections.singleton(CHANNEL_NAME_WATTS);
private String kWhChannelName = CHANNEL_NAME_KWH;
private Set<String> otherChannelNames = null;
private int channelMaxAgeSeconds = 30;
private long deviceSerialNumber = DEFAULT_SERIAL_NUMBER;
private long deviceLockTimeoutSeconds = 20;
private DynamicServiceTracker<ObjectFactory<YasdiMaster>> yasdi;
private MessageSource messageSource;
public SMAyasdi4jPowerDatumDataSource() {
super();
setChannelNamesToMonitor(DEFAULT_CHANNEL_NAMES_TO_MONITOR);
}
@Override
public Class<? extends GeneralNodeACEnergyDatum> getDatumType() {
return SMAPowerDatum.class;
}
private YasdiDevice getYasdiDevice() {
final ObjectFactory<YasdiMaster> service = yasdi.service();
if ( service == null ) {
log.debug("No YASDI service available.");
return null;
}
final YasdiMaster master = service.getObject();
YasdiDevice device = master.getDevice(this.deviceSerialNumber);
if ( device == null ) {
log.info("YASDI device {} not available", this.deviceSerialNumber);
}
return device;
}
private void releaseYasdiDevice(YasdiDevice device) {
if ( device == null ) {
return;
}
final ObjectFactory<YasdiMaster> service = yasdi.service();
if ( service == null ) {
log.debug("No YASDI service available.");
return;
}
final YasdiMaster master = service.getObject();
master.releaseDeviceLock(device);
}
@Override
public GeneralNodeACEnergyDatum readCurrentDatum() {
YasdiDevice device = null;
final SMAPowerDatum datum = new SMAPowerDatum();
datum.setCreated(new Date());
try {
device = getYasdiDevice();
if ( device == null ) {
return null;
}
datum.setSourceId(getSourceId());
if ( this.pvWattsChannelNames != null && this.pvWattsChannelNames.size() > 0 ) {
// we sum up all channels into a single value
int totalWatts = 0;
for ( String channelName : this.pvWattsChannelNames ) {
Object v = captureChannelValue(device, channelName);
if ( v instanceof Number ) {
totalWatts += ((Number) v).intValue();
}
}
datum.setWatts(totalWatts);
} else {
Object volts = captureChannelValue(device, this.pvVoltsChannelName);
Object amps = captureChannelValue(device, this.pvAmpsChannelName);
if ( volts instanceof Number && amps instanceof Number ) {
datum.setWatts(((Number) volts).intValue() * ((Number) amps).intValue());
}
}
Object wh = captureChannelValue(device, this.kWhChannelName);
if ( wh instanceof Number ) {
datum.setWattHourReading(((Number) wh).longValue());
}
if ( otherChannelNames != null ) {
Map<String, Object> map = new LinkedHashMap<String, Object>(otherChannelNames.size());
for ( String channelName : otherChannelNames ) {
captureDataValue(device, channelName, channelName, map);
}
if ( map.size() > 0 ) {
if ( datum.getChannelData() == null ) {
datum.setChannelData(map);
} else {
datum.getChannelData().putAll(map);
}
}
}
} finally {
releaseYasdiDevice(device);
}
if ( !isValidDatum(datum) ) {
log.debug("No valid data available.");
return null;
}
postDatumCapturedEvent(datum, ACEnergyDatum.class);
addEnergyDatumSourceMetadata(datum);
return datum;
}
private void addEnergyDatumSourceMetadata(SMAPowerDatum d) {
// associate generation tags with this source
GeneralDatumMetadata sourceMeta = new GeneralDatumMetadata();
sourceMeta.addTag(net.solarnetwork.node.domain.EnergyDatum.TAG_GENERATION);
addSourceMetadata(d.getSourceId(), sourceMeta);
}
private boolean isValidDatum(SMAPowerDatum d) {
if ( (d.getWatts() == null || d.getWatts() < 1)
&& (d.getWattHourReading() == null || d.getWattHourReading() < 1) ) {
return false;
}
return true;
}
private Object captureChannelValue(YasdiDevice device, String channelName) {
if ( channelName == null || channelName.length() < 1 ) {
return null;
}
log.trace("Reading SMA channel {}", channelName);
YasdiChannel channel = device.getChannel(channelName);
if ( channel == null ) {
log.warn("Channel {} not available from YASDI device", channelName);
return null;
}
try {
// get updated value, at most channelMaxAgeSeconds old
channel.updateValue(channelMaxAgeSeconds);
} catch ( IOException e ) {
log.debug("Exception updating channel {} value: {}", channelName, e.toString());
return null;
}
Object value = null;
if ( channel.hasText() ) {
value = channel.getValueText();
} else {
double v = channel.getValue();
String unit = channel.getUnit();
if ( unit != null ) {
if ( unit.startsWith("m") ) {
v /= 1000.0;
} else if ( unit.startsWith("k") ) {
v *= 1000;
}
}
value = Double.valueOf(v);
}
log.trace("Read SMA channel {}: {}", channelName, value);
return value;
}
/**
* Read a specific channel and set that value into a Map.
*
* @param device
* the YasdiDevice to collect the data from
* @param channelName
* the name of the channel to read
* @param key
* the Map key to set with the data value
* @param accessor
* the datum to update
*/
private void captureDataValue(YasdiDevice device, String channelName, String key,
Map<String, Object> map) {
Object value = captureChannelValue(device, channelName);
if ( value != null ) {
map.put(key, value);
}
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.power.sma.yasdi4j";
}
@Override
public String getDisplayName() {
return "SMA inverter (YASDI)";
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20);
SMAyasdi4jPowerDatumDataSource defaults = new SMAyasdi4jPowerDatumDataSource();
String yasdiDeviceName = "N/A";
YasdiDevice device = null;
try {
device = getYasdiDevice();
if ( device != null ) {
yasdiDeviceName = device.getName();
}
} catch ( RuntimeException e ) {
log.warn("Exception getting YASDI device name: {}", e.getMessage());
} finally {
releaseYasdiDevice(device);
}
results.add(new BasicTitleSettingSpecifier("address", yasdiDeviceName, true));
results.add(new BasicTextFieldSettingSpecifier("deviceSerialNumber", String
.valueOf(defaults.deviceSerialNumber)));
results.add(new BasicTextFieldSettingSpecifier("sourceId", defaults.getSourceId()));
results.add(new BasicTextFieldSettingSpecifier("groupUID", defaults.getGroupUID()));
results.add(new BasicTextFieldSettingSpecifier("channelMaxAgeSeconds", String.valueOf(defaults
.getChannelMaxAgeSeconds())));
results.add(new BasicTextFieldSettingSpecifier("deviceLockTimeoutSeconds", String
.valueOf(defaults.getDeviceLockTimeoutSeconds())));
results.add(new BasicTextFieldSettingSpecifier("pvWattsChannelNamesValue", defaults
.getPvWattsChannelNamesValue()));
results.add(new BasicTextFieldSettingSpecifier("pvVoltsChannelName", defaults
.getPvVoltsChannelName()));
results.add(new BasicTextFieldSettingSpecifier("pvAmpsChannelName", defaults
.getPvAmpsChannelName()));
results.add(new BasicTextFieldSettingSpecifier("kWhChannelName", defaults.getkWhChannelName()));
results.add(new BasicTextFieldSettingSpecifier("otherChannelNamesValue", defaults
.getOtherChannelNamesValue()));
return results;
}
private void setupChannelNamesToMonitor() {
Set<String> s = new LinkedHashSet<String>(3);
if ( getPvWattsChannelNames() != null && getPvWattsChannelNames().size() > 0 ) {
s.addAll(getPvWattsChannelNames());
} else {
if ( getPvVoltsChannelName() != null ) {
s.add(getPvVoltsChannelName());
}
if ( getPvAmpsChannelName() != null ) {
s.add(getPvAmpsChannelName());
}
}
s.add(getkWhChannelName());
if ( otherChannelNames != null ) {
s.addAll(otherChannelNames);
}
if ( !s.equals(this.getChannelNamesToMonitor()) ) {
setChannelNamesToMonitor(s);
}
}
public String getPvVoltsChannelName() {
return pvVoltsChannelName;
}
public void setPvVoltsChannelName(String pvVoltsChannelName) {
this.pvVoltsChannelName = pvVoltsChannelName;
setupChannelNamesToMonitor();
}
public String getPvAmpsChannelName() {
return pvAmpsChannelName;
}
public void setPvAmpsChannelName(String pvAmpsChannelName) {
this.pvAmpsChannelName = pvAmpsChannelName;
setupChannelNamesToMonitor();
}
public String getkWhChannelName() {
return kWhChannelName;
}
public void setkWhChannelName(String kWhChannelName) {
this.kWhChannelName = kWhChannelName;
setupChannelNamesToMonitor();
}
public DynamicServiceTracker<ObjectFactory<YasdiMaster>> getYasdi() {
return yasdi;
}
public void setYasdi(DynamicServiceTracker<ObjectFactory<YasdiMaster>> yasdi) {
this.yasdi = yasdi;
}
public void setChannelMaxAgeSeconds(int channelMaxAgeSeconds) {
this.channelMaxAgeSeconds = channelMaxAgeSeconds;
}
/**
* Get {@code pvWattsChannelNames} as a comma-delimited string value.
*
* @return the channel names, as a delimited string
*/
public String getPvWattsChannelNamesValue() {
return (pvWattsChannelNames == null ? null : StringUtils
.commaDelimitedStringFromCollection(pvWattsChannelNames));
}
/**
* Set {@code pvWattsChannelNames} as a comma-delimited string value.
*
* @param value
* the channel names, as a delimited string
*/
public void setPvWattsChannelNamesValue(String value) {
setPvWattsChannelNames(StringUtils.commaDelimitedStringToSet(value));
}
public Set<String> getPvWattsChannelNames() {
return pvWattsChannelNames;
}
public void setPvWattsChannelNames(Set<String> pvWattsChannelNames) {
this.pvWattsChannelNames = pvWattsChannelNames;
setupChannelNamesToMonitor();
}
public void setDeviceSerialNumber(long deviceSerialNumber) {
this.deviceSerialNumber = deviceSerialNumber;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public Set<String> getOtherChannelNames() {
return otherChannelNames;
}
public void setOtherChannelNames(Set<String> otherChannelNames) {
this.otherChannelNames = otherChannelNames;
}
/**
* Get {@code otherChannelNames} as a comma-delimited string value.
*
* @return the other channel names, as a delimited string
*/
public String getOtherChannelNamesValue() {
return (otherChannelNames == null ? null : StringUtils
.commaDelimitedStringFromCollection(otherChannelNames));
}
/**
* Set {@code otherChannelNames} as a comma-delimited string value.
*
* @param value
* the channel names, as a delimited string
*/
public void setOtherChannelNamesValue(String value) {
setOtherChannelNames(StringUtils.commaDelimitedStringToSet(value));
}
public long getDeviceLockTimeoutSeconds() {
return deviceLockTimeoutSeconds;
}
public void setDeviceLockTimeoutSeconds(long deviceLockTimeoutSeconds) {
this.deviceLockTimeoutSeconds = deviceLockTimeoutSeconds;
}
public int getChannelMaxAgeSeconds() {
return channelMaxAgeSeconds;
}
}