/* ================================================================== * PM3200Support.java - 28/02/2014 2:17:24 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.schneider.meter; import java.util.ArrayList; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import net.solarnetwork.node.DatumDataSource; import net.solarnetwork.node.domain.ACPhase; import net.solarnetwork.node.domain.Datum; import net.solarnetwork.node.io.modbus.ModbusConnection; import net.solarnetwork.node.io.modbus.ModbusDeviceSupport; import net.solarnetwork.node.io.modbus.ModbusHelper; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier; import net.solarnetwork.node.util.ClassUtils; import net.solarnetwork.util.OptionalService; import net.solarnetwork.util.StringUtils; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; import org.joda.time.format.DateTimeFormat; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; /** * Supporting class for the PM3200 series power meter. * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>sourceMapping</dt> * <dd>A mapping of {@link ACPhase} to associated Source ID values to assign to * collected datum. Defaults to a mapping of {@code Total = Main}.</dd> * * <dt>eventAdmin</dt> * <dd>An optional {@link EventAdmin} service to use for posting events.</dd> * </dl> * * @author matt * @version 1.5 */ public class PM3200Support extends ModbusDeviceSupport { public static final Integer ADDR_SYSTEM_METER_NAME = 29; public static final Integer ADDR_SYSTEM_METER_MODEL = 49; public static final Integer ADDR_SYSTEM_METER_MANUFACTURER = 69; public static final Integer ADDR_SYSTEM_METER_SERIAL_NUMBER = 129; public static final Integer ADDR_SYSTEM_METER_MANUFACTURE_DATE = 131; /** The default source ID applied for the total reading values. */ public static final String MAIN_SOURCE_ID = "Main"; private Map<ACPhase, String> sourceMapping = getDefaulSourceMapping(); private OptionalService<EventAdmin> eventAdmin; /** * An instance of {@link PM3200Data} to support keeping the last-read values * of data in memory. */ protected final PM3200Data sample = new PM3200Data(); /** * Get a default {@code sourceMapping} value. This maps only the {@code 0} * source to the value {@code Main}. * * @return mapping */ public static Map<ACPhase, String> getDefaulSourceMapping() { Map<ACPhase, String> result = new EnumMap<ACPhase, String>(ACPhase.class); result.put(ACPhase.Total, MAIN_SOURCE_ID); return result; } /** * Read the name of the meter. * * @param conn * the serial connection * @return the meter name, or <em>null</em> if not available */ public String getMeterName(ModbusConnection conn) { return conn.readString(ADDR_SYSTEM_METER_NAME, 20, true, ModbusConnection.UTF8_CHARSET); } /** * Read the model of the meter. * * @param conn * the serial connection * @return the meter model, or <em>null</em> if not available */ public String getMeterModel(ModbusConnection conn) { return conn.readString(ADDR_SYSTEM_METER_MODEL, 20, true, ModbusConnection.UTF8_CHARSET); } /** * Read the manufacturer of the meter. * * @param conn * the serial connection * @return the meter manufacturer, or <em>null</em> if not available */ public String getMeterManufacturer(ModbusConnection conn) { return conn.readString(ADDR_SYSTEM_METER_MANUFACTURER, 20, true, ModbusConnection.UTF8_CHARSET); } /** * Read the serial number of the meter. * * @param conn * the connection * @return the meter serial number, or <em>null</em> if not available */ public Long getMeterSerialNumber(ModbusConnection conn) { Long result = null; Integer[] data = conn.readValues(ADDR_SYSTEM_METER_SERIAL_NUMBER, 2); if ( data != null && data.length == 2 ) { int longValue = ModbusHelper.getLongWord(data[0], data[1]); result = (long) longValue; } return result; } /** * Read the manufacture date of the meter. * * @param conn * the connection * @return the meter manufacture date, or <em>null</em> if not available */ public LocalDateTime getMeterManufactureDate(ModbusConnection conn) { int[] data = conn.readInts(ADDR_SYSTEM_METER_MANUFACTURE_DATE, 4); return parseDateTime(data); } @Override protected Map<String, Object> readDeviceInfo(ModbusConnection conn) { Map<String, Object> result = new LinkedHashMap<String, Object>(8); String str = getMeterName(conn); if ( str != null ) { result.put(INFO_KEY_DEVICE_NAME, str); } str = getMeterModel(conn); if ( str != null ) { result.put(INFO_KEY_DEVICE_MODEL, str); } str = getMeterManufacturer(conn); if ( result != null ) { result.put(INFO_KEY_DEVICE_MANUFACTURER, str); } LocalDateTime dt = getMeterManufactureDate(conn); if ( dt != null ) { result.put(INFO_KEY_DEVICE_MANUFACTURE_DATE, dt.toLocalDate()); } Long l = getMeterSerialNumber(conn); if ( l != null ) { result.put(INFO_KEY_DEVICE_SERIAL_NUMBER, l); } return result; } /** * Parse a DateTime value from raw Modbus register values. The {@code data} * array is expected to have a length of {@code 4}. * * @param data * the data array * @return the parsed date, or <em>null</em> if not available */ public static LocalDateTime parseDateTime(final int[] data) { LocalDateTime result = null; if ( data != null && data.length == 4 ) { int year = 2000 + (data[0] & 0x7F); int month = (data[1] & 0xF00) >> 8; int day = (data[1] & 0x1F); int hour = (data[2] & 0x1F00) >> 8; int minute = (data[2] & 0x3F); int ms = (data[3]); // this is really seconds + milliseconds int sec = ms / 1000; ms = ms - (sec * 1000); result = new LocalDateTime(year, month, day, hour, minute, sec, ms); } return result; } /** * Parse a 32-bit float value from a data array. * * @param data * the data array * @param offset * the offset within the array to parse the value from * @return the float, or <em>null</em> if not available */ public static Float parseFloat32(Integer[] data, int offset) { return parseBigEndianFloat32(data, offset); } /** * Parse a 64-bit integer value from a data array. * * @param data * the data array * @param offset * the offset within the array to parse the value from * @return the long, or <em>null</em> if not available */ public static Long parseInt64(Integer[] data, int offset) { return parseBigEndianInt64(data, offset); } /** * Set a {@code sourceMapping} Map via an encoded String value. * * <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 mapping * the encoding mapping * @see #getSourceMappingValue() */ public void setSourceMappingValue(String mapping) { Map<String, String> m = StringUtils.commaDelimitedStringToMap(mapping); Map<ACPhase, String> kindMap = new EnumMap<ACPhase, String>(ACPhase.class); if ( m != null ) for ( Map.Entry<String, String> me : m.entrySet() ) { String k = me.getKey(); ACPhase mk; try { mk = ACPhase.valueOf(k); } catch ( RuntimeException e ) { log.info("'{}' is not a valid ACPhase value, ignoring.", k); continue; } kindMap.put(mk, me.getValue()); } setSourceMapping(kindMap); } /** * Get a delimited string representation of the {@link #getSourceMapping()} * map. * * <p> * The format of the {@code mapping} String should be: * </p> * * <pre> * key=val[,key=val,...] * </pre> * * @return the encoded mapping * @see #setSourceMappingValue(String) */ public String getSourceMappingValue() { return StringUtils.delimitedStringFromMap(sourceMapping); } /** * Get a source ID value for a given measurement kind. * * @param kind * the measurement kind * @return the source ID value, or <em>null</em> if not available */ public String getSourceIdForACPhase(ACPhase kind) { return (sourceMapping == null ? null : sourceMapping.get(kind)); } private String getInfoMessage() { String msg = null; try { msg = getDeviceInfoMessage(); } catch ( RuntimeException e ) { log.debug("Error reading info: {}", e.getMessage()); } return (msg == null ? "N/A" : msg); } private String getSampleMessage(PM3200Data data) { if ( data.getDataTimestamp() < 1 ) { return "N/A"; } StringBuilder buf = new StringBuilder(); buf.append("W = ").append(sample.getPower(PM3200Data.ADDR_DATA_ACTIVE_POWER_TOTAL)); buf.append(", VA = ").append(sample.getPower(PM3200Data.ADDR_DATA_APPARENT_POWER_TOTAL)); buf.append(", Wh = ").append(sample.getEnergy(PM3200Data.ADDR_DATA_ACTIVE_ENERGY_IMPORT_TOTAL)); buf.append(", cos \ud835\udf11 = ").append(sample.getEffectiveTotalPowerFactor()); buf.append("; sampled at ").append( DateTimeFormat.forStyle("LS").print(new DateTime(sample.getDataTimestamp()))); return buf.toString(); } public List<SettingSpecifier> getSettingSpecifiers() { PM3200Support defaults = new PM3200Support(); List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(10); results.add(new BasicTitleSettingSpecifier("info", getInfoMessage(), true)); results.add(new BasicTitleSettingSpecifier("sample", getSampleMessage(sample), true)); results.add(new BasicTextFieldSettingSpecifier("uid", defaults.getUid())); 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("sourceMappingValue", defaults .getSourceMappingValue())); return results; } /** * Post a {@link DatumDataSource#EVENT_TOPIC_DATUM_CAPTURED} {@link Event}. * * <p> * This method calls {@link #createDatumCapturedEvent(Datum, Class)} to * create the actual Event, which may be overridden by extending classes. * </p> * * @param datum * the {@link Datum} to post the event for * @param eventDatumType * the Datum class to use for the * {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE} property * @since 1.3 */ protected final void postDatumCapturedEvent(final Datum datum, final Class<? extends Datum> eventDatumType) { EventAdmin ea = (eventAdmin == null ? null : eventAdmin.service()); if ( ea == null || datum == null ) { return; } Event event = createDatumCapturedEvent(datum, eventDatumType); ea.postEvent(event); } /** * Create a new {@link DatumDataSource#EVENT_TOPIC_DATUM_CAPTURED} * {@link Event} object out of a {@link Datum}. * * <p> * This method will populate all simple properties of the given * {@link Datum} into the event properties, along with the * {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE}. * * @param datum * the datum to create the event for * @param eventDatumType * the Datum class to use for the * {@link DatumDataSource#EVENT_DATUM_CAPTURED_DATUM_TYPE} property * @return the new Event instance * @since 1.3 */ protected Event createDatumCapturedEvent(final Datum datum, final Class<? extends Datum> eventDatumType) { Map<String, Object> props = ClassUtils.getSimpleBeanProperties(datum, null); props.put(DatumDataSource.EVENT_DATUM_CAPTURED_DATUM_TYPE, eventDatumType.getName()); log.debug("Created {} event with props {}", DatumDataSource.EVENT_TOPIC_DATUM_CAPTURED, props); return new Event(DatumDataSource.EVENT_TOPIC_DATUM_CAPTURED, props); } public boolean isCaptureTotal() { return (sourceMapping != null && sourceMapping.containsKey(ACPhase.Total)); } public boolean isCapturePhaseA() { return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseA)); } public boolean isCapturePhaseB() { return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseB)); } public boolean isCapturePhaseC() { return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseC)); } public Map<ACPhase, String> getSourceMapping() { return sourceMapping; } public void setSourceMapping(Map<ACPhase, String> sourceMapping) { this.sourceMapping = sourceMapping; } public OptionalService<EventAdmin> getEventAdmin() { return eventAdmin; } public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) { this.eventAdmin = eventAdmin; } }