/* ==================================================================
* BaseSDMData.java - 25/01/2016 5:52:26 pm
*
* Copyright 2007-2016 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.hw.deson.meter;
import java.util.Arrays;
import java.util.Map;
import gnu.trove.map.hash.TIntIntHashMap;
import net.solarnetwork.node.io.modbus.ModbusConnection;
import net.solarnetwork.node.io.modbus.ModbusHelper;
/**
* Abstract base class for other {@code SDMData} implementations to extend.
*
* The normal use for extending classes is to implement
* {@link SDMData#readMeterData(ModbusConnection)} by calling
* {@link #readInputData(ModbusConnection, int, int)} and
* {@link #readHoldingData(ModbusConnection, int, int)} for ranges of Modbus
* register data.
*
* @author matt
* @version 1.1
*/
public abstract class BaseSDMData implements SDMData {
private final TIntIntHashMap dataRegisters;
private final TIntIntHashMap controlRegisters;
private final boolean backwards;
private long meterDataTimestamp = 0;
private long controlDataTimestamp = 0;
/**
* Default constructor.
*/
public BaseSDMData() {
super();
this.dataRegisters = new TIntIntHashMap(64);
this.controlRegisters = new TIntIntHashMap(8);
this.backwards = false;
}
/**
* Construct with backwards setting.
*
* @param backwards
* If {@code true} then treat the meter as being installed backwards
* with respect to the current direction. In this case certain
* instantaneous measurements will be negated and certain
* accumulating properties will be switched (like {@code wattHours}
* and {@code wattHoursReverse}) when
* {@link SDMData#populateMeasurements} is called.
*/
public BaseSDMData(boolean backwards) {
super();
this.dataRegisters = new TIntIntHashMap(64);
this.controlRegisters = new TIntIntHashMap(8);
this.backwards = backwards;
}
/**
* Copy constructor.
*
* @param other
* the object to copy
*/
public BaseSDMData(BaseSDMData other) {
this.dataRegisters = new TIntIntHashMap(other.dataRegisters);
this.controlRegisters = new TIntIntHashMap(other.controlRegisters);
this.meterDataTimestamp = other.meterDataTimestamp;
this.controlDataTimestamp = other.controlDataTimestamp;
this.backwards = other.backwards;
}
@Override
public final synchronized void readMeterData(ModbusConnection conn) {
if ( readMeterDataInternal(conn) ) {
this.meterDataTimestamp = System.currentTimeMillis();
}
}
/**
* Called by {@link #readMeterData(ModbusConnection)}.
*
* @param conn
* The Modbus connection to use.
* @return <em>true</em> if the data has been read successfully
*/
protected abstract boolean readMeterDataInternal(ModbusConnection conn);
@Override
public long getMeterDataTimestamp() {
return meterDataTimestamp;
}
@Override
public final long getControlDataTimestamp() {
return controlDataTimestamp;
}
@Override
public final synchronized void readControlData(ModbusConnection conn) {
if ( readControlDataInternal(conn) ) {
this.controlDataTimestamp = System.currentTimeMillis();
}
}
/**
* Called by {@link #readControlData(ModbusConnection)}.
*
* @param conn
* The Modbus connection to use.
* @return <em>true</em> if the data has been read successfully
*/
protected abstract boolean readControlDataInternal(ModbusConnection conn);
/**
* Read Modbus input registers in an address range.
*
* @param conn
* The Modbus connection.
* @param startAddr
* The starting Modbus register address.
* @param endAddr
* The ending Modbus register address.
*/
protected void readInputData(final ModbusConnection conn, final int startAddr, final int endAddr) {
Map<Integer, Integer> data = conn.readInputValues(new Integer[] { startAddr },
(endAddr - startAddr + 1));
dataRegisters.putAll(data);
}
/**
* Read Modbus holding registers in an address range.
*
* @param conn
* The Modbus connection.
* @param startAddr
* The starting Modbus register address.
* @param endAddr
* The ending Modbus register address.
*/
protected void readHoldingData(final ModbusConnection conn, final int startAddr, final int endAddr) {
int[] data = conn.readInts(startAddr, (endAddr - startAddr + 1));
saveControlArray(data, startAddr);
}
/**
* Internally store an array of 16-bit integer register data values,
* starting at a given address.
*
* @param data
* the data array to save
* @param addr
* the starting address of the data
*/
protected void saveDataArray(final int[] data, int addr) {
if ( data == null || data.length < 1 ) {
return;
}
for ( int v : data ) {
dataRegisters.put(addr, v);
addr++;
}
}
/**
* Internally store an array of 16-bit integer register control values,
* starting at a given address.
*
* @param data
* the control data array to save
* @param addr
* the starting address of the data
*/
protected void saveControlArray(final int[] data, int addr) {
if ( data == null || data.length < 1 ) {
return;
}
for ( int v : data ) {
controlRegisters.put(addr, v);
addr++;
}
}
/**
* Get a string of data values, useful for debugging. The generated string
* will contain a register address followed by two register values per line,
* printed as hexidecimal integers, with a prefix and suffix line. The
* register addresses will be printed as {@bold 30001-based} values, to
* match Deson's documentation. For example:
*
* <pre>
* SDM120Data{
* 30001: 0x4141, 0x727E
* 30007: 0xFFC0, 0x0000
* ...
* 30345: 0x0000, 0x0000
* }
* </pre>
*
* @return debug string
*/
public final String dataDebugString(BaseSDMData snapshot) {
final StringBuilder buf = new StringBuilder(snapshot.getClass().getSimpleName()).append("{\n");
int[] keys = snapshot.dataRegisters.keys();
Arrays.sort(keys);
boolean odd = true;
for ( int k : keys ) {
if ( odd ) {
buf.append("\t").append(String.format("%5d", k + 30001)).append(": ");
}
buf.append(String.format("0x%04X", snapshot.dataRegisters.get(k)));
if ( odd ) {
buf.append(", ");
} else {
buf.append("\n");
}
odd = !odd;
}
buf.append("}");
return buf.toString();
}
/**
* Construct a Float from a saved data register address. This method can
* only be called after data register data has been passed to
* {@link #saveDataArray(int[], int)}.
*
* @param addr
* The address of the saved data register to read.
* @return The parsed value, or <em>null</em> if not available.
*/
protected final Float getFloat32(final int addr) {
return ModbusHelper.parseFloat32(dataRegisters.get(addr), dataRegisters.get(addr + 1));
}
/**
* Construct a Float from a saved control register address. This method can
* only be called after data register data has been passed to
* {@link #saveControlArray(int[], int)}.
*
* @param addr
* The address of the saved control register to read.
* @return The parsed value, or <em>null</em> if not available.
*/
protected final Float getControlFloat32(final int addr) {
return ModbusHelper.parseFloat32(controlRegisters.get(addr), controlRegisters.get(addr + 1));
}
@Override
public Float getVoltage(final int addr) {
return getFloat32(addr);
}
@Override
public Float getCurrent(final int addr) {
return getFloat32(addr);
}
@Override
public Float getFrequency(final int addr) {
return getFloat32(addr);
}
@Override
public Float getPowerFactor(final int addr) {
return getFloat32(addr);
}
@Override
public Integer getPower(final int addr) {
Float value = getFloat32(addr);
if ( value == null ) {
return null;
}
return Integer.valueOf((int) (Math.round(value.doubleValue())));
}
@Override
public Long getEnergy(final int addr) {
Float value = getFloat32(addr);
if ( value == null ) {
return null;
}
return Long.valueOf(Math.round(value.doubleValue() * 1000.0));
}
@Override
public boolean isBackwards() {
return backwards;
}
}