net.solarnetwork.node.power.modbus.ModbusPowerDatumDataSource.java Source code

Java tutorial

Introduction

Here is the source code for net.solarnetwork.node.power.modbus.ModbusPowerDatumDataSource.java

Source

/* ===================================================================
 * 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 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.io.modbus.ModbusNetwork;
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.DynamicServiceTracker;
import net.solarnetwork.util.StringUtils;
import org.springframework.beans.PropertyAccessException;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.context.MessageSource;

/**
 * {@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>
 * 
 * <dt>modbusNetwork</dt>
 * <dd>The {@link ModbusNetwork} to use.</dd>
 * 
 * @author matt.magoffin
 * @version 2.0
 */
public class ModbusPowerDatumDataSource extends ModbusDeviceSupport
        implements DatumDataSource<PowerDatum>, SettingSpecifierProvider {

    private DynamicServiceTracker<ModbusNetwork> modbusNetwork;

    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 [{}]", propertyName, Integer.toHexString(addr),
                    value);
            try {
                bean.setPropertyValue(propertyName, value);
            } catch (PropertyAccessException e) {
                log.warn("Unable to set property {} to {} for address 0x{}: {}", 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;
    }

    @Override
    public DynamicServiceTracker<ModbusNetwork> getModbusNetwork() {
        return modbusNetwork;
    }

    public void setModbusNetwork(DynamicServiceTracker<ModbusNetwork> modbusNetwork) {
        this.modbusNetwork = modbusNetwork;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

}