/* ================================================================== * SimpleSocketManager.java - 31/07/2016 7:34:18 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.socket.control; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import net.solarnetwork.node.Identifiable; 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.reactor.Instruction; import net.solarnetwork.node.reactor.InstructionHandler; import net.solarnetwork.node.reactor.InstructionStatus; import net.solarnetwork.node.reactor.InstructionStatus.InstructionState; import net.solarnetwork.node.reactor.support.BasicInstruction; import net.solarnetwork.node.reactor.support.InstructionUtils; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.SettingSpecifierProvider; import net.solarnetwork.node.settings.support.BasicTextFieldSettingSpecifier; import net.solarnetwork.util.FilterableService; import net.solarnetwork.util.OptionalService; /** * Implementation of {@link SocketManager} that uses the * {@link InstructionHandler#TOPIC_SET_CONTROL_PARAMETER} instruction to turn * sockets on/off. * * The {@link Identifiable} properties of this class are delegated to the * configured {@link ChargeSessionManager}. * * @author matt * @version 1.0 */ public class SimpleSocketManager implements SocketManager, SettingSpecifierProvider { private Collection<InstructionHandler> instructionHandlers = Collections.emptyList(); private OptionalService<EventAdmin> eventAdmin; private ChargeSessionManager chargeSessionManager; private int chargeSessionExpirationMinutes = 6 * 60; private MessageSource messageSource; private final Logger log = LoggerFactory.getLogger(getClass()); @Override public boolean adjustSocketEnabledState(String socketId, boolean enabled) { final BasicInstruction instr = new BasicInstruction( InstructionHandler.TOPIC_SET_CONTROL_PARAMETER, new Date(), Instruction.LOCAL_INSTRUCTION_ID, Instruction.LOCAL_INSTRUCTION_ID, null); instr.addParameter(socketId, String.valueOf(enabled)); log.debug("Requesting socket {} to be {}", socketId, enabled ? "enabled" : "disabled"); InstructionStatus.InstructionState result = InstructionUtils .handleInstruction(instructionHandlers, instr); log.debug("Request for socket {} to be {} resulted in {}", socketId, enabled ? "enabled" : "disabled", result); if ( result == null ) { result = InstructionStatus.InstructionState.Declined; } if ( result == InstructionStatus.InstructionState.Completed ) { String eventTopic = (enabled ? ChargeSessionManager.EVENT_TOPIC_SOCKET_ACTIVATED : ChargeSessionManager.EVENT_TOPIC_SOCKET_DEACTIVATED); Map<String, Object> eventProps = Collections .singletonMap(ChargeSessionManager.EVENT_PROPERTY_SOCKET_ID, (Object) socketId); postEvent(eventTopic, eventProps); } return (result != null && result != InstructionState.Declined); } @Override public String getUID() { return (chargeSessionManager != null ? chargeSessionManager.getUID() : null); } @Override public String getGroupUID() { return (chargeSessionManager != null ? chargeSessionManager.getGroupUID() : null); } private void postEvent(String topic, Map<String, Object> props) { OptionalService<EventAdmin> eaService = eventAdmin; EventAdmin ea = (eaService == null ? null : eaService.service()); if ( ea == null ) { return; } log.debug("Posting event {}: {}", topic, props); Event event = new Event(topic, props); ea.postEvent(event); } @Override public String getSettingUID() { return "net.solarnetwork.node.ocpp.socket.control"; } @Override public String getDisplayName() { return getClass().getSimpleName(); } @Override public MessageSource getMessageSource() { return messageSource; } @Override public List<SettingSpecifier> getSettingSpecifiers() { SimpleSocketManager defaults = new SimpleSocketManager(); List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(1); results.add(new BasicTextFieldSettingSpecifier( "filterableChargeSessionManager.propertyFilters['UID']", "OCPP Central System")); results.add(new BasicTextFieldSettingSpecifier("chargeSessionExpirationMinutes", String.valueOf(defaults.chargeSessionExpirationMinutes))); return results; } /** * Find all active charge sessions and make sure the sockets associated with * those sessions are enabled. This should be called when the service is * first initialized, to make sure the socket state is synchronized with * charge session state. */ public void verifyAllSockets() { Collection<String> availableSockets = chargeSessionManager.availableSocketIds(); for ( String socketId : availableSockets ) { ChargeSession session = chargeSessionManager.activeChargeSession(socketId); boolean expired = chargeSessionExpired(session); if ( expired ) { log.info("OCPP charge session {} for IdTag {} has expired", session.getSessionId(), session.getIdTag()); try { chargeSessionManager.completeChargeSession(session.getIdTag(), session.getSessionId()); } catch ( OCPPException e ) { log.warn("Error completing expired OCPP session: {}", e.getMessage()); } } boolean enabled = (session != null && !expired); if ( !adjustSocketEnabledState(socketId, enabled) ) { log.warn("Unable to adjust socket {} enabled state to {}", socketId, enabled ? "enabled" : "disabled"); } } } /** * Test if a session has expired. * * @param session * The session, or <em>null</em>. * @return <em>true</em> if {@code session} is non-null and the current time * is greater than {@link ChargeSession#getExpiryDate()} or the * {@link ChargeSession#getCreated()} plus * {@code chargeSessionExpirationMinutes}. */ private boolean chargeSessionExpired(ChargeSession session) { if ( session == null ) { return false; } final long now = System.currentTimeMillis(); if ( session.getExpiryDate() != null ) { long expireTime = session.getExpiryDate().toGregorianCalendar().getTimeInMillis(); if ( expireTime < now ) { return true; } } if ( session.getCreated() != null ) { long expireTime = session.getCreated().getTime() + (chargeSessionExpirationMinutes * 60 * 1000L); if ( expireTime < now ) { return true; } } return false; } /** * Set the {@link EventAdmin} to use for posting socket state change events. * * The {@link ChargeSessionManager#EVENT_TOPIC_SOCKET_ACTIVATED} and * {@link ChargeSessionManager#EVENT_TOPIC_SOCKET_DEACTIVATED} events will * be sent if configured. * * @param eventAdmin * The service to use. */ public void setEventAdmin(OptionalService<EventAdmin> eventAdmin) { this.eventAdmin = eventAdmin; } /** * Set the collection of instruction handlers to process the * {@link InstructionHandler#TOPIC_SET_CONTROL_PARAMETER} instruction for * changing socket states. * * @param instructionHandlers * The handlers to use. */ public void setInstructionHandlers(Collection<InstructionHandler> instructionHandlers) { if ( instructionHandlers == null ) { instructionHandlers = Collections.emptyList(); } this.instructionHandlers = instructionHandlers; } public void setMessageSource(MessageSource messageSource) { this.messageSource = messageSource; } public void setChargeSessionManager(ChargeSessionManager chargeSessionManager) { this.chargeSessionManager = chargeSessionManager; } /** * Get the minimum number of minutes a charge session is allowed to go * before expiring. * * @return The configured number of minutes. */ public int getChargeSessionExpirationMinutes() { return chargeSessionExpirationMinutes; } /** * Set the minimum number of minutes a charge session is allowed to go * before expiring. * * @param chargeSessionExpirationMinutes * The minimum number of minutes before a charge session is * considered expired. Defaults to {@code 360} (6 hours). */ public void setChargeSessionExpirationMinutes(int chargeSessionExpirationMinutes) { if ( chargeSessionExpirationMinutes < 0 ) { chargeSessionExpirationMinutes = 0; } this.chargeSessionExpirationMinutes = chargeSessionExpirationMinutes; } /** * 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; } }