diff --git a/src/main/java/de/rwth/idsg/steve/NotificationFeature.java b/src/main/java/de/rwth/idsg/steve/NotificationFeature.java index dc5d94422..1847498fc 100644 --- a/src/main/java/de/rwth/idsg/steve/NotificationFeature.java +++ b/src/main/java/de/rwth/idsg/steve/NotificationFeature.java @@ -35,6 +35,7 @@ public enum NotificationFeature { OcppStationWebSocketConnected(" a JSON charging station connects"), OcppStationWebSocketDisconnected(" a JSON charging station disconnects"), OcppTransactionStarted(" a charging station starts a transaction"), + OcppStationStatusSuspendedEV(" a EV suspended charging"), OcppTransactionEnded(" a charging station ends a transaction"); diff --git a/src/main/java/de/rwth/idsg/steve/repository/TransactionRepository.java b/src/main/java/de/rwth/idsg/steve/repository/TransactionRepository.java index ea40930b9..d1ff91792 100644 --- a/src/main/java/de/rwth/idsg/steve/repository/TransactionRepository.java +++ b/src/main/java/de/rwth/idsg/steve/repository/TransactionRepository.java @@ -30,11 +30,14 @@ * @since 19.08.2014 */ public interface TransactionRepository { + Transaction getTransaction(int transactionPk); + List getTransactions(TransactionQueryForm form); void writeTransactionsCSV(TransactionQueryForm form, Writer writer); List getActiveTransactionIds(String chargeBoxId); + Integer getActiveTransactionId(String chargeBoxId, Integer connectorId); TransactionDetails getDetails(int transactionPk); } diff --git a/src/main/java/de/rwth/idsg/steve/repository/UserRepository.java b/src/main/java/de/rwth/idsg/steve/repository/UserRepository.java index 9fc2798dc..89179fa74 100644 --- a/src/main/java/de/rwth/idsg/steve/repository/UserRepository.java +++ b/src/main/java/de/rwth/idsg/steve/repository/UserRepository.java @@ -31,6 +31,7 @@ public interface UserRepository { List getOverview(UserQueryForm form); User.Details getDetails(int userPk); + User.Details getDetails(String ocppTag); void add(UserForm form); void update(UserForm form); diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/TransactionRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/TransactionRepositoryImpl.java index 0ba4b3b1a..4f7697d36 100644 --- a/src/main/java/de/rwth/idsg/steve/repository/impl/TransactionRepositoryImpl.java +++ b/src/main/java/de/rwth/idsg/steve/repository/impl/TransactionRepositoryImpl.java @@ -64,6 +64,16 @@ public TransactionRepositoryImpl(DSLContext ctx) { this.ctx = ctx; } + @Override + public Transaction getTransaction(int transactionPk) { + TransactionQueryForm form = new TransactionQueryForm(); + form.setTransactionPk(transactionPk); + form.setReturnCSV(false); + form.setType(TransactionQueryForm.QueryType.ALL); + return getInternal(form).fetch() + .map(new TransactionMapper()).get(0); + } + @Override public List getTransactions(TransactionQueryForm form) { return getInternal(form).fetch() @@ -87,6 +97,19 @@ public List getActiveTransactionIds(String chargeBoxId) { .fetch(TRANSACTION.TRANSACTION_PK); } + @Override + public Integer getActiveTransactionId(String chargeBoxId, Integer connectorId) { + return ctx.select(TRANSACTION.TRANSACTION_PK) + .from(TRANSACTION) + .join(CONNECTOR) + .on(TRANSACTION.CONNECTOR_PK.equal(CONNECTOR.CONNECTOR_PK)) + .and(CONNECTOR.CHARGE_BOX_ID.equal(chargeBoxId)) + .where(TRANSACTION.STOP_TIMESTAMP.isNull()) + .and(CONNECTOR.CONNECTOR_ID.equal(connectorId)) + .orderBy(TRANSACTION.TRANSACTION_PK.desc()) // to avoid fetching ghost transactions, fetch the latest + .fetchAny(TRANSACTION.TRANSACTION_PK); + } + @Override public TransactionDetails getDetails(int transactionPk) { diff --git a/src/main/java/de/rwth/idsg/steve/repository/impl/UserRepositoryImpl.java b/src/main/java/de/rwth/idsg/steve/repository/impl/UserRepositoryImpl.java index b24646dc1..a4504cae2 100644 --- a/src/main/java/de/rwth/idsg/steve/repository/impl/UserRepositoryImpl.java +++ b/src/main/java/de/rwth/idsg/steve/repository/impl/UserRepositoryImpl.java @@ -116,6 +116,42 @@ public User.Details getDetails(int userPk) { .build(); } + @Override + public User.Details getDetails(String ocppIdTag) { + Integer ocppPk = ctx.select(OCPP_TAG.OCPP_TAG_PK) + .from(OCPP_TAG) + .where(OCPP_TAG.ID_TAG.eq(ocppIdTag)) + .fetchOne(OCPP_TAG.OCPP_TAG_PK); + + if (ocppPk == null) { + throw new SteveException("There is no OCPP_Tag: '%s'", ocppIdTag); + } + + // ------------------------------------------------------------------------- + // 1. user table + // ------------------------------------------------------------------------- + + UserRecord ur = ctx.selectFrom(USER) + .where(USER.OCPP_TAG_PK.equal(ocppPk)) + .fetchOne(); + + if (ur == null) { + throw new SteveException("There is no user with OCPP_TAG '%s'", ocppIdTag); + } + + // ------------------------------------------------------------------------- + // 2. address table + // ------------------------------------------------------------------------- + + AddressRecord ar = addressRepository.get(ctx, ur.getAddressPk()); + + return User.Details.builder() + .userRecord(ur) + .address(ar) + .ocppIdTag(Optional.ofNullable(ocppIdTag)) + .build(); + } + @Override public void add(UserForm form) { ctx.transaction(configuration -> { diff --git a/src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java b/src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java index 2ff468d46..d3b48463f 100644 --- a/src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java +++ b/src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java @@ -27,6 +27,7 @@ import de.rwth.idsg.steve.repository.dto.UpdateTransactionParams; import de.rwth.idsg.steve.service.notification.OccpStationBooted; import de.rwth.idsg.steve.service.notification.OcppStationStatusFailure; +import de.rwth.idsg.steve.service.notification.OcppStationStatusSuspendedEV; import de.rwth.idsg.steve.service.notification.OcppTransactionEnded; import de.rwth.idsg.steve.service.notification.OcppTransactionStarted; import jooq.steve.db.enums.TransactionStopEventActor; @@ -147,6 +148,11 @@ public StatusNotificationResponse statusNotification( chargeBoxIdentity, parameters.getConnectorId(), parameters.getErrorCode().value())); } + if (parameters.getStatus() == ChargePointStatus.SUSPENDED_EV) { + applicationEventPublisher.publishEvent(new OcppStationStatusSuspendedEV( + chargeBoxIdentity, parameters.getConnectorId(), parameters.getTimestamp())); + } + return new StatusNotificationResponse(); } diff --git a/src/main/java/de/rwth/idsg/steve/service/MailService.java b/src/main/java/de/rwth/idsg/steve/service/MailService.java index ef382f5b0..25e60edf0 100644 --- a/src/main/java/de/rwth/idsg/steve/service/MailService.java +++ b/src/main/java/de/rwth/idsg/steve/service/MailService.java @@ -40,6 +40,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import static de.rwth.idsg.steve.utils.StringUtils.splitByComma; +import static de.rwth.idsg.steve.utils.StringUtils.isValidAddress; +import java.util.List; /** * @author Sevket Goekay @@ -81,7 +84,7 @@ public MailSettings getSettings() { public void sendTestMail() { try { - send("Test", "Test"); + send("Test", "Test", ""); } catch (MessagingException e) { throw new SteveException("Failed to send mail", e); } @@ -90,29 +93,54 @@ public void sendTestMail() { public void sendAsync(String subject, String body) { executorService.execute(() -> { try { - send(subject, body); + send(subject, body, ""); } catch (MessagingException e) { log.error("Failed to send mail", e); } }); } - public void send(String subject, String body) throws MessagingException { - MailSettings settings = getSettings(); + public void sendAsync(String subject, String body, String recipientAddresses) { + executorService.execute(() -> { + try { + send(subject, body, recipientAddresses); + } catch (MessagingException e) { + log.error("Failed to send mail", e); + } + }); + } + + private void send(String subject, String body, String recipientAddresses) throws MessagingException { + MailSettings settingsLocal = getSettings(); Message mail = new MimeMessage(session); mail.setSubject("[SteVe] " + subject); mail.setContent(body, "text/plain"); - mail.setFrom(new InternetAddress(settings.getFrom())); + mail.setFrom(new InternetAddress(settingsLocal.getFrom())); + + List eMailAddresses; - for (String rep : settings.getRecipients()) { - mail.addRecipient(Message.RecipientType.TO, new InternetAddress(rep)); + if (recipientAddresses.isEmpty()) { + eMailAddresses = settingsLocal.getRecipients(); + } else { + eMailAddresses = splitByComma(recipientAddresses); + } + + for (String rep : eMailAddresses) { + if (isValidAddress(rep)) { + mail.addRecipient(Message.RecipientType.TO, new InternetAddress(rep)); + } else { + log.error("Failed to send mail to " + rep + "! Format of the address is invalid."); + } } try (Transport transport = session.getTransport()) { transport.connect(); transport.sendMessage(mail, mail.getAllRecipients()); } + catch (Exception e) { + log.error("Failed to send mail(s)! ", e); + } } // ------------------------------------------------------------------------- diff --git a/src/main/java/de/rwth/idsg/steve/service/NotificationService.java b/src/main/java/de/rwth/idsg/steve/service/NotificationService.java index 402d8ea7b..f2a6d5560 100644 --- a/src/main/java/de/rwth/idsg/steve/service/NotificationService.java +++ b/src/main/java/de/rwth/idsg/steve/service/NotificationService.java @@ -28,6 +28,7 @@ import de.rwth.idsg.steve.service.notification.OcppStationWebSocketConnected; import de.rwth.idsg.steve.service.notification.OcppStationWebSocketDisconnected; import de.rwth.idsg.steve.service.notification.OcppTransactionEnded; +import de.rwth.idsg.steve.service.notification.OcppStationStatusSuspendedEV; import de.rwth.idsg.steve.service.notification.OcppTransactionStarted; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; @@ -40,8 +41,14 @@ import static de.rwth.idsg.steve.NotificationFeature.OcppStationWebSocketConnected; import static de.rwth.idsg.steve.NotificationFeature.OcppStationWebSocketDisconnected; import static de.rwth.idsg.steve.NotificationFeature.OcppTransactionStarted; +import static de.rwth.idsg.steve.NotificationFeature.OcppStationStatusSuspendedEV; import static de.rwth.idsg.steve.NotificationFeature.OcppTransactionEnded; +import de.rwth.idsg.steve.repository.TransactionRepository; +import de.rwth.idsg.steve.repository.UserRepository; +import de.rwth.idsg.steve.repository.dto.Transaction; import static java.lang.String.format; +import jooq.steve.db.tables.records.UserRecord; +import java.util.concurrent.ScheduledExecutorService; /** * @author Sevket Goekay @@ -52,6 +59,9 @@ public class NotificationService { @Autowired private MailService mailService; + @Autowired private TransactionRepository transactionRepository; + @Autowired private UserRepository userRepository; + @Autowired private ScheduledExecutorService executorService; @EventListener public void ocppStationBooted(OccpStationBooted notification) { @@ -98,7 +108,10 @@ public void ocppStationStatusFailure(OcppStationStatusFailure notification) { return; } - String subject = format("Connector '%s' of charging station '%s' is FAULTED", notification.getConnectorId(), notification.getChargeBoxId()); + String subject = format("Connector '%s' of charging station '%s' is FAULTED", + notification.getConnectorId(), + notification.getChargeBoxId() + ); String body = format("Status Error Code: '%s'", notification.getErrorCode()); mailService.sendAsync(subject, addTimestamp(body)); @@ -110,18 +123,123 @@ public void ocppTransactionStarted(OcppTransactionStarted notification) { return; } - String subject = format("Transaction '%s' has started on charging station '%s' on connector '%s'", notification.getTransactionId(), notification.getParams().getChargeBoxId(), notification.getParams().getConnectorId()); + String subject = format("Transaction '%s' has started on charging station '%s' on connector '%s'", + notification.getTransactionId(), + notification.getParams().getChargeBoxId(), + notification.getParams().getConnectorId() + ); mailService.sendAsync(subject, addTimestamp(createContent(notification.getParams()))); } + @EventListener + public void ocppStationStatusSuspendedEV(OcppStationStatusSuspendedEV notification) { + executorService.execute(() -> { + try { + notificationActionSuspendedEV(notification); + } catch (Exception e) { + log.error("Failed to execute the notification of SuspendedEV", e); + } + }); + } + + private void notificationActionSuspendedEV(OcppStationStatusSuspendedEV notification) { + String subject = format("EV stopped charging at charging station %s, Connector %d", + notification.getChargeBoxId(), + notification.getConnectorId() + ); + + Integer transactionPk = transactionRepository.getActiveTransactionId(notification.getChargeBoxId(), + notification.getConnectorId()); + if (transactionPk != null) { + Transaction transaction = transactionRepository.getTransaction(transactionPk); + String ocppTag = transaction.getOcppIdTag(); + if (ocppTag != null) { + // No mail directly after the start of the transaction, + if (notification.getTimestamp().isAfter(transaction.getStartTimestamp().plusMinutes(1))) { + String eMailAddress = null; + UserRecord userRecord = new UserRecord(); + try { + userRecord = userRepository.getDetails(ocppTag).getUserRecord(); + eMailAddress = userRecord.getEMail(); + } catch (Exception e) { + log.error("Failed to send email (SuspendedEV). User not found! " + e.getMessage()); + } + // send email if user with eMail address found + if (!Strings.isNullOrEmpty(eMailAddress)) { + String bodyUserMail = + format("User: %s %s \n\n Connector %d of charging station %s notifies Suspended_EV", + userRecord.getFirstName(), + userRecord.getLastName(), + notification.getConnectorId(), + notification.getChargeBoxId() + ); + mailService.sendAsync(subject, addTimestamp(bodyUserMail), eMailAddress); + } + } + } + } + + /* mail defined in settings */ + if (isDisabled(OcppStationStatusSuspendedEV)) { + return; + } + String body = format("Connector %d of charging station %s notifies Suspended_EV", + notification.getConnectorId(), + notification.getChargeBoxId() + ); + mailService.sendAsync(subject, addTimestamp(body)); + } + @EventListener public void ocppTransactionEnded(OcppTransactionEnded notification) { - if (isDisabled(OcppTransactionEnded)) { + executorService.execute(() -> { + try { + notificationActionTransactionEnded(notification); + } catch (Exception e) { + log.error("Failed to execute the notification of SuspendedEV", e); + } + }); + } + + private void notificationActionTransactionEnded(OcppTransactionEnded notification) { + String eMailAddress = null; + UserRecord userRecord = new UserRecord(); + + Transaction transActParams = transactionRepository.getTransaction(notification.getParams().getTransactionId()); + + try { + userRecord = userRepository.getDetails(transActParams.getOcppIdTag()).getUserRecord(); + eMailAddress = userRecord.getEMail(); + } catch (Exception e) { + log.error("Failed to send email (TransactionStop). User not found! " + e.getMessage()); + } + + // mail to user + if (!Strings.isNullOrEmpty(eMailAddress)) { + String subjectUserMail = format("Transaction '%s' has ended on charging station '%s'", + transActParams.getId(), + transActParams.getChargeBoxId() + ); + + // if the Transactionstop is received within the first Minute don't send an E-Mail + if (transActParams.getStopTimestamp().isAfter(transActParams.getStartTimestamp().plusMinutes(1))) { + mailService.sendAsync(subjectUserMail, + addTimestamp(createContent(transActParams, userRecord)), + eMailAddress + ); + } + } + + /* mail defined in settings */ + if (isDisabled(OcppTransactionEnded)) { return; } - String subject = format("Transaction '%s' has ended on charging station '%s'", notification.getParams().getTransactionId(), notification.getParams().getChargeBoxId()); + String subject = format("Transaction '%s' has ended on charging station '%s'", + notification.getParams().getTransactionId(), + notification.getParams().getChargeBoxId() + ); mailService.sendAsync(subject, addTimestamp(createContent(notification.getParams()))); } @@ -130,7 +248,6 @@ public void ocppTransactionEnded(OcppTransactionEnded notification) { // Private helpers // ------------------------------------------------------------------------- - private static String createContent(InsertTransactionParams params) { StringBuilder sb = new StringBuilder("Details:").append(System.lineSeparator()) .append("- chargeBoxId: ").append(params.getChargeBoxId()).append(System.lineSeparator()) @@ -156,6 +273,36 @@ private static String createContent(UpdateTransactionParams params) { .toString(); } + private static String createContent(Transaction params, UserRecord userRecord) { + Double meterValueDiff; + Integer meterValueStop; + Integer meterValueStart; + String strMeterValueDiff = "-"; + try { + meterValueStop = Integer.valueOf(params.getStopValue()); + meterValueStart = Integer.valueOf(params.getStartValue()); + meterValueDiff = (meterValueStop - meterValueStart) / 1000.0; // --> kWh + strMeterValueDiff = meterValueDiff.toString() + " kWh"; + } catch (NumberFormatException e) { + log.error("Failed to calculate charged energy! ", e); + } + + return new StringBuilder("User: ") + .append(userRecord.getFirstName()).append(" ").append(userRecord.getLastName()) + .append(System.lineSeparator()) + .append(System.lineSeparator()) + .append("Details:").append(System.lineSeparator()) + .append("- chargeBoxId: ").append(params.getChargeBoxId()).append(System.lineSeparator()) + .append("- connectorId: ").append(params.getConnectorId()).append(System.lineSeparator()) + .append("- transactionId: ").append(params.getId()).append(System.lineSeparator()) + .append("- startTimestamp (UTC): ").append(params.getStartTimestamp()).append(System.lineSeparator()) + .append("- startMeterValue: ").append(params.getStartValue()).append(System.lineSeparator()) + .append("- stopTimestamp (UTC): ").append(params.getStopTimestamp()).append(System.lineSeparator()) + .append("- stopMeterValue: ").append(params.getStopValue()).append(System.lineSeparator()) + .append("- stopReason: ").append(params.getStopReason()).append(System.lineSeparator()) + .append("- charged energy: ").append(strMeterValueDiff).append(System.lineSeparator()) + .toString(); + } private boolean isDisabled(NotificationFeature f) { MailSettings settings = mailService.getSettings(); diff --git a/src/main/java/de/rwth/idsg/steve/service/notification/OcppStationStatusSuspendedEV.java b/src/main/java/de/rwth/idsg/steve/service/notification/OcppStationStatusSuspendedEV.java new file mode 100644 index 000000000..fed6cbdd0 --- /dev/null +++ b/src/main/java/de/rwth/idsg/steve/service/notification/OcppStationStatusSuspendedEV.java @@ -0,0 +1,35 @@ +/* + * SteVe - SteckdosenVerwaltung - https://github.com/steve-community/steve + * Copyright (C) 2013-2024 SteVe Community Team + * All Rights Reserved. + * + * 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 3 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, see . + */ +package de.rwth.idsg.steve.service.notification; + +import lombok.Data; +import org.joda.time.DateTime; + +/** + * @author fnkbsi + * @since 12.10.2022 + */ + +@Data +public class OcppStationStatusSuspendedEV { + + private final String chargeBoxId; + private final int connectorId; + private final DateTime timestamp; +} diff --git a/src/main/java/de/rwth/idsg/steve/utils/StringUtils.java b/src/main/java/de/rwth/idsg/steve/utils/StringUtils.java index 91157505f..c5cabcc1b 100644 --- a/src/main/java/de/rwth/idsg/steve/utils/StringUtils.java +++ b/src/main/java/de/rwth/idsg/steve/utils/StringUtils.java @@ -32,6 +32,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; +import java.util.regex.Pattern; /** * @author Sevket Goekay @@ -95,4 +96,19 @@ public static String getLastBitFromUrl(final String input) { return input.substring(index + substring.length()); } } + + // https://www.baeldung.com/java-email-validation-regex + public static boolean isValidAddress(String emailAddress) { + // Strict Regular Expression Validation + String regexPattern = + "^(?=.{1,64}@)[A-Za-z0-9_-]+(\\.[A-Za-z0-9_-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$"; + //Regular Expression for Validation of Non-Latin or Unicode Characters Email + //String regexPattern = + // "^(?=.{1,64}@)[\\p{L}0-9_-]+(\\.[\\p{L}0-9_-]+)*@[^-][\\p{L}0-9-]+(\\.[\\p{L}0-9-]+)*(\\.[\\p{L}]{2,})$"; + // Regular Expression by RFC 5322 for Email Validation + //String regexPattern = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$" + return Pattern.compile(regexPattern) + .matcher(emailAddress) + .matches(); + } }