/* ================================================================== * RfidChargeSessionManager.java - 30/07/2016 8:25:21 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.ocpp.charge.rfid; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.osgi.service.event.Event; import org.osgi.service.event.EventHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import net.solarnetwork.node.ocpp.ChargeSession; import net.solarnetwork.node.ocpp.ChargeSessionManager; import net.solarnetwork.node.ocpp.OCPPException; import net.solarnetwork.node.ocpp.SocketManager; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicGroupSettingSpecifier; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.node.settings.support.SettingsUtil; import net.solarnetwork.util.FilterableService; /** * Listen for RFID "message received" events and initiate/conclude OCPP charge * sessions accordingly. * * This service coordinates OCPP charge sessions with RFID cards. The typical * use case goes like this: * * <ol> * <li>Person arrives in EV and scans their OCPP provider's supplied RFID * card.</li> * <li>This service requests authorization from the OCPP server to start a * charge session.</li> * <li>Assuming the charge session is authorized, this service instructs the * first available socket to be <b>enabled</b> to start the flow of power to the * EV.</li> * <li>The ends the charging session by scanning the same RFID card from the * first step.</li> * <li>This service ends the OCPP charge session with the OCPP server.</li> * <li>This service instructs the same socket from before to be <b>disabled</b> * and cut off any power.</li> * </ol> * * To prevent open-ended charge sessions, a configurable time limit is enforced * on charge sessions. If a session goes past this limit, the session will be * automatically ended and the socket for that session disabled. * * @author matt * @version 1.0 */ public class RfidChargeSessionManager implements EventHandler, SettingSpecifierProvider { /** Topic for when a RFID message has been received. */ public static final String TOPIC_RFID_MESSAGE_RECEIVED = "net/solarnetwork/node/hw/rfid/MESSAGE_RECEIVED"; /** Event parameter for the RFID message value. */ public static final String EVENT_PARAM_MESSAGE = "message"; /** Event parameter for the configured {@code uid}. */ public static final String EVENT_PARAM_UID = "uid"; /** Event parameter for the configured {@code groupUID}. */ public static final String EVENT_PARAM_GROUP_UID = "groupUID"; private ChargeSessionManager chargeSessionManager; private SocketManager socketManager; private MessageSource messageSource; private List<RfidSocketMapping> rfidSocketMappings = new ArrayList<RfidSocketMapping>(2); private ExecutorService executor = Executors.newSingleThreadExecutor(); // to kick off the handleEvent() thread private final Logger log = LoggerFactory.getLogger(getClass()); /** * Initialize after all properties configured. */ public void startup() { // anything to do here? } /** * Call to stop handling charge sessions. */ public void shutdown() { executor.shutdown(); } @Override public void handleEvent(Event event) { if ( !TOPIC_RFID_MESSAGE_RECEIVED.equals(event.getTopic()) ) { return; } final Object rfidMessage = event.getProperty(EVENT_PARAM_MESSAGE); if ( rfidMessage == null ) { log.warn("Ignoring MESSAGE_RECEIVED event missing required message value"); return; } final Object rfidUid = event.getProperty(EVENT_PARAM_UID); if ( rfidUid == null && rfidSocketMappings != null && !rfidSocketMappings.isEmpty() ) { log.warn("Ignoring MESSAGE_RECEIVED event missing required UID value"); return; } // kick off to new thread so we don't block the event thread executor.submit(new Runnable() { @Override public void run() { try { handleRfidScan(rfidMessage.toString(), rfidUid.toString()); } catch ( Throwable t ) { log.error("Error handling RFID message {}", rfidMessage, t); if ( t instanceof RuntimeException ) { throw (RuntimeException) t; } throw new RuntimeException(t); } } }); } /** * In response to a RFID card scan, look for an available socket for a new * charge session, or an existing charge session to end. * * @param idTag * The RFID card ID value to treat as the OCPP IdTag. * @param rfidUid * The RFID scanner UID value the scan originated from. */ private void handleRfidScan(String idTag, String rfidUid) { // see if there is any active charge session for that tag Collection<String> availableSockets = chargeSessionManager.availableSocketIds(); Set<String> freeSockets = new LinkedHashSet<String>(availableSockets); for ( String socketId : availableSockets ) { ChargeSession session = chargeSessionManager.activeChargeSession(socketId); if ( session != null ) { if ( session.getIdTag().equals(idTag) ) { // found session with same IdTag... use that socket handleChargeSessionStateChange(socketId, idTag); return; } freeSockets.remove(socketId); } } if ( freeSockets.isEmpty() ) { // all sockets in use log.info("No free sockets to enable charge session for IdTag {}", idTag); } else { String socketId = socketToUse(freeSockets, rfidUid); if ( socketId != null ) { log.info("Socket {} free for charge session with IdTag {}", socketId, idTag); handleChargeSessionStateChange(socketId, idTag); } } } private String socketToUse(Set<String> freeSockets, String rfidUid) { if ( freeSockets == null ) { return null; } List<RfidSocketMapping> mappings = rfidSocketMappings; if ( rfidUid == null || mappings == null || mappings.isEmpty() ) { log.debug("Choosing first available free socket for scan from RFID device {}", rfidUid); return freeSockets.iterator().next(); } for ( RfidSocketMapping mapping : mappings ) { if ( rfidUid.equalsIgnoreCase(mapping.getRfidUid()) ) { if ( freeSockets.contains(mapping.getSocketId()) ) { log.debug("Choosing socket {}; configured for RFID device {}", mapping.getSocketId(), rfidUid); return mapping.getSocketId(); } log.info("Socket {} configured for RFID device {} but that socket is not available", mapping.getSocketId(), rfidUid); } } log.warn("No socket configured for RFID device {}", rfidUid); return null; } private void handleChargeSessionStateChange(final String socketId, final String idTag) { // is there an existing charge session available for this socket ID? ChargeSession session = chargeSessionManager.activeChargeSession(socketId); if ( session == null ) { // start a new session try { String sessionId = chargeSessionManager.initiateChargeSession(idTag, socketId, null); log.info("OCPP charge session {} for IdTag {} initiated on socket {}", sessionId, idTag, socketId); if ( !socketManager.adjustSocketEnabledState(socketId, true) ) { log.error("Unable to enable socket {} for charge session {}", socketId, sessionId); chargeSessionManager.completeChargeSession(idTag, sessionId); } } catch ( OCPPException e ) { log.error("Unable to initiate change session on {} for IdTag {}: {}", socketId, idTag, e.getStatus()); } } else if ( session.getIdTag().equals(idTag) ) { // end existing session try { chargeSessionManager.completeChargeSession(idTag, session.getSessionId()); } finally { if ( !socketManager.adjustSocketEnabledState(socketId, false) ) { log.error("Unable to disable socket {} for charge session {}", socketId, session.getSessionId()); } } log.info("OCPP charge session {} for IdTag {} completed on socket {}", session.getSessionId(), idTag, socketId); } else { // not allowed to modify existing session with different RFID card log.info( "Cannot start new charge session on socket {} for IdTag {} because session already active for IdTag {}", socketId, idTag, session.getIdTag()); } } @Override public String getSettingUID() { return "net.solarnetwork.node.ocpp.charge.rfid"; } @Override public String getDisplayName() { return getClass().getSimpleName(); } @Override public MessageSource getMessageSource() { return messageSource; } @Override public List<SettingSpecifier> getSettingSpecifiers() { List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(2); results.add(new BasicTextFieldSettingSpecifier( "filterableChargeSessionManager.propertyFilters['UID']", "OCPP Central System")); results.add(new BasicTextFieldSettingSpecifier("filterableSocketManager.propertyFilters['UID']", "OCPP Central System")); // dynamic list of RfidSocketMapping List<RfidSocketMapping> mappings = getRfidSocketMappings(); BasicGroupSettingSpecifier mappingsGroup = SettingsUtil.dynamicListSettingSpecifier( "rfidSocketMappings", mappings, new SettingsUtil.KeyedListCallback<RfidSocketMapping>() { @Override public Collection<SettingSpecifier> mapListSettingKey(RfidSocketMapping value, int index, String key) { BasicGroupSettingSpecifier mappingGroup = new BasicGroupSettingSpecifier( RfidSocketMapping.settings(key + ".")); return Collections.<SettingSpecifier> singletonList(mappingGroup); } }); results.add(mappingsGroup); return results; } public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public void setChargeSessionManager(ChargeSessionManager chargeSessionManager) { this.chargeSessionManager = chargeSessionManager; } /** * Get the {@link ChargeSessionManager} as a {@link FilterableService}. * * @return The filterable {@link ChargeSessionManager}, or <em>null</em> if * it is not filterable. */ public FilterableService getFilterableChargeSessionManager() { ChargeSessionManager mgr = chargeSessionManager; if ( mgr instanceof FilterableService ) { return (FilterableService) mgr; } return null; } /** * Set an {@link ExecutorService} to run tasks with. * * @param executor * The executor service to use. If <em>null</em> a default * implementation will be configured. */ public void setExecutor(ExecutorService executor) { if ( executor == null ) { executor = Executors.newSingleThreadExecutor(); } this.executor = executor; } /** * Get the configured {@link ExecutorService} for running tasks with. * * @return The configured service. */ public ExecutorService getExecutor() { return executor; } public void setSocketManager(SocketManager socketManager) { this.socketManager = socketManager; } /** * Get the {@link SocketManager} as a {@link FilterableService}. * * @return The filterable {@link SocketManager}, or <em>null</em> if it is * not filterable. */ public FilterableService getFilterableSocketManager() { SocketManager mgr = socketManager; if ( mgr instanceof FilterableService ) { return (FilterableService) mgr; } return null; } /** * Get the configured RFID socket mappings. * * @return The mappings, or {@code null}. */ public List<RfidSocketMapping> getRfidSocketMappings() { return rfidSocketMappings; } /** * Set the list of RFID UID values with associated socket ID values. * * If this list is not configured, then the first available socket will be * allocated when an RFID message is received. Otherwise this list will be * consulted and the first matching {@link RfidSocketMapping#getRfidUid()} * value will cause the associated {@link RfidSocketMapping#getSocketId()} * socket ID to use used. * * Generally this list should be left unconfigured <b>or</b> have one value * for each supported RFID scanner UID value. The later case is useful when * more than one RFID scanner is used in combination with more than one * socket, and each RFID scanner should be associated with a specific * socket. * * @param rfidSocketMappings * The list of RFID to socket ID mappings. */ public void setRfidSocketMappings(List<RfidSocketMapping> rfidSocketMappings) { this.rfidSocketMappings = rfidSocketMappings; } /** * Get the number of configured {@code listComplex} elements. * * @return The number of {@code listComplex} elements. */ public int getRfidSocketMappingsCount() { List<RfidSocketMapping> l = getRfidSocketMappings(); return (l == null ? 0 : l.size()); } /** * Adjust the number of configured {@code rfidSocketMappings} elements * * @param count * The desired number of {@code rfidSocketMappings} elements. */ public void setRfidSocketMappingsCount(int count) { if ( count < 0 ) { count = 0; } List<RfidSocketMapping> l = getRfidSocketMappings(); int lCount = (l == null ? 0 : l.size()); while ( lCount > count ) { l.remove(l.size() - 1); lCount--; } while ( lCount < count ) { l.add(new RfidSocketMapping()); lCount++; } } }