/* ==================================================================
* LATAController.java - Oct 25, 2011 8:16:16 PM
*
* Copyright 2007-2011 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.jf2.lata;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.springframework.context.MessageSource;
import net.solarnetwork.domain.NodeControlInfo;
import net.solarnetwork.domain.NodeControlPropertyType;
import net.solarnetwork.node.NodeControlProvider;
import net.solarnetwork.node.control.jf2.lata.command.AddressableCommand;
import net.solarnetwork.node.control.jf2.lata.command.Command;
import net.solarnetwork.node.control.jf2.lata.command.CommandInterface;
import net.solarnetwork.node.control.jf2.lata.command.CommandValidationException;
import net.solarnetwork.node.control.jf2.lata.command.ToggleMode;
import net.solarnetwork.node.domain.NodeControlInfoDatum;
import net.solarnetwork.node.io.serial.SerialConnection;
import net.solarnetwork.node.io.serial.SerialDeviceSupport;
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;
/**
* Implementation of both {@link NodeControlProvider} and
* {@link InstructionHandler} for the JF2 LATA switch.
*
* <p>
* This class allows the LATA switch to both report the on/off status of each
* configured address, and for those addresses to be toggled on/off.
* </p>
*
* <p>
* The configurable properties of this class are:
* </p>
*
* <dl class="class-properties">
* <dt>controlIdMapping</dt>
* <dd>A mapping of NodeControlInfo {@code controlId} value keys to associated
* LATA switch addresses, as hex string values. This can also be configured via
* the {@link #setControlIdMappingValue(String)} method, for easy configuration
* via a property placeholder.</dd>
*
* </dl>
*
* @author matt
* @version 2.1
*/
public class LATAController extends SerialDeviceSupport
implements NodeControlProvider, InstructionHandler, SettingSpecifierProvider {
/** The default value for the {@code controlIdMappingValue} property. */
public static final String DEFAULT_CONTROL_ID_MAPPING = "/power/switch/1 = 100000BD, /power/switch/2 = 100000FD";
private Map<String, String> controlIdMapping = new HashMap<String, String>();
private static final Pattern SWITCH_STATUS_RESULT_PATTERN = Pattern
.compile("^T(\\w{8})2\\d{2}(\\w*)");
private MessageSource messageSource;
private OptionalService<EventAdmin> eventAdmin;
/**
* Default constructor.
*/
public LATAController() {
super();
setControlIdMappingValue(DEFAULT_CONTROL_ID_MAPPING);
}
@Override
public boolean handlesTopic(String topic) {
return InstructionHandler.TOPIC_SET_CONTROL_PARAMETER.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 controls {}", instruction.getId(),
controlIdMapping.keySet());
for ( String controlId : instruction.getParameterNames() ) {
log.trace("Got instruction parameter {}", controlId);
if ( controlIdMapping.containsKey(controlId) ) {
// treat parameter value as boolean
String value = instruction.getParameterValue(controlId);
if ( value != null ) {
value = value.toLowerCase();
}
boolean newStatus = true;
if ( "false".equals(value) || "0".equals(value) ) {
newStatus = false;
}
result = InstructionState.Declined;
try {
if ( setSwitchStatus(controlId, newStatus) ) {
result = InstructionState.Completed;
}
} catch ( IOException e ) {
log.warn("Serial communications error: {}", e.getMessage());
}
}
}
return result;
}
private synchronized boolean setSwitchStatus(String controlId, boolean newStatus)
throws IOException {
String address = controlIdMapping.get(controlId);
log.debug("Setting switch {} status at address {}", controlId, address);
if ( address == null ) {
log.warn("Configuration error: address not available for control ID [{}]", controlId);
return false;
}
CommandInterface cmd;
try {
cmd = new AddressableCommand(address, newStatus ? Command.SwitchOn : Command.SwitchOff);
} catch ( CommandValidationException e ) {
log.error("Bad address [{}] configured for control ID {}: {}",
new Object[] { address, controlId, e.getMessage() });
return false;
}
performAction(new LATABusConverser(cmd));
log.trace("Set status to {} for control {}, address {}",
new Object[] { newStatus, controlId, address });
postControlEvent(newNodeControlInfoDatum(controlId, newStatus),
NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CHANGED);
return true;
}
@Override
public List<String> getAvailableControlIds() {
if ( controlIdMapping == null ) {
return Collections.emptyList();
}
return new ArrayList<String>(controlIdMapping.keySet());
}
@Override
protected Map<String, Object> readDeviceInfo(SerialConnection conn) throws IOException {
String version = new GetVersionAction(false).doWithConnection(conn);
if ( version != null ) {
return Collections.singletonMap(INFO_KEY_DEVICE_MODEL, (Object) version);
}
return Collections.emptyMap();
}
@Override
public synchronized NodeControlInfo getCurrentControlInfo(String controlId) {
// read the control's current status
String address = controlIdMapping.get(controlId);
log.debug("Reading switch {} status from address {}", controlId, address);
if ( address == null ) {
log.warn("Configuration error: address not available for control ID [{}]", controlId);
return null;
}
CommandInterface cmd;
try {
cmd = new AddressableCommand(address, Command.SwitchStatus);
} catch ( CommandValidationException e ) {
log.error("Bad address [{}] configured for control ID {}: {}",
new Object[] { address, controlId, e.getMessage() });
return null;
}
try {
log.trace("Executing command {} for control {}", cmd.getData(), controlId);
String result = performAction(new LATABusConverser(cmd));
if ( result == null ) {
log.info("Status unavailable for control {}, address {}", controlId, address);
return null;
}
log.trace("Got status result [{}] for control {}, address {}",
new Object[] { result, controlId, address });
Matcher m = SWITCH_STATUS_RESULT_PATTERN.matcher(result);
if ( m.find() ) {
String resultAddress = m.group(1);
if ( !resultAddress.equals(address) ) {
log.debug(
"Address returned in command {} does not match expected address {}, ignoring",
resultAddress, address);
} else {
String status = m.group(2);
Boolean switchOn = ToggleMode.ON.hexString().equals(status);
log.trace("Address {} status is {}", address, switchOn);
NodeControlInfo info = newNodeControlInfoDatum(controlId, switchOn);
postControlEvent(info, NodeControlProvider.EVENT_TOPIC_CONTROL_INFO_CAPTURED);
return info;
}
}
log.info("Invalid status result [{}], ignoring", result);
} catch ( IOException e ) {
log.warn("Serial communications error: {}", e.getMessage());
}
return null;
}
private NodeControlInfoDatum newNodeControlInfoDatum(String controlId, Boolean status) {
NodeControlInfoDatum info = new NodeControlInfoDatum();
info.setCreated(new Date());
info.setSourceId(controlId);
info.setType(NodeControlPropertyType.Boolean);
info.setReadonly(false);
info.setValue(status.toString());
return info;
}
private void postControlEvent(NodeControlInfo info, String topic) {
final EventAdmin admin = (eventAdmin != null ? eventAdmin.service() : null);
if ( admin == null ) {
return;
}
Map<String, Object> props = ClassUtils.getSimpleBeanProperties(info, null);
admin.postEvent(new Event(topic, props));
}
/**
* Set the {@code controlIdMapping} property using a string value.
*
* <p>
* The passed in value must be a comma-delimited list of key/value pairs,
* each pair separated by an equal sign. For example:
* {@code 1 = one, 2 = two} would define two keys and their associated
* values.
* </p>
*
* @param value
* the value string
*/
public void setControlIdMappingValue(String value) {
String[] keyValues = value.split("\\s*,\\s*");
Map<String, String> map = new LinkedHashMap<String, String>(3);
for ( String pair : keyValues ) {
String[] kv = pair.split("\\s*=\\s*");
if ( kv.length == 2 ) {
map.put(kv[0], kv[1]);
}
}
setControlIdMapping(map);
}
/**
* Get the {@code controlIdMapping} map as a string value.
*
* @return the string value
*/
public String getControlIdMappingValue() {
Map<String, String> mapping = getControlIdMapping();
if ( mapping == null || mapping.size() < 1 ) {
return "";
}
StringBuilder buf = new StringBuilder();
for ( Map.Entry<String, String> me : mapping.entrySet() ) {
if ( buf.length() > 0 ) {
buf.append(", ");
}
buf.append(me.getKey()).append(" = ").append(me.getValue());
}
return buf.toString();
}
@Override
public String getSettingUID() {
return "net.solarnetwork.node.control.jf2.lata";
}
@Override
public String getDisplayName() {
return "JF2 LATA switch control";
}
@Override
public List<SettingSpecifier> getSettingSpecifiers() {
return getDefaultSettingSpecifiers();
}
@Override
public MessageSource getMessageSource() {
return messageSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
public List<SettingSpecifier> getDefaultSettingSpecifiers() {
List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20);
LATAController defaults = new LATAController();
results.add(new BasicTitleSettingSpecifier("info", getDeviceInfoMessage(), true));
results.add(new BasicTextFieldSettingSpecifier("uid", defaults.getUid()));
results.add(new BasicTextFieldSettingSpecifier("groupUID", defaults.getGroupUID()));
results.add(
new BasicTextFieldSettingSpecifier("controlIdMappingValue", DEFAULT_CONTROL_ID_MAPPING));
results.add(new BasicTextFieldSettingSpecifier("serialNetwork.propertyFilters['UID']",
"Serial Port"));
return results;
}
public Map<String, String> getControlIdMapping() {
return controlIdMapping;
}
public void setControlIdMapping(Map<String, String> controlIdMapping) {
this.controlIdMapping = controlIdMapping;
}
/**
* Set an {@link EventAdmin} to use for posting control provider events.
*
* @param eventAdmin
* The service to use.
* @since 2.1
*/
public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) {
this.eventAdmin = eventAdmin;
}
}