/* ==================================================================
* ModbusPCMController.java - Jul 10, 2013 7:14:40 AM
*
* Copyright 2007-2013 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.control.sma.pcm;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Date;
import java.util.List;
import java.util.Map;
import net.solarnetwork.domain.NodeControlInfo;
import net.solarnetwork.domain.NodeControlPropertyType;
import net.solarnetwork.node.DatumDataSource;
import net.solarnetwork.node.NodeControlProvider;
import net.solarnetwork.node.domain.Datum;
import net.solarnetwork.node.domain.NodeControlInfoDatum;
import net.solarnetwork.node.io.modbus.ModbusConnection;
import net.solarnetwork.node.io.modbus.ModbusConnectionAction;
import net.solarnetwork.node.io.modbus.ModbusDeviceSupport;
import net.solarnetwork.node.reactor.Instruction;
import net.solarnetwork.node.reactor.InstructionHandler;
import net.solarnetwork.node.reactor.InstructionStatus.InstructionState;
import net.solarnetwork.node.settings.SettingSpecifier;
import net.solarnetwork.node.settings.SettingSpecifierProvider;
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 org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.springframework.context.MessageSource;
/**
* Toggle four Modbus "coil" type addresses to control the SMA Power Control
* Module.
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>d1Address</dt>
* <dd>The Modbus address for the PCM D1 input.</dd>
* <dt>d2Address</dt>
* <dd>The Modbus address for the PCM D2 input.</dd>
* <dt>d3Address</dt>
* <dd>The Modbus address for the PCM D3 input.</dd>
* <dt>d4Address</dt>
* <dd>The Modbus address for the PCM D4 input.</dd>
*
* <dt>eventAdmin</dt>
* <dd>An {@link EventAdmin} to publish events with.</dd>
* </dl>
*
* @author matt
* @version 1.4
*/
public class ModbusPCMController extends ModbusDeviceSupport implements SettingSpecifierProvider,
NodeControlProvider, InstructionHandler {
/**
* The suffix added to the configured control ID to handle percent-based PCM
* values.
*
* @since 1.3
*/
public static final String PERCENT_CONTROL_ID_SUFFIX = "?percent";
private Integer d1Address = 0x4000;
private Integer d2Address = 0x4002;
private Integer d3Address = 0x4004;
private Integer d4Address = 0x4006;
private String controlId = "/power/pcm/1";
private String groupUID;
private int sampleCacheSeconds = 1;
private OptionalService<EventAdmin> eventAdmin;
private MessageSource messageSource;
private final long sampleCaptureDate = 0;
private BitSet cachedSample = null;
private boolean isCachedSampleExpired() {
final long lastReadDiff = System.currentTimeMillis() - sampleCaptureDate;
if ( lastReadDiff > (sampleCacheSeconds * 1000) ) {
return true;
}
return false;
}
@Override
protected Map<String, Object> readDeviceInfo(ModbusConnection conn) {
return null;
}
/**
* Get the values of the D1 - D4 discreet values, as a BitSet.
*
* @return BitSet, with index 0 representing D1 and index 1 representing D2,
* etc.
*/
private synchronized BitSet currentDiscreetValue() throws IOException {
BitSet result;
if ( isCachedSampleExpired() ) {
result = performAction(new ModbusConnectionAction<BitSet>() {
@Override
public BitSet doWithConnection(ModbusConnection conn) throws IOException {
return conn.readDiscreetValues(new Integer[] { d1Address, d2Address, d3Address,
d4Address }, 1);
}
});
log.debug("Read discreet PCM values: {}", result);
Integer status = integerValueForBitSet(result);
postControlCapturedEvent(newNodeControlInfoDatum(getPercentControlId(), status, true));
cachedSample = result;
} else {
result = cachedSample;
}
return result;
}
/**
* Get the status value of the PCM, as an Integer.
*
* <p>
* This returns the overall vale of the PCM, as an integer between 0 and 15.
* A value of 0 represent a 0% output setting, while 15 represents 100%.
* </p>
*
* @return an integer between 0 and 15
*/
private Integer integerValueForBitSet(BitSet bits) {
return ((bits.get(0) ? 1 : 0) | ((bits.get(1) ? 1 : 0) << 1) | ((bits.get(2) ? 1 : 0) << 2) | ((bits
.get(3) ? 1 : 0) << 3));
}
private static final int PCM_LEVEL_0 = 0;
private static final int PCM_LEVEL_1 = 5;
private static final int PCM_LEVEL_2 = 10;
private static final int PCM_LEVEL_3 = 16;
private static final int PCM_LEVEL_4 = 23;
private static final int PCM_LEVEL_5 = 30;
private static final int PCM_LEVEL_6 = 36;
private static final int PCM_LEVEL_7 = 42;
private static final int PCM_LEVEL_8 = 50;
private static final int PCM_LEVEL_9 = 57;
private static final int PCM_LEVEL_10 = 65;
private static final int PCM_LEVEL_11 = 72;
private static final int PCM_LEVEL_12 = 80;
private static final int PCM_LEVEL_13 = 86;
private static final int PCM_LEVEL_14 = 93;
private static final int PCM_LEVEL_15 = 100;
/**
* Get the approximate power output setting, from 0 to 100.
*
* <p>
* These values are described in the SMA documentation, it is not a direct
* percentage value derived from the value itself.
* </p>
*/
private Integer percentValueForIntegerValue(Integer val) {
switch (val) {
case 1:
return PCM_LEVEL_1;
case 2:
return PCM_LEVEL_2;
case 3:
return PCM_LEVEL_3;
case 4:
return PCM_LEVEL_4;
case 5:
return PCM_LEVEL_5;
case 6:
return PCM_LEVEL_6;
case 7:
return PCM_LEVEL_7;
case 8:
return PCM_LEVEL_8;
case 9:
return PCM_LEVEL_9;
case 10:
return PCM_LEVEL_10;
case 11:
return PCM_LEVEL_11;
case 12:
return PCM_LEVEL_12;
case 13:
return PCM_LEVEL_13;
case 14:
return PCM_LEVEL_14;
default:
return (val < 1 ? PCM_LEVEL_0 : PCM_LEVEL_15);
}
}
/**
* Get the appropriate power output value, from 0 to 15, from an integer
* percentage (0-100). Note that the value is floored, such that the PCM
* value can never be larger than the percentage value passed in.
*
* @param percent
* an integer percentage from 0-100
* @return a PCM output value from 0-15
*/
private Integer pcmValueForPercentValue(Integer percent) {
final int p = (percent == null ? 0 : percent.intValue());
if ( p < PCM_LEVEL_1 ) {
return 0;
}
if ( p < PCM_LEVEL_2 ) {
return 1;
}
if ( p < PCM_LEVEL_3 ) {
return 2;
}
if ( p < PCM_LEVEL_4 ) {
return 3;
}
if ( p < PCM_LEVEL_5 ) {
return 4;
}
if ( p < PCM_LEVEL_6 ) {
return 5;
}
if ( p < PCM_LEVEL_7 ) {
return 6;
}
if ( p < PCM_LEVEL_8 ) {
return 7;
}
if ( p < PCM_LEVEL_9 ) {
return 8;
}
if ( p < PCM_LEVEL_10 ) {
return 9;
}
if ( p < PCM_LEVEL_11 ) {
return 10;
}
if ( p < PCM_LEVEL_12 ) {
return 11;
}
if ( p < PCM_LEVEL_13 ) {
return 12;
}
if ( p < PCM_LEVEL_14 ) {
return 13;
}
if ( p < PCM_LEVEL_15 ) {
return 14;
}
// all systems go!
return 15;
}
private synchronized boolean setPCMStatus(Integer desiredValue) {
final BitSet bits = new BitSet(4);
final int v = desiredValue;
for ( int i = 0; i < 4; i++ ) {
bits.set(i, ((v >> i) & 1) == 1);
}
log.info("Setting PCM status to {} ({}%)", desiredValue,
percentValueForIntegerValue(desiredValue));
final Integer[] addresses = new Integer[] { d1Address, d2Address, d3Address, d4Address };
try {
return performAction(new ModbusConnectionAction<Boolean>() {
@Override
public Boolean doWithConnection(ModbusConnection conn) throws IOException {
return conn.writeDiscreetValues(addresses, bits);
}
});
} catch ( IOException e ) {
log.error("Error communicating with PCM: {}", e.getMessage());
}
return false;
}
// NodeControlProvider
private String getPercentControlId() {
return controlId + PERCENT_CONTROL_ID_SUFFIX;
}
@Override
public String getUID() {
return getControlId();
}
@Override
public List<String> getAvailableControlIds() {
return Arrays.asList(controlId, getPercentControlId());
}
@Override
public NodeControlInfo getCurrentControlInfo(String controlId) {
// read the control's current status
log.debug("Reading PCM {} status", controlId);
NodeControlInfoDatum result = null;
try {
Integer value = integerValueForBitSet(currentDiscreetValue());
result = newNodeControlInfoDatum(controlId, value,
controlId.endsWith(PERCENT_CONTROL_ID_SUFFIX));
} catch ( Exception e ) {
log.error("Error reading PCM {} status: {}", controlId, e.getMessage());
}
return result;
}
private NodeControlInfoDatum newNodeControlInfoDatum(String controlId, Integer status,
boolean asPercent) {
NodeControlInfoDatum info = new NodeControlInfoDatum();
info.setCreated(new Date());
info.setSourceId(controlId);
info.setType(NodeControlPropertyType.Integer);
info.setReadonly(false);
if ( asPercent ) {
info.setValue(percentValueForIntegerValue(status).toString());
} else {
info.setValue(status.toString());
}
return info;
}
// InstructionHandler
@Override
public boolean handlesTopic(String topic) {
return (InstructionHandler.TOPIC_SET_CONTROL_PARAMETER.equals(topic) || InstructionHandler.TOPIC_DEMAND_BALANCE
.equals(topic));
}
@Override
public InstructionState processInstruction(Instruction instruction) {
// look for a parameter name that matches a control ID
InstructionState result = null;
log.debug("Inspecting instruction {} against control {}", instruction.getTopic(), controlId);
final String percentControlId = getPercentControlId();
for ( String paramName : instruction.getParameterNames() ) {
log.trace("Got instruction parameter {}", paramName);
if ( controlId.equals(paramName) || percentControlId.equals(paramName) ) {
String str = instruction.getParameterValue(paramName);
// by default, treat parameter value as a decimal integer, value between 0-15
Integer desiredValue = Integer.parseInt(str);
if ( paramName.equals(percentControlId)
|| InstructionHandler.TOPIC_DEMAND_BALANCE.equals(instruction.getTopic()) ) {
// treat as a percentage integer 0-100, translate to 0-15
Integer val = pcmValueForPercentValue(desiredValue);
log.info("Percent output request to {}%; PCM output to be capped at {} ({}%)",
desiredValue, val, percentValueForIntegerValue(val));
desiredValue = val;
}
if ( setPCMStatus(desiredValue) ) {
result = InstructionState.Completed;
} else {
result = InstructionState.Declined;
}
}
}
return result;
}
/**
* Post a {@link NodeControlProvider#EVENT_TOPIC_CONTROL_INFO_CAPTURED}
* {@link Event}.
*
* <p>
* This method calls {@link #createControlCapturedEvent(NodeControlInfo)} to
* create the actual Event, which may be overridden by extending classes.
* </p>
*
* @param info
* the {@link NodeControlInfo} to post the event for
* @since 1.2
*/
protected final void postControlCapturedEvent(final NodeControlInfo info) {
EventAdmin ea = (eventAdmin == null ? null : eventAdmin.service());
if ( ea == null || info == null ) {
return;
}
Event event = createControlCapturedEvent(info);
ea.postEvent(event);
}
/**
* Create a new
* {@link NodeControlProvider#EVENT_TOPIC_CONTROL_INFO_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 info
* the info to create the event for
* @return the new Event instance
* @since 1.2
*/
protected Event createControlCapturedEvent(final NodeControlInfo info) {
Map<String, Object> props = ClassUtils.getSimpleBeanProperties(info, null);
log.debug("Created {} event with props {}",
NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CAPTURED, props);
return new Event(NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CAPTURED, props);
}
// SettingSpecifierProvider
@Override
public String getSettingUID() {
return "net.solarnetwork.node.control.sma.pcm";
}
@Override
public String getDisplayName() {
return "SMA Power Control Module";
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
ModbusPCMController defaults = new ModbusPCMController();
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20);
// get current value
BasicTitleSettingSpecifier status = new BasicTitleSettingSpecifier("status", "N/A", true);
try {
BitSet bits = currentDiscreetValue();
Integer val = integerValueForBitSet(bits);
String binValue = Integer.toBinaryString(val);
String padding = "";
if ( binValue.length() < 4 ) {
padding = String.format("%0" + (4 - binValue.length()) + "d", 0);
}
status.setDefaultValue(String.format("%s%s - %d%%", padding, binValue,
percentValueForIntegerValue(val)));
} catch ( Exception e ) {
log.debug("Error reading PCM status: {}", e.getMessage());
}
results.add(status);
results.add(new BasicTextFieldSettingSpecifier("controlId", defaults.controlId));
results.add(new BasicTextFieldSettingSpecifier("groupUID", defaults.groupUID));
results.add(new BasicTextFieldSettingSpecifier("modbusNetwork.propertyFilters['UID']",
"Serial Port"));
results.add(new BasicTextFieldSettingSpecifier("unitId", String.valueOf(defaults.getUnitId())));
results.add(new BasicTextFieldSettingSpecifier("d1Address", defaults.d1Address.toString()));
results.add(new BasicTextFieldSettingSpecifier("d2Address", defaults.d2Address.toString()));
results.add(new BasicTextFieldSettingSpecifier("d3Address", defaults.d3Address.toString()));
results.add(new BasicTextFieldSettingSpecifier("d4Address", defaults.d4Address.toString()));
results.add(new BasicTextFieldSettingSpecifier("sampleCacheSeconds", String.valueOf(defaults
.getSampleCacheSeconds())));
return results;
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public Integer getD1Address() {
return d1Address;
}
public void setD1Address(Integer d1Address) {
this.d1Address = d1Address;
}
public Integer getD2Address() {
return d2Address;
}
public void setD2Address(Integer d2Address) {
this.d2Address = d2Address;
}
public Integer getD3Address() {
return d3Address;
}
public void setD3Address(Integer d3Address) {
this.d3Address = d3Address;
}
public Integer getD4Address() {
return d4Address;
}
public void setD4Address(Integer d4Address) {
this.d4Address = d4Address;
}
public String getControlId() {
return controlId;
}
public void setControlId(String controlId) {
this.controlId = controlId;
}
@Override
public String getGroupUID() {
return groupUID;
}
@Override
public void setGroupUID(String groupUID) {
this.groupUID = groupUID;
}
public OptionalService<EventAdmin> getEventAdmin() {
return eventAdmin;
}
public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) {
this.eventAdmin = eventAdmin;
}
public int getSampleCacheSeconds() {
return sampleCacheSeconds;
}
public void setSampleCacheSeconds(int sampleCacheSeconds) {
this.sampleCacheSeconds = sampleCacheSeconds;
}
}