/* =================================================================== * 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; } }