/* ==================================================================
* EM5600Data.java - Mar 26, 2014 4:13:48 PM
*
* Copyright 2007-2014 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.hc;
import java.util.Arrays;
import net.solarnetwork.node.io.modbus.ModbusConnection;
import net.solarnetwork.node.io.modbus.ModbusHelper;
/**
* Encapsulates raw Modbus register data from the EM5600 meters.
*
* @author matt
* @version 1.1
*/
public class EM5600Data {
// meter info
public static final int ADDR_SYSTEM_METER_MODEL = 0x0;
public static final int ADDR_SYSTEM_METER_HARDWARE_VERSION = 0x2; // length 2 ASCII characters
public static final int ADDR_SYSTEM_METER_SERIAL_NUMBER = 0x10; // length 4 ASCII characters
public static final int ADDR_SYSTEM_METER_MANUFACTURE_DATE = 0x18; // length 2 F10 encoding
// current
public static final int ADDR_DATA_I1 = 0x130;
public static final int ADDR_DATA_I2 = 0x131;
public static final int ADDR_DATA_I3 = 0x132;
public static final int ADDR_DATA_I_AVERAGE = 0x133;
// voltage
public static final int ADDR_DATA_V_L1_NEUTRAL = 0x136;
public static final int ADDR_DATA_V_L2_NEUTRAL = 0x137;
public static final int ADDR_DATA_V_L3_NEUTRAL = 0x138;
public static final int ADDR_DATA_V_NEUTRAL_AVERAGE = 0x139;
public static final int ADDR_DATA_V_L1_L2 = 0x13B;
public static final int ADDR_DATA_V_L2_L3 = 0x13C;
public static final int ADDR_DATA_V_L3_L1 = 0x13D;
public static final int ADDR_DATA_V_L_L_AVERAGE = 0x13E;
// power
public static final int ADDR_DATA_ACTIVE_POWER_TOTAL = 0x140;
public static final int ADDR_DATA_REACTIVE_POWER_TOTAL = 0x141;
public static final int ADDR_DATA_APPARENT_POWER_TOTAL = 0x142;
public static final int ADDR_DATA_POWER_FACTOR_TOTAL = 0x143;
public static final int ADDR_DATA_FREQUENCY = 0x144;
public static final int ADDR_DATA_ACTIVE_POWER_P1 = 0x145;
public static final int ADDR_DATA_REACTIVE_POWER_P1 = 0x146;
public static final int ADDR_DATA_APPARENT_POWER_P1 = 0x147;
public static final int ADDR_DATA_POWER_FACTOR_P1 = 0x148;
public static final int ADDR_DATA_ACTIVE_POWER_P2 = 0x149;
public static final int ADDR_DATA_REACTIVE_POWER_P2 = 0x14A;
public static final int ADDR_DATA_APPARENT_POWER_P2 = 0x14B;
public static final int ADDR_DATA_POWER_FACTOR_P2 = 0x14C;
public static final int ADDR_DATA_ACTIVE_POWER_P3 = 0x14D;
public static final int ADDR_DATA_REACTIVE_POWER_P3 = 0x14E;
public static final int ADDR_DATA_APPARENT_POWER_P3 = 0x14F;
public static final int ADDR_DATA_POWER_FACTOR_P3 = 0x150;
public static final int ADDR_DATA_PHASE_ROTATION = 0x151;
// energy
public static final int ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT = 0x160; // length 2
public static final int ADDR_DATA_TOTAL_ACTIVE_ENERGY_EXPORT = 0x162;
public static final int ADDR_DATA_TOTAL_REACTIVE_ENERGY_IMPORT = 0x164;
public static final int ADDR_DATA_TOTAL_REACTIVE_ENERGY_EXPORT = 0x166;
// units
public static final int ADDR_DATA_ENERGY_UNIT = 0x17E;
public static final int ADDR_DATA_PT_RATIO = 0x200A;
public static final int ADDR_DATA_CT_RATIO = 0x200B;
private static final int ADDR_INPUT_REG_START = ADDR_DATA_I1;
private static final int ADDR_INPUT_REG_END = ADDR_DATA_ENERGY_UNIT;
private final int[] inputRegisters;
private float ptRatio = 1;
private float ctRatio = 1;
private int energyUnit;
private int energyFactor;
private UnitFactor unitFactor = UnitFactor.EM5610;
private long dataTimestamp = 0;
/**
* Default constructor.
*/
public EM5600Data() {
super();
setEnergyUnit(0);
inputRegisters = new int[ADDR_INPUT_REG_END - ADDR_INPUT_REG_START + 1];
Arrays.fill(inputRegisters, 0);
}
/**
* Copy constructor.
*
* @param other
* the object to copy
*/
public EM5600Data(EM5600Data other) {
super();
inputRegisters = other.inputRegisters.clone();
ptRatio = other.ptRatio;
ctRatio = other.ctRatio;
energyUnit = other.energyUnit;
energyFactor = other.energyFactor;
unitFactor = other.unitFactor;
dataTimestamp = other.dataTimestamp;
}
@Override
public String toString() {
return "EM5600Data{U=" + unitFactor + ",PTR=" + ptRatio + ",CTR=" + ctRatio + ",EU="
+ energyUnit + ",EF=" + energyFactor + ",V1=" + getVoltage(ADDR_DATA_V_L1_NEUTRAL)
+ ",V2=" + getVoltage(ADDR_DATA_V_L2_NEUTRAL) + ",V3="
+ getVoltage(ADDR_DATA_V_L3_NEUTRAL) + ",A1=" + getCurrent(ADDR_DATA_I1) + ",A2="
+ getCurrent(ADDR_DATA_I2) + ",A3=" + getCurrent(ADDR_DATA_I3) + ",PF="
+ getPowerFactor(ADDR_DATA_POWER_FACTOR_TOTAL) + ",Hz="
+ getFrequency(ADDR_DATA_FREQUENCY) + ",W=" + getPower(ADDR_DATA_ACTIVE_POWER_TOTAL)
+ ",var=" + getPower(ADDR_DATA_REACTIVE_POWER_TOTAL) + ",VA="
+ getPower(ADDR_DATA_APPARENT_POWER_TOTAL) + ",Wh-I="
+ getEnergy(ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT) + ",varh-I="
+ getEnergy(ADDR_DATA_TOTAL_REACTIVE_ENERGY_IMPORT) + ",Wh-E="
+ getEnergy(ADDR_DATA_TOTAL_ACTIVE_ENERGY_EXPORT) + ",varh-E="
+ getEnergy(ADDR_DATA_TOTAL_REACTIVE_ENERGY_EXPORT) + "}";
}
/**
* Read data from the meter and store it internally. If data is populated
* successfully, the {@link dataTimestamp} will be updated to the current
* system time. <b>Note</b> this does <b>not</b> call
* {@link #readEnergyRatios(ModbusConnection)}. Those values are not
* expected to change much, so those values should be called manually as
* needed.
*
* @param conn
* the Modbus connection
*/
public void readMeterData(final ModbusConnection conn) {
int[] data = conn.readInts(ADDR_DATA_I1, (ADDR_DATA_PHASE_ROTATION - ADDR_DATA_I1 + 1));
// re-read some data to get signed values... these are mixed with unsigned values
// so we are reading some registers twice, but in fewer transactions
short[] signedData = conn.readSignedShorts(ADDR_DATA_ACTIVE_POWER_TOTAL,
(ADDR_DATA_POWER_FACTOR_P3 - ADDR_DATA_APPARENT_POWER_TOTAL + 1));
final int signedDataOffset = ADDR_DATA_ACTIVE_POWER_TOTAL - ADDR_DATA_I1;
for ( int i = 0; i < signedData.length; i++ ) {
final int addr = ADDR_DATA_ACTIVE_POWER_TOTAL + i;
switch (addr) {
case ADDR_DATA_APPARENT_POWER_P1:
case ADDR_DATA_APPARENT_POWER_P2:
case ADDR_DATA_APPARENT_POWER_P3:
case ADDR_DATA_APPARENT_POWER_TOTAL:
case ADDR_DATA_FREQUENCY:
// these are unsigned values, so skip as they were populated in the first tx
continue;
default:
data[i + signedDataOffset] = signedData[i];
}
}
setCurrentVoltagePower(data);
data = conn.readInts(ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT, (ADDR_DATA_ENERGY_UNIT
- ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT + 1));
setEnergy(data);
dataTimestamp = System.currentTimeMillis();
}
/**
* Read the PT ratio, and CT ratio values from the meter. If the
* {@code unitFactor} is set to {@link UnitFactor#EM5610} then this method
* will not actually query the meter, as the values are fixed for that
* meter.
*
* @param conn
* the Modbus connection
*/
public void readEnergyRatios(final ModbusConnection conn) {
int[] eUnit = conn.readInts(ADDR_DATA_ENERGY_UNIT, 1);
if ( eUnit != null && eUnit.length > 0 ) {
// a value of 0 here means we should treat the energy unit as 1, e.g. 5610
int eu = eUnit[0];
inputRegisters[ADDR_DATA_ENERGY_UNIT - ADDR_INPUT_REG_START] = eu;
setEnergyUnit(eu < 0 ? 0 : eu);
}
int[] transformerRatios = conn.readInts(ADDR_DATA_PT_RATIO, 2);
if ( transformerRatios != null && transformerRatios.length > 1 ) {
int ptr = transformerRatios[0];
ptRatio = (ptr < 1 ? 1 : ptr / 10);
int ctr = transformerRatios[1];
ctRatio = (ctr < 1 ? 1 : ctr / 10);
}
}
/**
* Get the system time for the last time data was successfully populated via
* {@link #readMeterData(ModbusConnection)}.
*
* @return the system time
*/
public long getDataTimestamp() {
return dataTimestamp;
}
/**
* Set the raw Modbus current, voltage, and power register data. This
* corresponds to the register range 0x130 - 0x151.
*
* @param current
* the data
*/
public void setCurrentVoltagePower(int[] data) {
if ( data == null ) {
return;
}
System.arraycopy(data, 0, inputRegisters, (ADDR_DATA_I1 - ADDR_INPUT_REG_START),
Math.min(data.length, (ADDR_DATA_PHASE_ROTATION - ADDR_DATA_I1 + 1)));
}
/**
* Set the raw Modbus energy register data. This corresponds to the register
* range 0x160 - 0x17E.
*
* @param power
* the data
*/
public void setEnergy(int[] energy) {
if ( energy == null ) {
return;
}
System.arraycopy(energy, 0, inputRegisters,
(ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT - ADDR_INPUT_REG_START), Math.min(energy.length,
(ADDR_DATA_ENERGY_UNIT - ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT + 1)));
}
/**
* Get the value of an input register. This will not return useful data
* until after {@link #readMeterData(ModbusConnection)} has been called. Use
* the various {@code ADDR_*} constants to query specific supported
* registers.
*
* @param addr
* the input register address to get the value of
* @return the register value
* @throws IllegalArgumentException
* if the provided address is out of range
*/
public int getInputRegister(final int addr) {
inputRegisterRangeCheck(addr);
return inputRegisters[addr - ADDR_DATA_I1];
}
private void inputRegisterRangeCheck(final int addr) {
if ( addr < ADDR_INPUT_REG_START || addr > ADDR_INPUT_REG_END ) {
throw new IllegalArgumentException("Input register ddress " + addr + " out of range");
}
}
/**
* Get the {@link UnitFactor} to use for calculating effective values.
*
* @return the unit factor
*/
public UnitFactor getUnitFactor() {
return unitFactor;
}
/**
* Set the {@link UnitFactor} to use for calculating effective values. This
* defaults to {@link EM5610}.
*
* @param unitFactor
*/
public void setUnitFactor(UnitFactor unitFactor) {
assert unitFactor != null;
this.unitFactor = unitFactor;
}
/**
* Get the effective PT ratio. This will return {@code 1} unless the
* {@code unitFactor} is set to {@code EM5630_5A}, in which case it will
* return {@link #getPtRatio()} which has presumably be set by reading from
* the meter via {@link #readEnergyRatios(ModbusConnection)}.
*
* @return effective PT ratio
*/
public float getEffectivePtRatio() {
return (unitFactor == UnitFactor.EM5630_5A ? ptRatio : 1);
}
/**
* Get the effective CT ratio. This will return {@code 1} unless the
* {@code unitFactor} is set to {@code EM5630_5A}, in which case it will
* return {@link #getCtRatio()} which has presumably be set by reading from
* the meter via {@link #readEnergyRatios(ModbusConnection)}.
*
* @return effective PT ratio
*/
public float getEffectiveCtRatio() {
return (unitFactor == UnitFactor.EM5630_5A ? ctRatio : 1);
}
/**
* Get an effective voltage value in V.
*
* @param addr
* the register address to read
* @return the value interpreted as a voltage value
*/
public float getVoltage(int addr) {
return (getInputRegister(addr) * getEffectivePtRatio() * unitFactor.getU().floatValue());
}
/**
* Get an effective current value in A.
*
* @param addr
* the register address to read
* @return the value interpreted as a current value
*/
public float getCurrent(int addr) {
return (getInputRegister(addr) * getEffectiveCtRatio() * unitFactor.getA().floatValue());
}
/**
* Get an effective frequency value in Hz.
*
* @param addr
* the register address to read
* @return the value interpreted as a frequency value
*/
public float getFrequency(int addr) {
return (getInputRegister(addr) * 2) / 1000f;
}
/**
* Get an effective power factor value (cosine of the phase angle).
*
* @param addr
* the register address to read
* @return the value interpreted as a power factor value
*/
public float getPowerFactor(int addr) {
return (getInputRegister(addr) / 10000f);
}
/**
* Get an effective power value in W (active), Var (reactive) or VA
* (apparent).
*
* @param addr
* the register address to read
* @return the value interpreted as a power value
*/
public int getPower(int addr) {
return (int) Math.ceil(getInputRegister(addr) * getEffectivePtRatio() * getEffectiveCtRatio()
* unitFactor.getP().floatValue());
}
/**
* Get an effective energy value in Wh (real), Varh (reactive).
*
* @param addr
* the register address to read
* @return the value interpreted as an energy value
*/
public long getEnergy(int addr) {
inputRegisterRangeCheck(addr);
inputRegisterRangeCheck(addr + 1);
Long l = ModbusHelper.parseInt32(inputRegisters, addr - ADDR_INPUT_REG_START);
return (l == null ? 0 : l.longValue() * energyFactor);
}
public float getPtRatio() {
return ptRatio;
}
public float getCtRatio() {
return ctRatio;
}
public int getEnergyUnit() {
return energyUnit;
}
public void setPtRatio(int ptRatio) {
this.ptRatio = ptRatio;
}
public void setCtRatio(int ctRatio) {
this.ctRatio = ctRatio;
}
public void setEnergyUnit(int energyUnit) {
assert energyUnit >= 0 && energyUnit <= 6;
this.energyUnit = energyUnit;
this.energyFactor = (int) Math.pow(10, energyUnit);
}
/**
* 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 1-based} values, to
* match Schneider's documentation. For example:
*
* <pre>
* EM5600Data{
* 3000: 0x4141, 0x727E
* 3002: 0xFFC0, 0x0000
* ...
* 3240: 0x0000, 0x0000
* }
* </pre>
*
* @return debug string
*/
public String dataDebugString() {
final StringBuilder buf = new StringBuilder("EM5600Data{\n");
EM5600Data snapshot = new EM5600Data(this);
int[] data = snapshot.inputRegisters;
boolean odd = true;
for ( int i = 0; i < data.length; i++ ) {
if ( odd ) {
buf.append("\t").append(String.format("%5d", ADDR_INPUT_REG_START + i + 1)).append(": ");
}
buf.append(String.format("0x%04X", data[i]));
if ( odd ) {
buf.append(", ");
} else {
buf.append("\n");
}
odd = !odd;
}
if ( !odd ) {
buf.append("\n");
}
buf.append("}");
return buf.toString();
}
}