package de.rwth.idsg.steve.ocpp.ws;
import de.rwth.idsg.steve.config.WebSocketConfiguration;
import de.rwth.idsg.steve.ocpp.ws.custom.WsSessionSelectStrategy;
import de.rwth.idsg.steve.ocpp.ws.data.CommunicationContext;
import de.rwth.idsg.steve.ocpp.ws.data.SessionContext;
import de.rwth.idsg.steve.ocpp.ws.pipeline.Pipeline;
import de.rwth.idsg.steve.repository.OcppServerRepository;
import de.rwth.idsg.steve.service.NotificationService;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.PongMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* @author Sevket Goekay <goekay@dbis.rwth-aachen.de>
* @since 17.03.2015
*/
public abstract class AbstractWebSocketEndpoint implements WebSocketHandler {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired private ScheduledExecutorService service;
@Autowired private OcppServerRepository ocppServerRepository;
@Autowired private FutureResponseContextStore futureResponseContextStore;
@Autowired private WsSessionSelectStrategy wsSessionSelectStrategy;
@Autowired private NotificationService notificationService;
public static final String CHARGEBOX_ID_KEY = "CHARGEBOX_ID_KEY";
private Pipeline pipeline;
private SessionContextStoreImpl sessionContextStore;
private final List<Consumer<String>> connectedCallbackList = new ArrayList<>();
private final List<Consumer<String>> disconnectedCallbackList = new ArrayList<>();
private final Object sessionContextLock = new Object();
public void init(Pipeline pipeline) {
this.pipeline = pipeline;
sessionContextStore = new SessionContextStoreImpl(wsSessionSelectStrategy);
connectedCallbackList.add((chargeBoxId) -> notificationService.ocppStationWebSocketConnected(chargeBoxId));
disconnectedCallbackList.add((chargeBoxId) -> notificationService.ocppStationWebSocketDisconnected(chargeBoxId));
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if (message instanceof TextMessage) {
handleTextMessage(session, (TextMessage) message);
} else if (message instanceof PongMessage) {
handlePongMessage(session);
} else if (message instanceof BinaryMessage) {
session.close(CloseStatus.NOT_ACCEPTABLE.withReason("Binary messages not supported"));
} else {
throw new IllegalStateException("Unexpected WebSocket message type: " + message);
}
}
private void handleTextMessage(WebSocketSession session, TextMessage webSocketMessage) throws Exception {
String incomingString = webSocketMessage.getPayload();
String chargeBoxId = getChargeBoxId(session);
log.info("[chargeBoxId={}, sessionId={}] Received message: {}", chargeBoxId, session.getId(), incomingString);
CommunicationContext context = new CommunicationContext();
context.setSession(session);
context.setChargeBoxId(chargeBoxId);
context.setIncomingString(incomingString);
pipeline.process(context);
}
private void handlePongMessage(WebSocketSession session) {
log.debug("[id={}] Received pong message", session.getId());
// TODO: Not sure about the following. Should update DB? Should call directly repo?
ocppServerRepository.updateChargeboxHeartbeat(getChargeBoxId(session), DateTime.now());
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("New connection established: {}", session);
// Just to keep the connection alive, such that the servers do not close
// the connection because of a idle timeout, we ping-pong at fixed intervals.
ScheduledFuture pingSchedule = service.scheduleAtFixedRate(
new PingTask(session),
WebSocketConfiguration.PING_INTERVAL,
WebSocketConfiguration.PING_INTERVAL,
TimeUnit.MINUTES);
String chargeBoxId = getChargeBoxId(session);
futureResponseContextStore.addSession(session);
int sizeBeforeAdd;
synchronized (sessionContextLock) {
sizeBeforeAdd = sessionContextStore.getSize(chargeBoxId);
sessionContextStore.add(chargeBoxId, session, pingSchedule);
}
// Take into account that there might be multiple connections to a charging station.
// Send notification only for the change 0 -> 1.
if (sizeBeforeAdd == 0) {
connectedCallbackList.forEach(consumer -> consumer.accept(chargeBoxId));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.warn("[id={}] Connection was closed, status: {}", session.getId(), closeStatus);
String chargeBoxId = getChargeBoxId(session);
futureResponseContextStore.removeSession(session);
int sizeAfterRemove;
synchronized (sessionContextLock) {
sessionContextStore.remove(chargeBoxId, session);
sizeAfterRemove = sessionContextStore.getSize(chargeBoxId);
}
// Take into account that there might be multiple connections to a charging station.
// Send notification only for the change 1 -> 0.
if (sizeAfterRemove == 0) {
disconnectedCallbackList.forEach(consumer -> consumer.accept(chargeBoxId));
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable throwable) throws Exception {
log.error("Oops", throwable);
// TODO: Do something about this
}
@Override
public boolean supportsPartialMessages() {
return false;
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
protected String getChargeBoxId(WebSocketSession session) {
return (String) session.getAttributes().get(CHARGEBOX_ID_KEY);
}
protected void registerConnectedCallback(Consumer<String> consumer) {
connectedCallbackList.add(consumer);
}
protected void registerDisconnectedCallback(Consumer<String> consumer) {
disconnectedCallbackList.add(consumer);
}
public List<String> getChargeBoxIdList() {
return sessionContextStore.getChargeBoxIdList();
}
public int getNumberOfChargeBoxes() {
return sessionContextStore.getNumberOfChargeBoxes();
}
public Map<String, Deque<SessionContext>> getACopy() {
return sessionContextStore.getACopy();
}
public WebSocketSession getSession(String chargeBoxId) {
return sessionContextStore.getSession(chargeBoxId);
}
}