aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2017-02-25 17:55:09 +1300
committerGitHub <noreply@github.com>2017-02-25 17:55:09 +1300
commite4aa15f0eb0646d6158fd8386f8f9c5477db6bfe (patch)
tree05c597d59b9dde5199721e0076a372cfbceab419
parent981ab9d051d0b84cdc256fa48883f2c214e8bc3c (diff)
parente8500e8f6c440277d60e0418ab6a82f4bf80b887 (diff)
downloadtrackermap-server-e4aa15f0eb0646d6158fd8386f8f9c5477db6bfe.tar.gz
trackermap-server-e4aa15f0eb0646d6158fd8386f8f9c5477db6bfe.tar.bz2
trackermap-server-e4aa15f0eb0646d6158fd8386f8f9c5477db6bfe.zip
Merge pull request #2942 from Abyss777/smpp
Implement SMS notifications with help of smpp protocol
-rw-r--r--pom.xml5
-rw-r--r--schema/changelog-3.11.xml21
-rw-r--r--schema/changelog-master.xml1
-rw-r--r--setup/default.xml10
-rw-r--r--src/org/traccar/Context.java15
-rw-r--r--src/org/traccar/api/resource/NotificationResource.java12
-rw-r--r--src/org/traccar/database/NotificationManager.java14
-rw-r--r--src/org/traccar/model/Notification.java10
-rw-r--r--src/org/traccar/model/User.java10
-rw-r--r--src/org/traccar/notification/NotificationFormatter.java34
-rw-r--r--src/org/traccar/notification/NotificationMail.java5
-rw-r--r--src/org/traccar/notification/NotificationSms.java50
-rw-r--r--src/org/traccar/smpp/ClientSmppSessionHandler.java74
-rw-r--r--src/org/traccar/smpp/EnquireLinkTask.java56
-rw-r--r--src/org/traccar/smpp/ReconnectionTask.java31
-rw-r--r--src/org/traccar/smpp/SmppClient.java217
-rw-r--r--templates/sms/alarm.vm1
-rw-r--r--templates/sms/commandResult.vm1
-rw-r--r--templates/sms/deviceMoving.vm1
-rw-r--r--templates/sms/deviceOffline.vm1
-rw-r--r--templates/sms/deviceOnline.vm1
-rw-r--r--templates/sms/deviceOverspeed.vm8
-rw-r--r--templates/sms/deviceStopped.vm1
-rw-r--r--templates/sms/deviceUnknown.vm1
-rw-r--r--templates/sms/geofenceEnter.vm1
-rw-r--r--templates/sms/geofenceExit.vm1
-rw-r--r--templates/sms/ignitionOff.vm1
-rw-r--r--templates/sms/ignitionOn.vm1
-rw-r--r--templates/sms/maintenance.vm1
-rw-r--r--templates/sms/test.vm1
-rw-r--r--templates/sms/unknown.vm1
31 files changed, 565 insertions, 22 deletions
diff --git a/pom.xml b/pom.xml
index 643627b64..952a24795 100644
--- a/pom.xml
+++ b/pom.xml
@@ -148,6 +148,11 @@
<artifactId>ical4j</artifactId>
<version>2.0.0</version>
</dependency>
+ <dependency>
+ <groupId>com.fizzed</groupId>
+ <artifactId>ch-smpp</artifactId>
+ <version>5.0.9</version>
+ </dependency>
</dependencies>
<build>
diff --git a/schema/changelog-3.11.xml b/schema/changelog-3.11.xml
new file mode 100644
index 000000000..a8deb0295
--- /dev/null
+++ b/schema/changelog-3.11.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<databaseChangeLog
+ xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+ http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.4.xsd"
+ logicalFilePath="changelog-3.11">
+
+ <changeSet author="author" id="changelog-3.11">
+
+ <addColumn tableName="users">
+ <column name="phone" type="VARCHAR(128)" />
+ </addColumn>
+
+ <addColumn tableName="notifications">
+ <column name="sms" type="BOOLEAN" defaultValueBoolean="false" />
+ </addColumn>
+
+ </changeSet>
+
+</databaseChangeLog>
diff --git a/schema/changelog-master.xml b/schema/changelog-master.xml
index 2aed50e72..eaa5324f1 100644
--- a/schema/changelog-master.xml
+++ b/schema/changelog-master.xml
@@ -12,4 +12,5 @@
<include file="changelog-3.8.xml" relativeToChangelogFile="true" />
<include file="changelog-3.9.xml" relativeToChangelogFile="true" />
<include file="changelog-3.10.xml" relativeToChangelogFile="true" />
+ <include file="changelog-3.11.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git a/setup/default.xml b/setup/default.xml
index 7cbf9dab6..b5bc4202a 100644
--- a/setup/default.xml
+++ b/setup/default.xml
@@ -64,14 +64,15 @@
</entry>
<entry key='database.insertUser'>
- INSERT INTO users (name, email, hashedPassword, salt, readonly, admin, map, distanceUnit, speedUnit, latitude, longitude, zoom, twelveHourFormat, coordinateFormat, disabled, expirationTime, deviceLimit, userLimit, deviceReadonly, token, attributes)
- VALUES (:name, :email, :hashedPassword, :salt, :readonly, :admin, :map, :distanceUnit, :speedUnit, :latitude, :longitude, :zoom, :twelveHourFormat, :coordinateFormat, :disabled, :expirationTime, :deviceLimit, :userLimit, :deviceReadonly, :token, :attributes)
+ INSERT INTO users (name, email, phone, hashedPassword, salt, readonly, admin, map, distanceUnit, speedUnit, latitude, longitude, zoom, twelveHourFormat, coordinateFormat, disabled, expirationTime, deviceLimit, userLimit, deviceReadonly, token, attributes)
+ VALUES (:name, :email, :phone, :hashedPassword, :salt, :readonly, :admin, :map, :distanceUnit, :speedUnit, :latitude, :longitude, :zoom, :twelveHourFormat, :coordinateFormat, :disabled, :expirationTime, :deviceLimit, :userLimit, :deviceReadonly, :token, :attributes)
</entry>
<entry key='database.updateUser'>
UPDATE users SET
name = :name,
email = :email,
+ phone = :phone,
readonly = :readonly,
admin = :admin,
map = :map,
@@ -268,8 +269,8 @@
</entry>
<entry key='database.insertNotification'>
- INSERT INTO notifications (userId, type, web, mail, attributes)
- VALUES (:userId, :type, :web, :mail, :attributes)
+ INSERT INTO notifications (userId, type, web, mail, sms, attributes)
+ VALUES (:userId, :type, :web, :mail, :sms, :attributes)
</entry>
<entry key='database.updateNotification'>
@@ -278,6 +279,7 @@
type = :type,
web = :web,
mail = :mail,
+ sms = :sms,
attributes = :attributes
WHERE id = :id
</entry>
diff --git a/src/org/traccar/Context.java b/src/org/traccar/Context.java
index daabb8c32..68558630d 100644
--- a/src/org/traccar/Context.java
+++ b/src/org/traccar/Context.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 - 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2015 - 2017 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@ import org.traccar.geolocation.GeolocationProvider;
import org.traccar.geolocation.MozillaGeolocationProvider;
import org.traccar.geolocation.OpenCellIdGeolocationProvider;
import org.traccar.notification.EventForwarder;
+import org.traccar.smpp.SmppClient;
import org.traccar.web.WebServer;
public final class Context {
@@ -177,6 +178,12 @@ public final class Context {
return statisticsManager;
}
+ private static SmppClient smppClient;
+
+ public static SmppClient getSmppManager() {
+ return smppClient;
+ }
+
public static void init(String[] arguments) throws Exception {
config = new Config();
@@ -278,7 +285,7 @@ public final class Context {
notificationManager = new NotificationManager(dataManager);
Properties velocityProperties = new Properties();
velocityProperties.setProperty("file.resource.loader.path",
- Context.getConfig().getString("mail.templatesPath", "templates/mail") + "/");
+ Context.getConfig().getString("templates.rootPath", "templates") + "/");
velocityProperties.setProperty("runtime.log.logsystem.class",
"org.apache.velocity.runtime.log.NullLogChute");
@@ -307,6 +314,10 @@ public final class Context {
statisticsManager = new StatisticsManager();
+ if (config.getBoolean("sms.smpp.enable")) {
+ smppClient = new SmppClient();
+ }
+
}
public static void init(IdentityManager testIdentityManager) {
diff --git a/src/org/traccar/api/resource/NotificationResource.java b/src/org/traccar/api/resource/NotificationResource.java
index 03f7e4ba0..dee972607 100644
--- a/src/org/traccar/api/resource/NotificationResource.java
+++ b/src/org/traccar/api/resource/NotificationResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,12 @@ import org.traccar.api.BaseResource;
import org.traccar.model.Event;
import org.traccar.model.Notification;
import org.traccar.notification.NotificationMail;
+import org.traccar.notification.NotificationSms;
+
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
@Path("users/notifications")
@Produces(MediaType.APPLICATION_JSON)
@@ -62,8 +68,10 @@ public class NotificationResource extends BaseResource {
@Path("test")
@POST
- public Response testMail() throws MessagingException {
+ public Response testMessage() throws MessagingException, RecoverablePduException,
+ UnrecoverablePduException, SmppTimeoutException, SmppChannelException, InterruptedException {
NotificationMail.sendMailSync(getUserId(), new Event("test", 0), null);
+ NotificationSms.sendSmsSync(getUserId(), new Event("test", 0), null);
return Response.noContent().build();
}
diff --git a/src/org/traccar/database/NotificationManager.java b/src/org/traccar/database/NotificationManager.java
index 4ae173c6a..48caa615c 100644
--- a/src/org/traccar/database/NotificationManager.java
+++ b/src/org/traccar/database/NotificationManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@ import org.traccar.model.Event;
import org.traccar.model.Notification;
import org.traccar.model.Position;
import org.traccar.notification.NotificationMail;
+import org.traccar.notification.NotificationSms;
public class NotificationManager {
@@ -65,6 +66,9 @@ public class NotificationManager {
if (notification.getMail()) {
NotificationMail.sendMailAsync(userId, event, position);
}
+ if (notification.getSms()) {
+ NotificationSms.sendSmsAsync(userId, event, position);
+ }
}
}
}
@@ -131,8 +135,9 @@ public class NotificationManager {
Notification cachedNotification = getUserNotificationByType(notification.getUserId(), notification.getType());
if (cachedNotification != null) {
if (cachedNotification.getWeb() != notification.getWeb()
- || cachedNotification.getMail() != notification.getMail()) {
- if (!notification.getWeb() && !notification.getMail()) {
+ || cachedNotification.getMail() != notification.getMail()
+ || cachedNotification.getSms() != notification.getSms()) {
+ if (!notification.getWeb() && !notification.getMail() && !notification.getSms()) {
try {
dataManager.removeNotification(cachedNotification);
} catch (SQLException error) {
@@ -149,6 +154,7 @@ public class NotificationManager {
try {
cachedNotification.setWeb(notification.getWeb());
cachedNotification.setMail(notification.getMail());
+ cachedNotification.setSms(notification.getSms());
cachedNotification.setAttributes(notification.getAttributes());
} finally {
notificationsLock.writeLock().unlock();
@@ -162,7 +168,7 @@ public class NotificationManager {
} else {
notification.setId(cachedNotification.getId());
}
- } else if (notification.getWeb() || notification.getMail()) {
+ } else if (notification.getWeb() || notification.getMail() || notification.getSms()) {
try {
dataManager.addNotification(notification);
} catch (SQLException error) {
diff --git a/src/org/traccar/model/Notification.java b/src/org/traccar/model/Notification.java
index dd5f66f15..6c61cafaf 100644
--- a/src/org/traccar/model/Notification.java
+++ b/src/org/traccar/model/Notification.java
@@ -56,4 +56,14 @@ public class Notification extends Extensible {
public void setMail(boolean mail) {
this.mail = mail;
}
+
+ private boolean sms;
+
+ public boolean getSms() {
+ return sms;
+ }
+
+ public void setSms(boolean sms) {
+ this.sms = sms;
+ }
}
diff --git a/src/org/traccar/model/User.java b/src/org/traccar/model/User.java
index a89a75dd8..274f2b2a2 100644
--- a/src/org/traccar/model/User.java
+++ b/src/org/traccar/model/User.java
@@ -42,6 +42,16 @@ public class User extends Extensible {
this.email = email;
}
+ private String phone;
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
private boolean readonly;
public boolean getReadonly() {
diff --git a/src/org/traccar/notification/NotificationFormatter.java b/src/org/traccar/notification/NotificationFormatter.java
index b68d53ee0..cec238548 100644
--- a/src/org/traccar/notification/NotificationFormatter.java
+++ b/src/org/traccar/notification/NotificationFormatter.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +34,7 @@ public final class NotificationFormatter {
private NotificationFormatter() {
}
- public static MailMessage formatMessage(long userId, Event event, Position position) {
+ private static VelocityContext prepareContext(long userId, Event event, Position position) {
Device device = Context.getIdentityManager().getDeviceById(event.getDeviceId());
VelocityContext velocityContext = new VelocityContext();
@@ -47,18 +48,35 @@ public final class NotificationFormatter {
velocityContext.put("geofence", Context.getGeofenceManager().getGeofence(event.getGeofenceId()));
}
velocityContext.put("webUrl", Context.getVelocityEngine().getProperty("web.url"));
+ return velocityContext;
+ }
- Template template = null;
+ private static Template getTemplate(Event event, String path) {
+ Template template;
try {
- template = Context.getVelocityEngine().getTemplate(event.getType() + ".vm", StandardCharsets.UTF_8.name());
+ template = Context.getVelocityEngine().getTemplate(path + event.getType() + ".vm",
+ StandardCharsets.UTF_8.name());
} catch (ResourceNotFoundException error) {
Log.warning(error);
- template = Context.getVelocityEngine().getTemplate("unknown.vm", StandardCharsets.UTF_8.name());
+ template = Context.getVelocityEngine().getTemplate(path + "unknown.vm",
+ StandardCharsets.UTF_8.name());
}
+ return template;
+ }
+
+ public static MailMessage formatMailMessage(long userId, Event event, Position position) {
+ VelocityContext velocityContext = prepareContext(userId, event, position);
+ StringWriter writer = new StringWriter();
+ getTemplate(event, Context.getConfig().getString("mail.templatesPath", "mail") + "/")
+ .merge(velocityContext, writer);
+ return new MailMessage((String) velocityContext.get("subject"), writer.toString());
+ }
+ public static String formatSmsMessage(long userId, Event event, Position position) {
+ VelocityContext velocityContext = prepareContext(userId, event, position);
StringWriter writer = new StringWriter();
- template.merge(velocityContext, writer);
- String subject = (String) velocityContext.get("subject");
- return new MailMessage(subject, writer.toString());
+ getTemplate(event, Context.getConfig().getString("sms.templatesPath", "sms") + "/")
+ .merge(velocityContext, writer);
+ return writer.toString();
}
}
diff --git a/src/org/traccar/notification/NotificationMail.java b/src/org/traccar/notification/NotificationMail.java
index 7b7ef6e74..41cb2e681 100644
--- a/src/org/traccar/notification/NotificationMail.java
+++ b/src/org/traccar/notification/NotificationMail.java
@@ -1,5 +1,6 @@
/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -106,7 +107,7 @@ public final class NotificationMail {
}
message.addRecipient(Message.RecipientType.TO, new InternetAddress(user.getEmail()));
- MailMessage mailMessage = NotificationFormatter.formatMessage(userId, event, position);
+ MailMessage mailMessage = NotificationFormatter.formatMailMessage(userId, event, position);
message.setSubject(mailMessage.getSubject());
message.setContent(mailMessage.getBody(), "text/html; charset=utf-8");
diff --git a/src/org/traccar/notification/NotificationSms.java b/src/org/traccar/notification/NotificationSms.java
new file mode 100644
index 000000000..cb5dd563a
--- /dev/null
+++ b/src/org/traccar/notification/NotificationSms.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.notification;
+
+import org.traccar.Context;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.model.User;
+
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public final class NotificationSms {
+
+ private NotificationSms() {
+ }
+
+ public static void sendSmsAsync(long userId, Event event, Position position) {
+ User user = Context.getPermissionsManager().getUser(userId);
+ if (Context.getSmppManager() != null && user.getPhone() != null) {
+ Context.getSmppManager().sendMessageAsync(user.getPhone(),
+ NotificationFormatter.formatSmsMessage(userId, event, position));
+ }
+ }
+
+ public static void sendSmsSync(long userId, Event event, Position position) throws RecoverablePduException,
+ UnrecoverablePduException, SmppTimeoutException, SmppChannelException, InterruptedException {
+ User user = Context.getPermissionsManager().getUser(userId);
+ if (Context.getSmppManager() != null && user.getPhone() != null) {
+ Context.getSmppManager().sendMessageSync(user.getPhone(),
+ NotificationFormatter.formatSmsMessage(userId, event, position));
+ }
+ }
+}
diff --git a/src/org/traccar/smpp/ClientSmppSessionHandler.java b/src/org/traccar/smpp/ClientSmppSessionHandler.java
new file mode 100644
index 000000000..721243f9f
--- /dev/null
+++ b/src/org/traccar/smpp/ClientSmppSessionHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.smpp;
+
+import org.traccar.helper.Log;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.DeliverSm;
+import com.cloudhopper.smpp.pdu.PduRequest;
+import com.cloudhopper.smpp.pdu.PduResponse;
+
+public class ClientSmppSessionHandler extends DefaultSmppSessionHandler {
+
+ private SmppClient smppClient;
+
+ public ClientSmppSessionHandler(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void firePduRequestExpired(PduRequest pduRequest) {
+ Log.warning("PDU request expired: " + pduRequest);
+ }
+
+ @Override
+ public PduResponse firePduRequestReceived(PduRequest request) {
+ PduResponse response = null;
+ try {
+ if (request instanceof DeliverSm) {
+ if (request.getOptionalParameters() != null) {
+ Log.debug("Message Delivered: "
+ + request.getOptionalParameter(SmppConstants.TAG_RECEIPTED_MSG_ID).getValueAsString()
+ + ", State: "
+ + request.getOptionalParameter(SmppConstants.TAG_MSG_STATE).getValueAsByte());
+ } else {
+ Log.debug("Message Received: "
+ + CharsetUtil.decode(((DeliverSm) request).getShortMessage(),
+ smppClient.mapDataCodingToCharset(((DeliverSm) request).getDataCoding()))
+ + ", Source Address: "
+ + ((DeliverSm) request).getSourceAddress().getAddress());
+ }
+ }
+ response = request.createResponse();
+ } catch (Throwable error) {
+ Log.warning(error);
+ response = request.createResponse();
+ response.setResultMessage(error.getMessage());
+ response.setCommandStatus(SmppConstants.STATUS_UNKNOWNERR);
+ }
+ return response;
+ }
+
+ @Override
+ public void fireChannelUnexpectedlyClosed() {
+ Log.warning("Smpp session channel unexpectedly closed");
+ smppClient.scheduleReconnect();
+ }
+}
diff --git a/src/org/traccar/smpp/EnquireLinkTask.java b/src/org/traccar/smpp/EnquireLinkTask.java
new file mode 100644
index 000000000..9a3121e24
--- /dev/null
+++ b/src/org/traccar/smpp/EnquireLinkTask.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.smpp;
+
+import org.traccar.helper.Log;
+
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.pdu.EnquireLink;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class EnquireLinkTask implements Runnable {
+
+ private SmppClient smppClient;
+ private Integer enquireLinkTimeout;
+
+ public EnquireLinkTask(SmppClient smppClient, Integer enquireLinkTimeout) {
+ this.smppClient = smppClient;
+ this.enquireLinkTimeout = enquireLinkTimeout;
+ }
+
+ @Override
+ public void run() {
+ SmppSession smppSession = smppClient.getSession();
+ if (smppSession != null && smppSession.isBound()) {
+ try {
+ smppSession.enquireLink(new EnquireLink(), enquireLinkTimeout);
+ } catch (SmppTimeoutException | SmppChannelException
+ | RecoverablePduException | UnrecoverablePduException error) {
+ Log.warning("Enquire link failed, executing reconnect: ", error);
+ smppClient.reconnect();
+ } catch (InterruptedException error) {
+ Log.info("Enquire link interrupted, probably killed by reconnecting");
+ }
+ } else {
+ Log.warning("Enquire link running while session is not connected");
+ }
+ }
+
+}
diff --git a/src/org/traccar/smpp/ReconnectionTask.java b/src/org/traccar/smpp/ReconnectionTask.java
new file mode 100644
index 000000000..c9d9173ae
--- /dev/null
+++ b/src/org/traccar/smpp/ReconnectionTask.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.smpp;
+
+public class ReconnectionTask implements Runnable {
+
+ private final SmppClient smppClient;
+
+ protected ReconnectionTask(SmppClient smppClient) {
+ this.smppClient = smppClient;
+ }
+
+ @Override
+ public void run() {
+ smppClient.reconnect();
+ }
+}
diff --git a/src/org/traccar/smpp/SmppClient.java b/src/org/traccar/smpp/SmppClient.java
new file mode 100644
index 000000000..3680d20e2
--- /dev/null
+++ b/src/org/traccar/smpp/SmppClient.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2017 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.smpp;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import org.traccar.Context;
+import org.traccar.helper.Log;
+
+import com.cloudhopper.commons.charset.CharsetUtil;
+import com.cloudhopper.smpp.SmppBindType;
+import com.cloudhopper.smpp.SmppConstants;
+import com.cloudhopper.smpp.SmppSession;
+import com.cloudhopper.smpp.SmppSessionConfiguration;
+import com.cloudhopper.smpp.impl.DefaultSmppClient;
+import com.cloudhopper.smpp.impl.DefaultSmppSessionHandler;
+import com.cloudhopper.smpp.pdu.SubmitSm;
+import com.cloudhopper.smpp.pdu.SubmitSmResp;
+import com.cloudhopper.smpp.type.Address;
+import com.cloudhopper.smpp.type.RecoverablePduException;
+import com.cloudhopper.smpp.type.SmppChannelException;
+import com.cloudhopper.smpp.type.SmppTimeoutException;
+import com.cloudhopper.smpp.type.UnrecoverablePduException;
+
+public class SmppClient {
+
+ private SmppSessionConfiguration sessionConfig = new SmppSessionConfiguration();
+ private SmppSession smppSession;
+ private DefaultSmppSessionHandler sessionHandler = new ClientSmppSessionHandler(this);
+ private ExecutorService executorService = Executors.newCachedThreadPool();
+ private DefaultSmppClient clientBootstrap = new DefaultSmppClient(executorService, 1);
+
+ private ScheduledExecutorService enquireLinkExecutor;
+ private ScheduledFuture<?> enquireLinkTask;
+ private Integer enquireLinkPeriod;
+ private Integer enquireLinkTimeout;
+
+ private ScheduledExecutorService reconnectionExecutor;
+ private ScheduledFuture<?> reconnectionTask;
+ private Integer reconnectionDelay;
+
+ private String sourceAddress;
+ private int submitTimeout;
+ private String charsetName;
+ private byte dataCoding;
+
+ private byte sourceTon;
+ private byte sourceNpi;
+
+ private byte destTon;
+ private byte destNpi;
+
+ public SmppClient() {
+ sessionConfig.setName("Traccar.smppSession");
+ sessionConfig.setInterfaceVersion(
+ (byte) Context.getConfig().getInteger("sms.smpp.version", SmppConstants.VERSION_3_4));
+ sessionConfig.setType(SmppBindType.TRANSCEIVER);
+ sessionConfig.setHost(Context.getConfig().getString("sms.smpp.host", "localhost"));
+ sessionConfig.setPort(Context.getConfig().getInteger("sms.smpp.port", 2775));
+ sessionConfig.setSystemId(Context.getConfig().getString("sms.smpp.username", "user"));
+ sessionConfig.setPassword(Context.getConfig().getString("sms.smpp.password", "password"));
+ sessionConfig.getLoggingOptions().setLogBytes(false);
+ sessionConfig.getLoggingOptions().setLogPdu(Context.getConfig().getBoolean("sms.smpp.logPdu"));
+
+ sourceAddress = Context.getConfig().getString("sms.smpp.sourceAddress", "");
+ submitTimeout = Context.getConfig().getInteger("sms.smpp.submitTimeout", 10000);
+
+ charsetName = Context.getConfig().getString("sms.smpp.charsetName", CharsetUtil.NAME_UCS_2);
+ dataCoding = (byte) Context.getConfig().getInteger("sms.smpp.dataCoding", SmppConstants.DATA_CODING_UCS2);
+
+ sourceTon = (byte) Context.getConfig().getInteger("sms.smpp.sourceTon", SmppConstants.TON_ALPHANUMERIC);
+ sourceNpi = (byte) Context.getConfig().getInteger("sms.smpp.sourceNpi", SmppConstants.NPI_UNKNOWN);
+
+ destTon = (byte) Context.getConfig().getInteger("sms.smpp.destTon", SmppConstants.TON_INTERNATIONAL);
+ destNpi = (byte) Context.getConfig().getInteger("sms.smpp.destNpi", SmppConstants.NPI_E164);
+
+ enquireLinkPeriod = Context.getConfig().getInteger("sms.smpp.enquireLinkPeriod", 60000);
+ enquireLinkTimeout = Context.getConfig().getInteger("sms.smpp.enquireLinkTimeout", 10000);
+ enquireLinkExecutor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("EnquireLink-" + name);
+ return thread;
+ }
+ });
+
+ reconnectionDelay = Context.getConfig().getInteger("sms.smpp.reconnectionDelay", 10000);
+ reconnectionExecutor = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable);
+ String name = sessionConfig.getName();
+ thread.setName("Reconnection-" + name);
+ return thread;
+ }
+ });
+
+ scheduleReconnect();
+ }
+
+ public synchronized SmppSession getSession() {
+ return smppSession;
+ }
+
+ public String mapDataCodingToCharset(byte dataCoding) {
+ switch (dataCoding) {
+ case SmppConstants.DATA_CODING_LATIN1:
+ return CharsetUtil.NAME_ISO_8859_1;
+ case SmppConstants.DATA_CODING_UCS2:
+ return CharsetUtil.NAME_UCS_2;
+ default:
+ return CharsetUtil.NAME_GSM;
+ }
+ }
+
+ protected synchronized void reconnect() {
+ try {
+ disconnect();
+ smppSession = clientBootstrap.bind(sessionConfig, sessionHandler);
+ stopReconnectionkTask();
+ runEnquireLinkTask();
+ Log.info("Smpp session connected");
+ } catch (SmppTimeoutException | SmppChannelException
+ | UnrecoverablePduException | InterruptedException error) {
+ Log.warning("Unable to connect to smpp server: ", error);
+ }
+ }
+
+ public void scheduleReconnect() {
+ reconnectionTask = reconnectionExecutor.scheduleWithFixedDelay(
+ new ReconnectionTask(this),
+ reconnectionDelay, reconnectionDelay, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopReconnectionkTask() {
+ if (reconnectionTask != null) {
+ reconnectionTask.cancel(false);
+ }
+ }
+
+ private void disconnect() {
+ stopEnquireLinkTask();
+ destroySession();
+ }
+
+ private void runEnquireLinkTask() {
+ enquireLinkTask = enquireLinkExecutor.scheduleWithFixedDelay(
+ new EnquireLinkTask(this, enquireLinkTimeout),
+ enquireLinkPeriod, enquireLinkPeriod, TimeUnit.MILLISECONDS);
+ }
+
+ private void stopEnquireLinkTask() {
+ if (enquireLinkTask != null) {
+ enquireLinkTask.cancel(true);
+ }
+ }
+
+ private void destroySession() {
+ if (smppSession != null) {
+ Log.debug("Cleaning up smpp session... ");
+ smppSession.destroy();
+ smppSession = null;
+ }
+ }
+
+ public synchronized void sendMessageSync(String destAddress, String message) throws RecoverablePduException,
+ UnrecoverablePduException, SmppTimeoutException, SmppChannelException, InterruptedException {
+ if (getSession() != null && getSession().isBound()) {
+ byte[] textBytes = CharsetUtil.encode(message, charsetName);
+
+ SubmitSm submit = new SubmitSm();
+ submit.setSourceAddress(new Address(sourceTon, sourceNpi, sourceAddress));
+ submit.setDestAddress(new Address(destTon, destNpi, destAddress));
+ submit.setDataCoding(dataCoding);
+ submit.setShortMessage(textBytes);
+ SubmitSmResp submitResponce = getSession().submit(submit, submitTimeout);
+ Log.debug("SMS submited, msg_id: " + submitResponce.getMessageId());
+ } else {
+ throw new SmppChannelException("Smpp session is not connected");
+ }
+ }
+
+ public void sendMessageAsync(final String destAddress, final String message) {
+ executorService.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sendMessageSync(destAddress, message);
+ } catch (InterruptedException | RecoverablePduException | UnrecoverablePduException
+ | SmppTimeoutException | SmppChannelException error) {
+ Log.warning(error);
+ }
+ }
+ });
+ }
+}
diff --git a/templates/sms/alarm.vm b/templates/sms/alarm.vm
new file mode 100644
index 000000000..e35a573b3
--- /dev/null
+++ b/templates/sms/alarm.vm
@@ -0,0 +1 @@
+$device.name alarm: $position.getString("alarm") at $event.serverTime
diff --git a/templates/sms/commandResult.vm b/templates/sms/commandResult.vm
new file mode 100644
index 000000000..38fddcf25
--- /dev/null
+++ b/templates/sms/commandResult.vm
@@ -0,0 +1 @@
+$device.name command result received: $position.getString("result") at $event.serverTime
diff --git a/templates/sms/deviceMoving.vm b/templates/sms/deviceMoving.vm
new file mode 100644
index 000000000..febae6331
--- /dev/null
+++ b/templates/sms/deviceMoving.vm
@@ -0,0 +1 @@
+$device.name moving at $event.serverTime
diff --git a/templates/sms/deviceOffline.vm b/templates/sms/deviceOffline.vm
new file mode 100644
index 000000000..490502946
--- /dev/null
+++ b/templates/sms/deviceOffline.vm
@@ -0,0 +1 @@
+$device.name offline at $event.serverTime
diff --git a/templates/sms/deviceOnline.vm b/templates/sms/deviceOnline.vm
new file mode 100644
index 000000000..e8bc0a5bf
--- /dev/null
+++ b/templates/sms/deviceOnline.vm
@@ -0,0 +1 @@
+$device.name online at $event.serverTime
diff --git a/templates/sms/deviceOverspeed.vm b/templates/sms/deviceOverspeed.vm
new file mode 100644
index 000000000..63f967bc3
--- /dev/null
+++ b/templates/sms/deviceOverspeed.vm
@@ -0,0 +1,8 @@
+#if($speedUnits == 'kmh')
+#set($speedString = $position.speed * 1.852 + ' km/h')
+#elseif($speedUnits == 'mph')
+#set($speedString = $position.speed * 1.15078 + ' mph')
+#else
+#set($speedString = "$position.speed kn")
+#end
+$device.name exceeds the speed $speedString at $event.serverTime
diff --git a/templates/sms/deviceStopped.vm b/templates/sms/deviceStopped.vm
new file mode 100644
index 000000000..cf9118cac
--- /dev/null
+++ b/templates/sms/deviceStopped.vm
@@ -0,0 +1 @@
+$device.name stopped at $event.serverTime
diff --git a/templates/sms/deviceUnknown.vm b/templates/sms/deviceUnknown.vm
new file mode 100644
index 000000000..533219799
--- /dev/null
+++ b/templates/sms/deviceUnknown.vm
@@ -0,0 +1 @@
+$device.name status is unknown at $event.serverTime
diff --git a/templates/sms/geofenceEnter.vm b/templates/sms/geofenceEnter.vm
new file mode 100644
index 000000000..5a216f361
--- /dev/null
+++ b/templates/sms/geofenceEnter.vm
@@ -0,0 +1 @@
+$device.name has entered geofence $geofence.name at $event.serverTime
diff --git a/templates/sms/geofenceExit.vm b/templates/sms/geofenceExit.vm
new file mode 100644
index 000000000..fd8a8409d
--- /dev/null
+++ b/templates/sms/geofenceExit.vm
@@ -0,0 +1 @@
+$device.name has exited geofence $geofence.name at $event.serverTime
diff --git a/templates/sms/ignitionOff.vm b/templates/sms/ignitionOff.vm
new file mode 100644
index 000000000..6f90eb552
--- /dev/null
+++ b/templates/sms/ignitionOff.vm
@@ -0,0 +1 @@
+$device.name ignition OFF at $event.serverTime
diff --git a/templates/sms/ignitionOn.vm b/templates/sms/ignitionOn.vm
new file mode 100644
index 000000000..9c0d79888
--- /dev/null
+++ b/templates/sms/ignitionOn.vm
@@ -0,0 +1 @@
+$device.name ignition ON at $event.serverTime
diff --git a/templates/sms/maintenance.vm b/templates/sms/maintenance.vm
new file mode 100644
index 000000000..f2e6940f6
--- /dev/null
+++ b/templates/sms/maintenance.vm
@@ -0,0 +1 @@
+$device.name maintenance is required at $event.serverTime
diff --git a/templates/sms/test.vm b/templates/sms/test.vm
new file mode 100644
index 000000000..0311b6203
--- /dev/null
+++ b/templates/sms/test.vm
@@ -0,0 +1 @@
+Traccar test message
diff --git a/templates/sms/unknown.vm b/templates/sms/unknown.vm
new file mode 100644
index 000000000..fd20b50cc
--- /dev/null
+++ b/templates/sms/unknown.vm
@@ -0,0 +1 @@
+Unknown type