/* ===================================================================
* ModbusPowerDatumDataSource.java
*
* 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.modbus;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.PropertyAccessException;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.context.MessageSource;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.io.modbus.ModbusConnection;
import net.solarnetwork.node.io.modbus.ModbusConnectionAction;
import net.solarnetwork.node.io.modbus.ModbusDeviceSupport;
import net.solarnetwork.node.io.modbus.ModbusHelper;
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.util.StringUtils;
/**
* {@link GenerationDataSource} implementation using the Jamod modbus serial
* communication implementation.
*
* <p>
* This implementation was written to support one specific Morningstar TS-45
* charge controller, but ideally this class could be used to support any
* Modbus-based controller.
* </p>
*
* <p>
* Pass -Dnet.wimpi.modbus.debug=true to the JVM to enable Jamod debug
* communication output to STDOUT.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>address</dt>
* <dd>The Modbus address of the coil-type register to read from.</dd>
*
* <dt>unitId</dt>
* <dd>The Modbus unit ID to use.</dd>
*
* @author matt.magoffin
* @version 2.2
*/
public class ModbusPowerDatumDataSource extends ModbusDeviceSupport
implements DatumDataSource<PowerDatum>, SettingSpecifierProvider {
private Integer[] addresses = new Integer[] { 0x8, 0x10 };
private Integer count = 5;
private String sourceId = "Main";
private Map<Integer, String> registerMapping = defaultRegisterMapping();
private Map<Integer, Double> registerScaleFactor = defaultRegisterScaleFactor();
private Map<Integer, String> hiLoRegisterMapping = defaultHiLoRegisterMapping();
private MessageSource messageSource;
@Override
public Class<? extends PowerDatum> getDatumType() {
return PowerDatum.class;
}
@Override
public PowerDatum readCurrentDatum() {
Map<Integer, Integer> words = null;
try {
words = performAction(new ModbusConnectionAction<Map<Integer, Integer>>() {
@Override
public Map<Integer, Integer> doWithConnection(ModbusConnection conn) throws IOException {
return conn.readInputValues(addresses, count);
}
});
} catch ( IOException e ) {
log.error("Error communicating with Modbus device: {}", e.getMessage());
}
if ( words == null ) {
return null;
}
PowerDatum datum = new PowerDatum();
datum.setSourceId(sourceId);
PropertyAccessor bean = PropertyAccessorFactory.forBeanPropertyAccess(datum);
if ( registerMapping != null ) {
for ( Map.Entry<Integer, String> me : registerMapping.entrySet() ) {
final Integer addr = me.getKey();
if ( words.containsKey(addr) ) {
final Integer word = words.get(addr);
setRegisterAddressValue(bean, addr, me.getValue(), word);
} else {
log.warn("Register address 0x{} not available", Integer.toHexString(addr));
}
}
}
if ( hiLoRegisterMapping != null ) {
for ( Map.Entry<Integer, String> me : hiLoRegisterMapping.entrySet() ) {
final int hiAddr = me.getKey();
final int loAddr = hiAddr + 1;
if ( words.containsKey(hiAddr) && words.containsKey(loAddr) ) {
final int hiWord = words.get(hiAddr);
final int loWord = words.get(loAddr);
final int word = ModbusHelper.getLongWord(hiWord, loWord);
setRegisterAddressValue(bean, hiAddr, me.getValue(), word);
} else {
log.warn("Register address 0x{} out of bounds, {} available",
Integer.toHexString(me.getKey()), words.size());
}
}
}
return datum;
}
private void setRegisterAddressValue(final PropertyAccessor bean, final Integer addr,
final String propertyName, final Integer propertyValue) {
if ( bean.isWritableProperty(propertyName) ) {
Number value = propertyValue;
if ( registerScaleFactor != null && registerScaleFactor.containsKey(addr) ) {
value = Double.valueOf(value.intValue() * registerScaleFactor.get(addr));
}
log.trace("Setting property {} for address 0x{} to [{}]",
new Object[] { propertyName, Integer.toHexString(addr), value });
try {
bean.setPropertyValue(propertyName, value);
} catch ( PropertyAccessException e ) {
log.warn("Unable to set property {} to {} for address 0x{}: {}",
new Object[] { propertyName, value, Integer.toHexString(addr),
e.getMostSpecificCause().getMessage() });
}
} else {
log.warn("Property {} not available; bad configuration", propertyName);
}
}
private static Map<Integer, Double> defaultRegisterScaleFactor() {
// these are for the Morningstar TS-45
Map<Integer, Double> map = new LinkedHashMap<Integer, Double>(5);
map.put(0x8, 96.667 / 32768.0); // battery volts
map.put(0xA, 139.15 / 32768.0); // pv volts
map.put(0XB, 66.667 / 32768.0); // pv amps
map.put(0xC, 316.667 / 32768.0); // dc output amps
map.put(0x13, 0.1); // amp hours total
return map;
}
private static Map<Integer, String> defaultHiLoRegisterMapping() {
Map<Integer, String> map = new LinkedHashMap<Integer, String>(1);
map.put(0x13, "ampHourReading");
return map;
}
private static Map<Integer, String> defaultRegisterMapping() {
Map<Integer, String> map = new LinkedHashMap<Integer, String>(1);
map.put(0x8, "batteryVolts");
map.put(0xA, "pvVolts");
map.put(0xB, "pvAmps");
map.put(0xC, "dcOutputAmps");
return map;
}
/**
* Set the hi/lo register mapping via a comma and equal delimited string.
*
* <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 value
* the mapping value
*/
public void setHiLoRegisterMappingValue(String value) {
Map<String, String> map = StringUtils.commaDelimitedStringToMap(value);
Map<Integer, String> regMap = new LinkedHashMap<Integer, String>(map.size());
for ( Map.Entry<String, String> me : map.entrySet() ) {
try {
regMap.put(Integer.valueOf(me.getKey(), 16), me.getValue());
} catch ( NumberFormatException e ) {
log.warn("HiLo register mapping keys must be hexidecimal integers); {} ignored",
me.getKey());
}
}
setHiLoRegisterMapping(regMap);
}
/**
* Get the high/low register mapping as a comma and equal delimited string.
*
* @return
* @see #setHiLoRegisterMappingValue(String)
*/
public String getHiLoRegisterMappingValue() {
return hexAddressMappingValue(hiLoRegisterMapping);
}
/**
* Set the register mapping via a comma and equal delimited string.
*
* <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 value
* the mapping value
*/
public void setRegisterMappingValue(String value) {
Map<String, String> map = StringUtils.commaDelimitedStringToMap(value);
Map<Integer, String> regMap = new LinkedHashMap<Integer, String>(map.size());
for ( Map.Entry<String, String> me : map.entrySet() ) {
try {
regMap.put(Integer.valueOf(me.getKey(), 16), me.getValue());
} catch ( NumberFormatException e ) {
log.warn("Register mapping keys must be hexideciaml integers); {} ignored", me.getKey());
}
}
setRegisterMapping(regMap);
}
/**
* Get the register mapping as a comma and equal delimited string.
*
* @return
* @see #setRegisterMappingValue(String)
*/
public String getRegisterMappingValue() {
return hexAddressMappingValue(registerMapping);
}
/**
* Set the register mapping via a comma and equal delimited string.
*
* <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 value
* the mapping value
*/
public void setRegisterScaleFactorValue(String value) {
Map<String, String> map = StringUtils.commaDelimitedStringToMap(value);
Map<Integer, Double> regMap = new LinkedHashMap<Integer, Double>(map.size());
for ( Map.Entry<String, String> me : map.entrySet() ) {
try {
regMap.put(Integer.valueOf(me.getKey(), 16), Double.valueOf(me.getValue()));
} catch ( NumberFormatException e ) {
log.warn(
"Register mapping keys must be hexidecimal integers and values doubles); {} -> {} ignored",
me.getKey(), me.getValue());
}
}
setRegisterScaleFactor(regMap);
}
/**
* Get the register mapping as a comma and equal delimited string.
*
* @return
* @see #setRegisterScaleFactorValue(String)
*/
public String getRegisterScaleFactorValue() {
return hexAddressMappingValue(registerScaleFactor);
}
/**
* Set the Modbus addresses to read via a comma-delimited string of
* hexidecimal numbers.
*
* @param value
* the list of addresses
*/
public void setAddressesValue(String value) {
Set<String> addressSet = StringUtils.commaDelimitedStringToSet(value);
List<Integer> addressList = new ArrayList<Integer>(addressSet.size());
for ( String addr : addressSet ) {
try {
addressList.add(Integer.valueOf(addr, 16));
} catch ( NumberFormatException e ) {
log.warn("Address values must be hexidecimal integers; {} ignored", addr);
}
}
setAddresses(addressList.toArray(new Integer[addressList.size()]));
}
/**
* Get the Modbus addresses to read as a comma-delimited string of
* hexidecimal numbers.
*
* @return the list of addresses
*/
public String getAddressessValue() {
List<String> addressList = new ArrayList<String>(addresses == null ? 0 : addresses.length);
if ( addresses != null ) {
for ( Integer addr : addresses ) {
addressList.add(Integer.toHexString(addr));
}
}
return StringUtils.delimitedStringFromCollection(addressList, ",");
}
private String hexAddressMappingValue(Map<Integer, ?> map) {
StringBuilder buf = new StringBuilder();
if ( map != null ) {
for ( Map.Entry<Integer, ?> me : map.entrySet() ) {
if ( buf.length() > 0 ) {
buf.append(",");
}
buf.append(Integer.toHexString(me.getKey())).append('=')
.append(me.getValue().toString());
}
}
return buf.toString();
}
@Override
protected Map<String, Object> readDeviceInfo(ModbusConnection conn) {
return null;
}
// SettingSpecifierProvider
@Override
public String getSettingUID() {
return "net.solarnetwork.node.power.modbus";
}
@Override
public String getDisplayName() {
return "Modbus power generation";
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
ModbusPowerDatumDataSource defaults = new ModbusPowerDatumDataSource();
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(8);
results.add(new BasicTextFieldSettingSpecifier("sourceId",
(defaults.sourceId == null ? "" : defaults.sourceId)));
results.add(new BasicTextFieldSettingSpecifier("groupUID", defaults.getGroupUID()));
results.add(new BasicTextFieldSettingSpecifier("modbusNetwork.propertyFilters['UID']",
"Serial Port"));
results.add(new BasicTextFieldSettingSpecifier("unitId", String.valueOf(defaults.getUnitId())));
results.add(new BasicTextFieldSettingSpecifier("addressesValue", defaults.getAddressessValue()));
results.add(new BasicTextFieldSettingSpecifier("count",
(defaults.getCount() == null ? "" : defaults.getCount().toString())));
results.add(new BasicTextFieldSettingSpecifier("registerMappingValue",
defaults.getRegisterMappingValue()));
results.add(new BasicTextFieldSettingSpecifier("hiLoRegisterMappingValue",
defaults.getHiLoRegisterMappingValue()));
results.add(new BasicTextFieldSettingSpecifier("registerScaleFactorValue",
defaults.getRegisterScaleFactorValue()));
return results;
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
@Override
public String getUID() {
return getSourceId();
}
public Integer[] getAddresses() {
return addresses;
}
public void setAddresses(Integer[] addresses) {
this.addresses = addresses;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Map<Integer, String> getRegisterMapping() {
return registerMapping;
}
public void setRegisterMapping(Map<Integer, String> registerMapping) {
this.registerMapping = registerMapping;
}
public String getSourceId() {
return sourceId;
}
public void setSourceId(String sourceId) {
this.sourceId = sourceId;
}
public Map<Integer, Double> getRegisterScaleFactor() {
return registerScaleFactor;
}
public void setRegisterScaleFactor(Map<Integer, Double> registerScaleFactor) {
this.registerScaleFactor = registerScaleFactor;
}
public Map<Integer, String> getHiLoRegisterMapping() {
return hiLoRegisterMapping;
}
public void setHiLoRegisterMapping(Map<Integer, String> hiLoRegisterMapping) {
this.hiLoRegisterMapping = hiLoRegisterMapping;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}