/* ==================================================================
* ModbusHeartbeatJob.java - Mar 22, 2014 4:02:14 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.control.modbus.heartbeat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.joda.time.DateTime;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.context.MessageSource;
import net.solarnetwork.node.io.modbus.ModbusConnection;
import net.solarnetwork.node.io.modbus.ModbusConnectionAction;
import net.solarnetwork.node.io.modbus.ModbusNetwork;
import net.solarnetwork.node.job.AbstractJob;
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.settings.support.BasicToggleSettingSpecifier;
import net.solarnetwork.util.DynamicServiceTracker;
/**
* Periodically set a Modbus "coil" type register to a specific value, to act as
* a "heartbeat" to the device so it knows the SolarNode is alive and well.
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>address</dt>
* <dd>The Modbus address of the coil-type register to use.</dd>
* <dt>unitId</dt>
* <dd>The Modbus unit ID to use.</dd>
* <dt>registerValue</dt>
* <dd>The value to set the Modbus register to.</dd>
* <dt>modbusNetwork</dt>
* <dd>The {@link ModbusNetwork} service to use.</dd>
* <dt>messageSource</dt>
* <dd>The {@link MessageSource} to use to support
* {@link SettingSpecifierProvider}.</dd>
* </dl>
*
* @author matt
* @version 3.0
*/
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class ModbusHeartbeatJob extends AbstractJob implements SettingSpecifierProvider {
private Integer address = 0x4008;
private Integer unitId = 1;
private Boolean registerValue = Boolean.TRUE;
private DynamicServiceTracker<ModbusNetwork> modbusNetwork;
private MessageSource messageSource;
// static map to keep track of job execution status info
private static final ConcurrentMap<Long, JobStatus> STATUS_MAP = new ConcurrentHashMap<Long, JobStatus>(
8);
@Override
protected void executeInternal(JobExecutionContext jobContext) throws Exception {
final DateTime heartbeatDate = new DateTime();
String heartbeatMessage = null;
boolean heartbeatSuccess = false;
try {
final Boolean executed = setValue(registerValue);
if ( executed == null ) {
// hmm, how can this be localized?
heartbeatMessage = "No Modbus connection available.";
} else if ( executed.booleanValue() == false ) {
heartbeatMessage = "Unknown Modbus error.";
} else {
// good
heartbeatSuccess = true;
}
} catch ( RuntimeException e ) {
log.error("Error sending heartbeat message: {}", e.toString());
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
heartbeatMessage = "Error: " + root.getMessage();
}
STATUS_MAP.put(getStatusKey(), new JobStatus(heartbeatDate, heartbeatSuccess, heartbeatMessage));
}
private Long getStatusKey() {
return ((unitId == null ? 0L : unitId.longValue()) << 32)
| (address == null ? 0L : address.longValue());
}
private synchronized Boolean setValue(Boolean desiredValue) throws IOException {
final ModbusNetwork network = (modbusNetwork == null ? null : modbusNetwork.service());
if ( network == null ) {
log.debug("No ModbusNetwork avaialble");
return Boolean.FALSE;
}
final BitSet bits = new BitSet(1);
bits.set(0, desiredValue);
log.info("Setting modbus unit {} register {} value to {}", unitId, address, desiredValue);
final Integer[] addresses = new Integer[] { address };
return network.performAction(new ModbusConnectionAction<Boolean>() {
@Override
public Boolean doWithConnection(ModbusConnection conn) throws IOException {
return conn.writeDiscreetValues(addresses, bits);
}
}, unitId);
}
// SettingSpecifierProvider
@Override
public String getSettingUID() {
return "net.solarnetwork.node.control.modbus.heartbeat";
}
@Override
public String getDisplayName() {
return "Modbus Heartbeat";
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
ModbusHeartbeatJob defaults = new ModbusHeartbeatJob();
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(6);
// add info on last execution state
BasicTitleSettingSpecifier lhDate = new BasicTitleSettingSpecifier("lastHeartbeatDate", "N/A",
true);
JobStatus lastHeartbeatStatus = STATUS_MAP.get(getStatusKey());
if ( lastHeartbeatStatus != null ) {
lhDate.setDefaultValue(lastHeartbeatStatus.getDate().toString());
}
results.add(lhDate);
if ( lastHeartbeatStatus != null && lastHeartbeatStatus.getMessage() != null ) {
results.add(new BasicTitleSettingSpecifier("lastHeartbeatMessage",
lastHeartbeatStatus.getMessage(), true));
}
results.add(new BasicTextFieldSettingSpecifier("modbusNetwork.propertyFilters['UID']",
"Serial Port"));
results.add(new BasicTextFieldSettingSpecifier("unitId", defaults.unitId.toString()));
results.add(new BasicTextFieldSettingSpecifier("address", defaults.address.toString()));
results.add(new BasicToggleSettingSpecifier("registerValue", defaults.registerValue.toString()));
return results;
}
// Accessors
public void setAddress(Integer address) {
this.address = address;
}
public void setUnitId(Integer unitId) {
this.unitId = unitId;
}
public void setRegisterValue(Boolean registerValue) {
this.registerValue = registerValue;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public void setModbusNetwork(DynamicServiceTracker<ModbusNetwork> modbusNetwork) {
this.modbusNetwork = modbusNetwork;
}
}