/* ================================================================== * EM5600Support.java - Mar 26, 2014 6:00:50 AM * * 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 static net.solarnetwork.node.hw.hc.EM5600Data.ADDR_SYSTEM_METER_MANUFACTURE_DATE; import static net.solarnetwork.node.hw.hc.EM5600Data.ADDR_SYSTEM_METER_MODEL; import static net.solarnetwork.node.hw.hc.EM5600Data.ADDR_SYSTEM_METER_SERIAL_NUMBER; import java.util.ArrayList; import java.util.EnumMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import net.solarnetwork.domain.GeneralDatumMetadata; import net.solarnetwork.node.DatumDataSource; import net.solarnetwork.node.DatumMetadataService; 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.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 EM5600 series power meter. * * <p> * The EM5600 series watt-hour meter supports the following serial port * configuration: * </p> * * <ul> * <li><b>Baud</b> - 1200, 2400, 4800, 9600, 19200</li> * <li><b>Mode</b> - RTU</li> * <li><b>Start bit</b> - 1</li> * <li><b>Data bits</b> - 8</li> * <li><b>Parity</b> - None</li> * <li><b>Stop bit</b> - 1</li> * <li><b>Error checking</b> - CRC</li> * </ul> * * <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> * * <dt>datumMetadataService</dt> * <dd>An optional {@link DatumMetadataService} to use for managing metadata.</dd> * </dl> * * @author matt * @version 2.1 */ public class EM5600Support extends ModbusDeviceSupport { /** 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; private OptionalService<DatumMetadataService> datumMetadataService; private final ConcurrentMap<String, GeneralDatumMetadata> sourceMetadataCache = new ConcurrentHashMap<String, GeneralDatumMetadata>( 4); /** * An instance of {@link EM5600Data} to support keeping the last-read values * of data in memory. */ protected final EM5600Data sample = new EM5600Data(); private UnitFactor unitFactor = null; /** * 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; } /** * 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)); } /** * Get settings supported by this class. Extending classes can use this to * support the Settings API. * * @return list of settings */ public List<SettingSpecifier> getSettingSpecifiers() { EM5600Support defaults = new EM5600Support(); List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(10); results.add(new BasicTitleSettingSpecifier("info", getInfoMessage(), true)); results.add(new BasicTitleSettingSpecifier("sample", getSampleMessage(sample), true)); /* * BasicRadioGroupSettingSpecifier unitFactorSpec = new * BasicRadioGroupSettingSpecifier( "unitFactor", * defaults.getUnitFactor().toString()); Map<String, String> radioValues * = new LinkedHashMap<String, String>(3); for ( UnitFactor f : * UnitFactor.values() ) { radioValues.put(f.toString(), * f.getDisplayName()); } unitFactorSpec.setValueTitles(radioValues); * results.add(unitFactorSpec); */ 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; } private String getInfoMessage() { String msg = null; try { msg = getDeviceInfoMessage(); } catch ( RuntimeException e ) { log.debug("Error reading {} info: {}", getUnitId(), e.getMessage()); } return (msg == null ? "N/A" : msg); } private String getSampleMessage(EM5600Data data) { if ( data.getDataTimestamp() < 1 ) { return "N/A"; } StringBuilder buf = new StringBuilder(); buf.append("W = ").append(sample.getPower(EM5600Data.ADDR_DATA_ACTIVE_POWER_TOTAL)); buf.append(", VA = ").append(sample.getPower(EM5600Data.ADDR_DATA_APPARENT_POWER_TOTAL)); buf.append(", Wh = ").append(sample.getEnergy(EM5600Data.ADDR_DATA_TOTAL_ACTIVE_ENERGY_IMPORT)); buf.append(", \ud835\udf11 = ").append( sample.getPowerFactor(EM5600Data.ADDR_DATA_POWER_FACTOR_TOTAL)); buf.append("; sampled at ").append( DateTimeFormat.forStyle("LS").print(new DateTime(sample.getDataTimestamp()))); return buf.toString(); } @Override protected Map<String, Object> readDeviceInfo(ModbusConnection conn) { // note the order of these elements determines the output of getDeviceInfoMessage() Map<String, Object> result = new LinkedHashMap<String, Object>(8); String str; Integer i; i = getMeterModel(conn); if ( i != null ) { result.put(INFO_KEY_DEVICE_MODEL, i); } LocalDateTime dt = getMeterManufactureDate(conn); if ( dt != null ) { result.put(INFO_KEY_DEVICE_MANUFACTURE_DATE, dt.toLocalDate()); } str = getMeterSerialNumber(conn); if ( str != null ) { result.put(INFO_KEY_DEVICE_SERIAL_NUMBER, str); } return result; } /** * Read the serial number of the meter. * * @param conn * the connection * @return the meter serial number, or <em>null</em> if not available */ public String getMeterSerialNumber(ModbusConnection conn) { return conn.readString(ADDR_SYSTEM_METER_SERIAL_NUMBER, 4, true, ModbusConnection.ASCII_CHARSET); } /** * Read the model number of the meter. The {@code unitFactor} will also be * populated if not already available. * * @param conn * the connection * @return the meter model number, or <em>null</em> if not available */ public Integer getMeterModel(ModbusConnection conn) { Integer[] data = conn.readValues(ADDR_SYSTEM_METER_MODEL, 1); if ( data != null && data.length > 0 ) { if ( unitFactor == null ) { switch (data[0].intValue()) { case 5630: setUnitFactor(UnitFactor.EM5630_30A); break; // TODO: how tell if EM5630_5A default: setUnitFactor(UnitFactor.EM5610); break; } } return data[0]; } return null; } /** * 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, 2); return parseDate(data); } /** * Parse a DateTime value from raw Modbus register values. The {@code data} * array is expected to have a length of {@code 2} and follow the documented * F10 and F9 formats. * * @param data * the data array * @return the parsed date, or <em>null</em> if not available */ public static LocalDateTime parseDate(final int[] data) { LocalDateTime result = null; if ( data != null && data.length == 2 ) { int day = (data[0] & 0x1F00) >> 8; // 1 - 31 int year = 2000 + ((data[1] & 0xFF00) >> 8); // 0 - 255 int month = (data[1] & 0xF); //1-12 result = new LocalDateTime(year, month, day, 0, 0, 0, 0); } return result; } /** * 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 2.1 */ 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 2.1 */ 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); } /** * Add source metadata using the configured {@link DatumMetadataService} (if * available). The metadata will be cached so that subseqent calls to this * method with the same metadata value will not try to re-save the unchanged * value. This method will catch all exceptions and silently discard them. * * @param sourceId * the source ID to add metadata to * @param meta * the metadata to add * @param returns * <em>true</em> if the metadata was saved successfully, or does not * need to be updated */ protected boolean addSourceMetadata(final String sourceId, final GeneralDatumMetadata meta) { GeneralDatumMetadata cached = sourceMetadataCache.get(sourceId); if ( cached != null && meta.equals(cached) ) { // we've already posted this metadata... don't bother doing it again log.debug("Source {} metadata already added, not posting again", sourceId); return true; } DatumMetadataService service = null; if ( datumMetadataService != null ) { service = datumMetadataService.service(); } if ( service == null ) { return false; } try { service.addSourceMetadata(sourceId, meta); sourceMetadataCache.put(sourceId, meta); return true; } catch ( Exception e ) { log.debug("Error saving source {} metadata: {}", sourceId, e.getMessage()); } return false; } 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 UnitFactor getUnitFactor() { return unitFactor; } public void setUnitFactor(UnitFactor unitFactor) { assert unitFactor != null; this.unitFactor = unitFactor; this.sample.setUnitFactor(unitFactor); } public OptionalService<EventAdmin> getEventAdmin() { return eventAdmin; } public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) { this.eventAdmin = eventAdmin; } public OptionalService<DatumMetadataService> getDatumMetadataService() { return datumMetadataService; } public void setDatumMetadataService(OptionalService<DatumMetadataService> datumMetadataService) { this.datumMetadataService = datumMetadataService; } }