/* ==================================================================
* SDMSupport.java - 26/01/2016 7:21:53 am
*
* 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.ArrayList;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
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.settings.SettingSpecifier;
import net.solarnetwork.node.settings.support.BasicRadioGroupSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicTitleSettingSpecifier;
import net.solarnetwork.node.settings.support.BasicToggleSettingSpecifier;
import net.solarnetwork.node.util.ClassUtils;
import net.solarnetwork.util.OptionalService;
import net.solarnetwork.util.StringUtils;
/**
* Supporting class for the SDM series power meters.
*
* @author matt
* @version 1.1
*/
public class SDMSupport extends ModbusDeviceSupport {
/** The default source ID applied for the total reading values. */
public static final String MAIN_SOURCE_ID = "Main";
// a mapping of AC phase to source ID
private Map<ACPhase, String> sourceMapping = getDefaulSourceMapping();
// an optional EventAdmin service
private OptionalService<EventAdmin> eventAdmin;
// the type of device to use
private SDMDeviceType deviceType = SDMDeviceType.SDM120;
// the "installed backwards" setting
private boolean backwards = false;
/**
* An instance of {@link SDMData} to support keeping the last-read values of
* data in memory.
*/
protected SDMData sample = new SDM120Data();
/**
* Get a default {@code sourceMapping} value. This maps only the
* {@code Total} phase 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;
}
@Override
protected Map<String, Object> readDeviceInfo(ModbusConnection conn) {
sample.readControlData(conn);
return sample.getDeviceInfo();
}
/**
* 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(SDMData data) {
if ( data.getMeterDataTimestamp() < 1 ) {
return "N/A";
}
StringBuilder buf = new StringBuilder();
buf.append(data.getOperationStatusMessage());
buf.append("; sampled at ").append(
DateTimeFormat.forStyle("LS").print(new DateTime(sample.getMeterDataTimestamp())));
return buf.toString();
}
public List<SettingSpecifier> getSettingSpecifiers() {
SDMSupport defaults = new SDMSupport();
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']",
"Modbus Port"));
results.add(new BasicTextFieldSettingSpecifier("unitId", String.valueOf(defaults.getUnitId())));
BasicRadioGroupSettingSpecifier deviceTypeSpec = new BasicRadioGroupSettingSpecifier(
"deviceTypeValue", defaults.getDeviceTypeValue());
Map<String, String> deviceTypeValues = new LinkedHashMap<String, String>(3);
for ( SDMDeviceType model : SDMDeviceType.values() ) {
deviceTypeValues.put(model.toString(), model.toString());
}
deviceTypeSpec.setValueTitles(deviceTypeValues);
results.add(deviceTypeSpec);
results.add(new BasicToggleSettingSpecifier("backwards", Boolean.valueOf(defaults.backwards)));
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);
}
/**
* Test if the {@code Total} phase should be captured.
*
* @return <em>true</em> if the {@code sourceMapping} contains a
* {@code Total} key
*/
public boolean isCaptureTotal() {
return (sourceMapping != null && sourceMapping.containsKey(ACPhase.Total));
}
/**
* Test if the {@code PhaseA} phase should be captured.
*
* @return <em>true</em> if the {@code sourceMapping} contains a
* {@code PhaseA} key
*/
public boolean isCapturePhaseA() {
return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseA));
}
/**
* Test if the {@code PhaseB} phase should be captured.
*
* @return <em>true</em> if the {@code sourceMapping} contains a
* {@code PhaseB} key
*/
public boolean isCapturePhaseB() {
return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseB));
}
/**
* Test if the {@code PhaseC} phase should be captured.
*
* @return <em>true</em> if the {@code sourceMapping} contains a
* {@code PhaseC} key
*/
public boolean isCapturePhaseC() {
return (sourceMapping != null && sourceMapping.containsKey(ACPhase.PhaseC));
}
/**
* Get the configured mapping from AC phase constants to source ID values.
*
* @return The source mapping.
*/
public Map<ACPhase, String> getSourceMapping() {
return sourceMapping;
}
/**
* Configure a mapping from AC phase constants to source ID values.
*
* @param sourceMapping
* The source mappinng to set.
*/
public void setSourceMapping(Map<ACPhase, String> sourceMapping) {
this.sourceMapping = sourceMapping;
}
/**
* Get the configured optional EventAdmin service.
*
* @return The service.
*/
public OptionalService<EventAdmin> getEventAdmin() {
return eventAdmin;
}
/**
* Set an optional {@code EventAdmin} service for posting events with.
*
* @param eventAdmin
* The service to use.
*/
public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) {
this.eventAdmin = eventAdmin;
}
public SDMDeviceType getDeviceType() {
return deviceType;
}
/**
* Set the type of device to use. If this value changes, any cached sample
* data will be cleared.
*
* @param deviceType
* The type of device to use.
*/
public void setDeviceType(final SDMDeviceType deviceType) {
if ( deviceType == null ) {
throw new IllegalArgumentException("The deviceType cannot be null.");
}
if ( this.deviceType.equals(deviceType) ) {
// no change
return;
}
this.deviceType = deviceType;
setupNewSample(deviceType);
}
private void setupNewSample(final SDMDeviceType deviceType) {
switch (deviceType) {
case SDM630:
sample = new SDM630Data(backwards);
break;
default:
sample = new SDM120Data(backwards);
break;
}
}
/**
* Get the device type, as a string.
*
* @return The device type, as a string.
*/
public String getDeviceTypeValue() {
final SDMDeviceType type = getDeviceType();
return (type == null ? "" : type.toString());
}
/**
* Set the device type, as a string.
*
* @param type
* The {@link SDMDeviceType} string value to set.
*/
public void setDeviceTypeValue(String type) {
try {
setDeviceType(SDMDeviceType.valueOf(type));
} catch ( IllegalArgumentException e ) {
// not supported type
}
}
/**
* Set the backwards setting.
*
* @param backwards
* the backwards setting
* @since 1.1
*/
public void setBackwards(boolean value) {
if ( value == backwards ) {
return;
}
this.backwards = value;
setupNewSample(this.deviceType);
}
}