diff --git a/application/pom.xml b/application/pom.xml
index 1b89752925..98a65bc5bb 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -249,6 +249,10 @@
io.grpc
grpc-stub
+
+ org.opensmpp
+ opensmpp-core
+
org.thingsboard
springfox-boot-starter
diff --git a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
index 2591790137..b42dcb6b83 100644
--- a/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
+++ b/application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
@@ -19,9 +19,11 @@ import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.sms.SmsSender;
import org.thingsboard.rule.engine.api.sms.SmsSenderFactory;
import org.thingsboard.server.common.data.sms.config.AwsSnsSmsProviderConfiguration;
+import org.thingsboard.server.common.data.sms.config.SmppSmsProviderConfiguration;
import org.thingsboard.server.common.data.sms.config.SmsProviderConfiguration;
import org.thingsboard.server.common.data.sms.config.TwilioSmsProviderConfiguration;
import org.thingsboard.server.service.sms.aws.AwsSmsSender;
+import org.thingsboard.server.service.sms.smpp.SmppSmsSender;
import org.thingsboard.server.service.sms.twilio.TwilioSmsSender;
@Component
@@ -34,6 +36,8 @@ public class DefaultSmsSenderFactory implements SmsSenderFactory {
return new AwsSmsSender((AwsSnsSmsProviderConfiguration)config);
case TWILIO:
return new TwilioSmsSender((TwilioSmsProviderConfiguration)config);
+ case SMPP:
+ return new SmppSmsSender((SmppSmsProviderConfiguration) config);
default:
throw new RuntimeException("Unknown SMS provider type " + config.getType());
}
diff --git a/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java b/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java
new file mode 100644
index 0000000000..57c5c5d25c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java
@@ -0,0 +1,186 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.service.sms.smpp;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.smpp.Connection;
+import org.smpp.Data;
+import org.smpp.Session;
+import org.smpp.TCPIPConnection;
+import org.smpp.TimeoutException;
+import org.smpp.WrongSessionStateException;
+import org.smpp.pdu.Address;
+import org.smpp.pdu.BindReceiver;
+import org.smpp.pdu.BindRequest;
+import org.smpp.pdu.BindResponse;
+import org.smpp.pdu.BindTransciever;
+import org.smpp.pdu.BindTransmitter;
+import org.smpp.pdu.PDUException;
+import org.smpp.pdu.SubmitSM;
+import org.smpp.pdu.SubmitSMResp;
+import org.thingsboard.rule.engine.api.sms.exception.SmsException;
+import org.thingsboard.server.common.data.sms.config.SmppSmsProviderConfiguration;
+import org.thingsboard.server.service.sms.AbstractSmsSender;
+
+import java.io.IOException;
+import java.util.Optional;
+
+@Slf4j
+public class SmppSmsSender extends AbstractSmsSender {
+ protected SmppSmsProviderConfiguration config;
+
+ protected Session smppSession;
+
+ public SmppSmsSender(SmppSmsProviderConfiguration config) {
+ if (config.getBindType() == null) {
+ config.setBindType(SmppSmsProviderConfiguration.SmppBindType.TX);
+ }
+ if (StringUtils.isNotEmpty(config.getSourceAddress())) {
+ if (config.getSourceTon() == null) {
+ config.setSourceTon((byte) 5);
+ }
+ if (config.getSourceNpi() == null) {
+ config.setSourceNpi((byte) 0);
+ }
+ }
+ if (config.getDestinationTon() == null) {
+ config.setDestinationTon((byte) 5);
+ }
+ if (config.getDestinationNpi() == null) {
+ config.setDestinationNpi((byte) 0);
+ }
+
+ this.config = config;
+ this.smppSession = initSmppSession();
+ }
+
+ private SmppSmsSender() {} // for testing purposes
+
+
+ @Override
+ public int sendSms(String numberTo, String message) throws SmsException {
+ try {
+ checkSmppSession();
+
+ SubmitSM request = new SubmitSM();
+ if (StringUtils.isNotEmpty(config.getServiceType())) {
+ request.setServiceType(config.getServiceType());
+ }
+ if (StringUtils.isNotEmpty(config.getSourceAddress())) {
+ request.setSourceAddr(new Address(config.getSourceTon(), config.getSourceNpi(), config.getSourceAddress()));
+ }
+ request.setDestAddr(new Address(config.getDestinationTon(), config.getDestinationNpi(), prepareNumber(numberTo)));
+ request.setShortMessage(message);
+ request.setDataCoding(Optional.ofNullable(config.getCodingScheme()).orElse((byte) 0));
+ request.setReplaceIfPresentFlag((byte) 0);
+ request.setEsmClass((byte) 0);
+ request.setProtocolId((byte) 0);
+ request.setPriorityFlag((byte) 0);
+ request.setRegisteredDelivery((byte) 0);
+ request.setSmDefaultMsgId((byte) 0);
+
+ SubmitSMResp response = smppSession.submit(request);
+
+ log.debug("SMPP submit command status: {}", response.getCommandStatus());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ return countMessageSegments(message);
+ }
+
+ private synchronized void checkSmppSession() {
+ if (smppSession == null || !smppSession.isOpened()) {
+ smppSession = initSmppSession();
+ }
+ }
+
+ protected Session initSmppSession() {
+ try {
+ Connection connection = new TCPIPConnection(config.getHost(), config.getPort());
+ Session session = new Session(connection);
+
+ BindRequest bindRequest;
+ switch (config.getBindType()) {
+ case TX:
+ bindRequest = new BindTransmitter();
+ break;
+ case RX:
+ bindRequest = new BindReceiver();
+ break;
+ case TRX:
+ bindRequest = new BindTransciever();
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported bind type " + config.getBindType());
+ }
+
+ bindRequest.setSystemId(config.getSystemId());
+ bindRequest.setPassword(config.getPassword());
+
+ byte interfaceVersion;
+ switch (config.getProtocolVersion()) {
+ case "3.3":
+ interfaceVersion = Data.SMPP_V33;
+ break;
+ case "3.4":
+ interfaceVersion = Data.SMPP_V34;
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported SMPP version: " + config.getProtocolVersion());
+ }
+ bindRequest.setInterfaceVersion(interfaceVersion);
+
+ if (StringUtils.isNotEmpty(config.getSystemType())) {
+ bindRequest.setSystemType(config.getSystemType());
+ }
+ if (StringUtils.isNotEmpty(config.getAddressRange())) {
+ bindRequest.setAddressRange(config.getDestinationTon(), config.getDestinationNpi(), config.getAddressRange());
+ }
+
+ BindResponse bindResponse = session.bind(bindRequest);
+ log.debug("SMPP bind response: {}", bindResponse.debugString());
+
+ if (bindResponse.getCommandStatus() != 0) {
+ throw new IllegalStateException("Error status when binding: " + bindResponse.getCommandStatus());
+ }
+
+ return session;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to establish SMPP session: " + ExceptionUtils.getRootCauseMessage(e), e);
+ }
+ }
+
+ private String prepareNumber(String number) {
+ if (config.getDestinationTon() == Data.GSM_TON_INTERNATIONAL) {
+ return StringUtils.removeStart(number, "+");
+ }
+ return number;
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ smppSession.unbind();
+ smppSession.close();
+ } catch (TimeoutException | PDUException | IOException | WrongSessionStateException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
new file mode 100644
index 0000000000..7cbc8c03d2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.service.sms.smpp;
+
+import org.apache.commons.lang3.StringUtils;
+import org.assertj.core.api.Assertions;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.smpp.Session;
+import org.smpp.pdu.SubmitSMResp;
+import org.thingsboard.server.common.data.sms.config.SmppSmsProviderConfiguration;
+
+import java.lang.reflect.Constructor;
+import java.net.UnknownHostException;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SmppSmsSenderTest {
+
+ SmppSmsSender smppSmsSender;
+ SmppSmsProviderConfiguration smppConfig;
+ Session smppSession;
+
+ @Before
+ public void beforeEach() throws Exception {
+ Constructor constructor = SmppSmsSender.class.getDeclaredConstructor();
+ constructor.setAccessible(true);
+ smppSmsSender = spy(constructor.newInstance());
+
+ smppSession = mock(Session.class);
+ smppSmsSender.smppSession = smppSession;
+
+ smppConfig = new SmppSmsProviderConfiguration();
+ smppSmsSender.config = smppConfig;
+ }
+
+ @Test
+ public void testSendSms() throws Exception {
+ when(smppSession.isOpened()).thenReturn(true);
+ when(smppSession.submit(any())).thenReturn(new SubmitSMResp());
+ setDefaultSmppConfig();
+
+ String number = "123545";
+ String message = "message";
+ smppSmsSender.sendSms(number, message);
+
+ verify(smppSmsSender, never()).initSmppSession();
+ verify(smppSession).submit(argThat(submitRequest -> {
+ try {
+ return submitRequest.getShortMessage().equals(message) &&
+ submitRequest.getDestAddr().getAddress().equals(number) &&
+ submitRequest.getServiceType().equals(smppConfig.getServiceType()) &&
+ (StringUtils.isEmpty(smppConfig.getSourceAddress()) ? submitRequest.getSourceAddr().getAddress().equals("")
+ : submitRequest.getSourceAddr().getAddress().equals(smppConfig.getSourceAddress()) &&
+ submitRequest.getSourceAddr().getTon() == smppConfig.getSourceTon() &&
+ submitRequest.getSourceAddr().getNpi() == smppConfig.getSourceNpi()) &&
+ submitRequest.getDestAddr().getTon() == smppConfig.getDestinationTon() &&
+ submitRequest.getDestAddr().getNpi() == smppConfig.getDestinationNpi() &&
+ submitRequest.getDataCoding() == smppConfig.getCodingScheme() &&
+ submitRequest.getReplaceIfPresentFlag() == 0 &&
+ submitRequest.getEsmClass() == 0 &&
+ submitRequest.getProtocolId() == 0 &&
+ submitRequest.getPriorityFlag() == 0 &&
+ submitRequest.getRegisteredDelivery() == 0 &&
+ submitRequest.getSmDefaultMsgId() == 0;
+ } catch (Exception e) {
+ fail(e.getMessage());
+ return false;
+ }
+ }));
+ }
+
+ private void setDefaultSmppConfig() {
+ smppConfig.setProtocolVersion("3.3");
+ smppConfig.setHost("smpphost");
+ smppConfig.setPort(5687);
+ smppConfig.setSystemId("213131");
+ smppConfig.setPassword("35125q");
+
+ smppConfig.setSystemType("");
+ smppConfig.setBindType(SmppSmsProviderConfiguration.SmppBindType.TX);
+ smppConfig.setServiceType("");
+
+ smppConfig.setSourceAddress("");
+ smppConfig.setSourceTon((byte) 5);
+ smppConfig.setSourceNpi((byte) 0);
+
+ smppConfig.setDestinationTon((byte) 5);
+ smppConfig.setDestinationNpi((byte) 0);
+
+ smppConfig.setAddressRange("");
+ smppConfig.setCodingScheme((byte) 0);
+ }
+
+}
diff --git a/application/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/application/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..ca6ee9cea8
--- /dev/null
+++ b/application/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
new file mode 100644
index 0000000000..1c94097500
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright © 2016-2022 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.common.data.sms.config;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class SmppSmsProviderConfiguration implements SmsProviderConfiguration {
+ @ApiModelProperty(value = "SMPP version", allowableValues = "3.3, 3.4", required = true)
+ private String protocolVersion;
+
+ @ApiModelProperty(value = "SMPP host", required = true)
+ private String host;
+ @ApiModelProperty(value = "SMPP port", required = true)
+ private Integer port;
+
+ @ApiModelProperty(value = "System ID", required = true)
+ private String systemId;
+ @ApiModelProperty(value = "Password", required = true)
+ private String password;
+
+ @ApiModelProperty(value = "System type", required = false)
+ private String systemType;
+ @ApiModelProperty(value = "TX - Transmitter, RX - Receiver, TRX - Transciever. By default TX is used", required = false)
+ private SmppBindType bindType;
+ @ApiModelProperty(value = "Service type", required = false)
+ private String serviceType;
+
+ @ApiModelProperty(value = "Source address", required = false)
+ private String sourceAddress;
+ @ApiModelProperty(value = "Source TON (Type of Number). Needed is source address is set. 5 by default.\n" +
+ "0 - Unknown\n" +
+ "1 - International\n" +
+ "2 - National\n" +
+ "3 - Network Specific\n" +
+ "4 - Subscriber Number\n" +
+ "5 - Alphanumeric\n" +
+ "6 - Abbreviated", required = false)
+ private Byte sourceTon;
+ @ApiModelProperty(value = "Source NPI (Numbering Plan Identification). Needed is source address is set. 0 by default.\n" +
+ "0 - Unknown\n" +
+ "1 - ISDN/telephone numbering plan (E163/E164)\n" +
+ "3 - Data numbering plan (X.121)\n" +
+ "4 - Telex numbering plan (F.69)\n" +
+ "6 - Land Mobile (E.212) =6\n" +
+ "8 - National numbering plan\n" +
+ "9 - Private numbering plan\n" +
+ "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" +
+ "13 - Internet (IP)\n" +
+ "18 - WAP Client Id (to be defined by WAP Forum)", required = false)
+ private Byte sourceNpi;
+
+ @ApiModelProperty(value = "Destination TON (Type of Number). 5 by default.\n" +
+ "0 - Unknown\n" +
+ "1 - International\n" +
+ "2 - National\n" +
+ "3 - Network Specific\n" +
+ "4 - Subscriber Number\n" +
+ "5 - Alphanumeric\n" +
+ "6 - Abbreviated", required = false)
+ private Byte destinationTon;
+ @ApiModelProperty(value = "Destination NPI (Numbering Plan Identification). 0 by default.\n" +
+ "0 - Unknown\n" +
+ "1 - ISDN/telephone numbering plan (E163/E164)\n" +
+ "3 - Data numbering plan (X.121)\n" +
+ "4 - Telex numbering plan (F.69)\n" +
+ "6 - Land Mobile (E.212) =6\n" +
+ "8 - National numbering plan\n" +
+ "9 - Private numbering plan\n" +
+ "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)\n" +
+ "13 - Internet (IP)\n" +
+ "18 - WAP Client Id (to be defined by WAP Forum)", required = false)
+ private Byte destinationNpi;
+
+ @ApiModelProperty(value = "Address range", required = false)
+ private String addressRange;
+
+ @ApiModelProperty(allowableValues = "0-10,13-14",
+ value = "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free, used as default)\n" +
+ "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))\n" +
+ "2 - Octet Unspecified (8-bit binary)\n" +
+ "3 - Latin 1 (ISO-8859-1)\n" +
+ "4 - Octet Unspecified (8-bit binary)\n" +
+ "5 - JIS (X 0208-1990)\n" +
+ "6 - Cyrillic (ISO-8859-5)\n" +
+ "7 - Latin/Hebrew (ISO-8859-8)\n" +
+ "8 - UCS2/UTF-16 (ISO/IEC-10646)\n" +
+ "9 - Pictogram Encoding\n" +
+ "10 - Music Codes (ISO-2022-JP)\n" +
+ "13 - Extended Kanji JIS (X 0212-1990)\n" +
+ "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)", required = false)
+ private Byte codingScheme;
+
+ @Override
+ public SmsProviderType getType() {
+ return SmsProviderType.SMPP;
+ }
+
+ public enum SmppBindType {
+ TX, RX, TRX
+ }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java
index 4feaf6e07c..cf5892dd8b 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java
@@ -27,7 +27,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = AwsSnsSmsProviderConfiguration.class, name = "AWS_SNS"),
- @JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO")})
+ @JsonSubTypes.Type(value = TwilioSmsProviderConfiguration.class, name = "TWILIO"),
+ @JsonSubTypes.Type(value = SmppSmsProviderConfiguration.class, name = "SMPP")
+})
public interface SmsProviderConfiguration {
@JsonIgnore
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderType.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderType.java
index 4bdb831862..9abdc1dd2e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderType.java
@@ -17,5 +17,6 @@ package org.thingsboard.server.common.data.sms.config;
public enum SmsProviderType {
AWS_SNS,
- TWILIO
+ TWILIO,
+ SMPP
}
diff --git a/pom.xml b/pom.xml
index 52bae83f64..02297203e6 100755
--- a/pom.xml
+++ b/pom.xml
@@ -134,6 +134,7 @@
1.16.0
1.12
+ 3.0.0
@@ -1875,6 +1876,11 @@
${zeroturnaround.version}
test
+
+ org.opensmpp
+ opensmpp-core
+ ${opensmpp.version}
+
diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index 0c58328e46..97cbbe2207 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -117,6 +117,7 @@ import { DefaultTenantProfileConfigurationComponent } from '@home/components/pro
import { TenantProfileConfigurationComponent } from '@home/components/profile/tenant/tenant-profile-configuration.component';
import { SmsProviderConfigurationComponent } from '@home/components/sms/sms-provider-configuration.component';
import { AwsSnsProviderConfigurationComponent } from '@home/components/sms/aws-sns-provider-configuration.component';
+import { SmppSmsProviderConfigurationComponent } from '@home/components/sms/smpp-sms-provider-configuration.component';
import { TwilioSmsProviderConfigurationComponent } from '@home/components/sms/twilio-sms-provider-configuration.component';
import { Lwm2mProfileComponentsModule } from '@home/components/profile/device/lwm2m/lwm2m-profile-components.module';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
@@ -253,6 +254,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
EditAlarmDetailsDialogComponent,
SmsProviderConfigurationComponent,
AwsSnsProviderConfigurationComponent,
+ SmppSmsProviderConfigurationComponent,
TwilioSmsProviderConfigurationComponent,
DashboardToolbarComponent,
DashboardPageComponent,
@@ -366,6 +368,7 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
AlarmScheduleComponent,
SmsProviderConfigurationComponent,
AwsSnsProviderConfigurationComponent,
+ SmppSmsProviderConfigurationComponent,
TwilioSmsProviderConfigurationComponent,
DashboardToolbarComponent,
DashboardPageComponent,
diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html
new file mode 100644
index 0000000000..1d16aefb19
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html
@@ -0,0 +1,162 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts
new file mode 100644
index 0000000000..605af2cb51
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts
@@ -0,0 +1,137 @@
+///
+/// Copyright © 2016-2022 The Thingsboard Authors
+///
+/// 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.
+///
+
+import { Component, forwardRef, Input, OnInit } from '@angular/core';
+import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
+import {
+ AwsSnsSmsProviderConfiguration,
+ BindTypes,
+ bindTypesTranslationMap,
+ CodingSchemes,
+ codingSchemesMap,
+ NumberingPlanIdentification,
+ numberingPlanIdentificationMap,
+ SmppSmsProviderConfiguration,
+ smppVersions,
+ SmsProviderConfiguration,
+ SmsProviderType,
+ TypeOfNumber,
+ typeOfNumberMap
+} from '@shared/models/settings.models';
+import { isDefinedAndNotNull } from '@core/utils';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+
+@Component({
+ selector: 'tb-smpp-sms-provider-configuration',
+ templateUrl: './smpp-sms-provider-configuration.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SmppSmsProviderConfigurationComponent),
+ multi: true
+ }]
+})
+
+export class SmppSmsProviderConfigurationComponent implements ControlValueAccessor, OnInit{
+ constructor(private fb: FormBuilder) {
+ }
+ private requiredValue: boolean;
+
+ get required(): boolean {
+ return this.requiredValue;
+ }
+
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+ @Input()
+ disabled: boolean;
+
+ smppSmsProviderConfigurationFormGroup: FormGroup;
+
+ smppVersions = smppVersions;
+
+ bindTypes = Object.keys(BindTypes);
+ bindTypesTranslation = bindTypesTranslationMap;
+
+ typeOfNumber = Object.keys(TypeOfNumber);
+ typeOfNumberMap = typeOfNumberMap;
+
+ numberingPlanIdentification = Object.keys(NumberingPlanIdentification);
+ numberingPlanIdentificationMap = numberingPlanIdentificationMap;
+
+ codingSchemes = Object.keys(CodingSchemes);
+ codingSchemesMap = codingSchemesMap;
+
+ private propagateChange = (v: any) => { };
+
+ ngOnInit(): void {
+ this.smppSmsProviderConfigurationFormGroup = this.fb.group({
+ protocolVersion: [null, [Validators.required]],
+ host: [null, [Validators.required]],
+ port: [null, [Validators.required]],
+ systemId: [null, [Validators.required]],
+ password: [null, [Validators.required]],
+ systemType: [null],
+ bindType: [null, []],
+ serviceType: [null, []],
+ sourceAddress: [null, []],
+ sourceTon: [null, []],
+ sourceNpi: [null, []],
+ destinationTon: [null, []],
+ destinationNpi: [null, []],
+ addressRange: [null, []],
+ codingScheme: [null, []],
+ });
+
+ this.smppSmsProviderConfigurationFormGroup.valueChanges.subscribe(() => {
+ this.updateValue();
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.smppSmsProviderConfigurationFormGroup.disable({emitEvent: false});
+ } else {
+ this.smppSmsProviderConfigurationFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: AwsSnsSmsProviderConfiguration | null): void {
+ if (isDefinedAndNotNull(value)) {
+ this.smppSmsProviderConfigurationFormGroup.patchValue(value, {emitEvent: false});
+ }
+ }
+
+ private updateValue() {
+ let configuration: SmppSmsProviderConfiguration = null;
+ if (this.smppSmsProviderConfigurationFormGroup.valid) {
+ configuration = this.smppSmsProviderConfigurationFormGroup.value;
+ (configuration as SmsProviderConfiguration).type = SmsProviderType.SMPP;
+ }
+ this.propagateChange(configuration);
+ }
+
+}
diff --git a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html
index a99c4ec029..f4d9d0d1c9 100644
--- a/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html
@@ -40,5 +40,11 @@
formControlName="configuration">
+
+
+
+
diff --git a/ui-ngx/src/app/shared/models/settings.models.ts b/ui-ngx/src/app/shared/models/settings.models.ts
index 440a0b12ef..615d0994c9 100644
--- a/ui-ngx/src/app/shared/models/settings.models.ts
+++ b/ui-ngx/src/app/shared/models/settings.models.ts
@@ -15,7 +15,7 @@
///
import { ValidatorFn } from '@angular/forms';
-import { isNotEmptyStr } from '@core/utils';
+import { isNotEmptyStr, isNumber } from '@core/utils';
export const smtpPortPattern: RegExp = /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
@@ -72,13 +72,15 @@ export const phoneNumberPatternTwilio = /^\+[1-9]\d{1,14}$|^(MG|PN).*$/;
export enum SmsProviderType {
AWS_SNS = 'AWS_SNS',
- TWILIO = 'TWILIO'
+ TWILIO = 'TWILIO',
+ SMPP = 'SMPP'
}
export const smsProviderTypeTranslationMap = new Map(
[
[SmsProviderType.AWS_SNS, 'admin.sms-provider-type-aws-sns'],
- [SmsProviderType.TWILIO, 'admin.sms-provider-type-twilio']
+ [SmsProviderType.TWILIO, 'admin.sms-provider-type-twilio'],
+ [SmsProviderType.SMPP, 'admin.sms-provider-type-smpp']
]
);
@@ -94,7 +96,216 @@ export interface TwilioSmsProviderConfiguration {
numberFrom?: string;
}
-export type SmsProviderConfigurations = AwsSnsSmsProviderConfiguration & TwilioSmsProviderConfiguration;
+export interface SmppSmsProviderConfiguration {
+ protocolVersion: number;
+ host: string;
+ port: number;
+ systemId: string;
+ password: string;
+ systemType?: string;
+ bindType?: string;
+ serviceType?: string;
+ sourceAddress?: string;
+ sourceTon?: number;
+ sourceNpi?: number;
+ destinationTon?: number;
+ destinationNpi?: number;
+ addressRange?: string;
+ codingScheme?: number;
+}
+
+export const smppVersions = [
+ {value: 3.3},
+ {value: 3.4}
+];
+
+export enum BindTypes {
+ TX = 'TX',
+ RX = 'RX',
+ TRX = 'TRX'
+}
+
+export const bindTypesTranslationMap = new Map([
+ [BindTypes.TX, 'admin.smpp-provider.bind-type-tx'],
+ [BindTypes.RX, 'admin.smpp-provider.bind-type-rx'],
+ [BindTypes.TRX, 'admin.smpp-provider.bind-type-trx']
+]);
+
+export enum TypeOfNumber {
+ Unknown = 'Unknown',
+ International = 'International',
+ National = 'National',
+ NetworkSpecific = 'NetworkSpecific',
+ SubscriberNumber = 'SubscriberNumber',
+ Alphanumeric = 'Alphanumeric',
+ Abbreviated = 'Abbreviated'
+}
+
+interface TypeDescriptor {
+ name: string;
+ value: number;
+}
+
+export const typeOfNumberMap = new Map([
+ [TypeOfNumber.Unknown, {
+ name: 'admin.smpp-provider.ton-unknown',
+ value: 0
+ }],
+ [TypeOfNumber.International, {
+ name: 'admin.smpp-provider.ton-international',
+ value: 1
+ }],
+ [TypeOfNumber.National, {
+ name: 'admin.smpp-provider.ton-national',
+ value: 2
+ }],
+ [TypeOfNumber.NetworkSpecific, {
+ name: 'admin.smpp-provider.ton-network-specific',
+ value: 3
+ }],
+ [TypeOfNumber.SubscriberNumber, {
+ name: 'admin.smpp-provider.ton-subscriber-number',
+ value: 4
+ }],
+ [TypeOfNumber.Alphanumeric, {
+ name: 'admin.smpp-provider.ton-alphanumeric',
+ value: 5
+ }],
+ [TypeOfNumber.Abbreviated, {
+ name: 'admin.smpp-provider.ton-abbreviated',
+ value: 6
+ }],
+]);
+
+export enum NumberingPlanIdentification {
+ Unknown = 'Unknown',
+ ISDN = 'ISDN',
+ DataNumberingPlan = 'DataNumberingPlan',
+ TelexNumberingPlan = 'TelexNumberingPlan',
+ LandMobile = 'LandMobile',
+ NationalNumberingPlan = 'NationalNumberingPlan',
+ PrivateNumberingPlan = 'PrivateNumberingPlan',
+ ERMESNumberingPlan = 'ERMESNumberingPlan',
+ Internet = 'Internet',
+ WAPClientId = 'WAPClientId',
+}
+
+export const numberingPlanIdentificationMap = new Map([
+ [NumberingPlanIdentification.Unknown, {
+ name: 'admin.smpp-provider.npi-unknown',
+ value: 0
+ }],
+ [NumberingPlanIdentification.ISDN, {
+ name: 'admin.smpp-provider.npi-isdn',
+ value: 1
+ }],
+ [NumberingPlanIdentification.DataNumberingPlan, {
+ name: 'admin.smpp-provider.npi-data-numbering-plan',
+ value: 3
+ }],
+ [NumberingPlanIdentification.TelexNumberingPlan, {
+ name: 'admin.smpp-provider.npi-telex-numbering-plan',
+ value: 4
+ }],
+ [NumberingPlanIdentification.LandMobile, {
+ name: 'admin.smpp-provider.npi-land-mobile',
+ value: 5
+ }],
+ [NumberingPlanIdentification.NationalNumberingPlan, {
+ name: 'admin.smpp-provider.npi-national-numbering-plan',
+ value: 8
+ }],
+ [NumberingPlanIdentification.PrivateNumberingPlan, {
+ name: 'admin.smpp-provider.npi-private-numbering-plan',
+ value: 9
+ }],
+ [NumberingPlanIdentification.ERMESNumberingPlan, {
+ name: 'admin.smpp-provider.npi-ermes-numbering-plan',
+ value: 10
+ }],
+ [NumberingPlanIdentification.Internet, {
+ name: 'admin.smpp-provider.npi-internet',
+ value: 13
+ }],
+ [NumberingPlanIdentification.WAPClientId, {
+ name: 'admin.smpp-provider.npi-wap-client-id',
+ value: 18
+ }],
+]);
+
+export enum CodingSchemes {
+ SMSC = 'SMSC',
+ IA5 = 'IA5',
+ OctetUnspecified2 = 'OctetUnspecified2',
+ Latin1 = 'Latin1',
+ OctetUnspecified4 = 'OctetUnspecified4',
+ JIS = 'JIS',
+ Cyrillic = 'Cyrillic',
+ LatinHebrew = 'LatinHebrew',
+ UCS2UTF16 = 'UCS2UTF16',
+ PictogramEncoding = 'PictogramEncoding',
+ MusicCodes = 'MusicCodes',
+ ExtendedKanjiJIS = 'ExtendedKanjiJIS',
+ KoreanGraphicCharacterSet = 'KoreanGraphicCharacterSet',
+}
+
+export const codingSchemesMap = new Map([
+ [CodingSchemes.SMSC, {
+ name: 'admin.smpp-provider.scheme-smsc',
+ value: 0
+ }],
+ [CodingSchemes.IA5, {
+ name: 'admin.smpp-provider.scheme-ia5',
+ value: 1
+ }],
+ [CodingSchemes.OctetUnspecified2, {
+ name: 'admin.smpp-provider.scheme-octet-unspecified-2',
+ value: 2
+ }],
+ [CodingSchemes.Latin1, {
+ name: 'admin.smpp-provider.scheme-latin-1',
+ value: 3
+ }],
+ [CodingSchemes.OctetUnspecified4, {
+ name: 'admin.smpp-provider.scheme-octet-unspecified-4',
+ value: 4
+ }],
+ [CodingSchemes.JIS, {
+ name: 'admin.smpp-provider.scheme-jis',
+ value: 5
+ }],
+ [CodingSchemes.Cyrillic, {
+ name: 'admin.smpp-provider.scheme-cyrillic',
+ value: 6
+ }],
+ [CodingSchemes.LatinHebrew, {
+ name: 'admin.smpp-provider.scheme-latin-hebrew',
+ value: 7
+ }],
+ [CodingSchemes.UCS2UTF16, {
+ name: 'admin.smpp-provider.scheme-ucs-utf',
+ value: 8
+ }],
+ [CodingSchemes.PictogramEncoding, {
+ name: 'admin.smpp-provider.scheme-pictogram-encoding',
+ value: 9
+ }],
+ [CodingSchemes.MusicCodes, {
+ name: 'admin.smpp-provider.scheme-music-codes',
+ value: 10
+ }],
+ [CodingSchemes.ExtendedKanjiJIS, {
+ name: 'admin.smpp-provider.scheme-extended-kanji-jis',
+ value: 13
+ }],
+ [CodingSchemes.KoreanGraphicCharacterSet, {
+ name: 'admin.smpp-provider.scheme-korean-graphic-character-set',
+ value: 14
+ }],
+]);
+
+export type SmsProviderConfigurations =
+ Partial & AwsSnsSmsProviderConfiguration & TwilioSmsProviderConfiguration;
export interface SmsProviderConfiguration extends SmsProviderConfigurations {
type: SmsProviderType;
@@ -118,6 +329,11 @@ export function smsProviderConfigurationValidator(required: boolean): ValidatorF
valid = isNotEmptyStr(twilioConfiguration.numberFrom) && isNotEmptyStr(twilioConfiguration.accountSid)
&& isNotEmptyStr(twilioConfiguration.accountToken);
break;
+ case SmsProviderType.SMPP:
+ const smppConfiguration = configuration as SmppSmsProviderConfiguration;
+ valid = isNotEmptyStr(smppConfiguration.host) && isNumber(smppConfiguration.port)
+ && isNotEmptyStr(smppConfiguration.systemId) && isNotEmptyStr(smppConfiguration.password);
+ break;
}
}
if (!valid) {
@@ -156,6 +372,26 @@ export function createSmsProviderConfiguration(type: SmsProviderType): SmsProvid
};
smsProviderConfiguration = {...twilioSmsProviderConfiguration, type: SmsProviderType.TWILIO};
break;
+ case SmsProviderType.SMPP:
+ const smppSmsProviderConfiguration: SmppSmsProviderConfiguration = {
+ protocolVersion: 3.3,
+ host: '',
+ port: null,
+ systemId: '',
+ password: '',
+ systemType: '',
+ bindType: 'TX',
+ serviceType: '',
+ sourceAddress: '',
+ sourceTon: 5,
+ sourceNpi: 0,
+ destinationTon: 5,
+ destinationNpi: 0,
+ addressRange: '',
+ codingScheme: 0
+ };
+ smsProviderConfiguration = {...smppSmsProviderConfiguration, type: SmsProviderType.SMPP};
+ break;
}
}
return smsProviderConfiguration;
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 2e55b7cb0d..6a1e568821 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -112,6 +112,7 @@
"sms-provider-type-required": "SMS provider type is required.",
"sms-provider-type-aws-sns": "Amazon SNS",
"sms-provider-type-twilio": "Twilio",
+ "sms-provider-type-smpp": "SMPP",
"aws-access-key-id": "AWS Access Key ID",
"aws-access-key-id-required": "AWS Access Key ID is required",
"aws-secret-access-key": "AWS Secret Access Key",
@@ -252,6 +253,64 @@
"platform-ios": "iOS",
"all-platforms": "All platforms",
"allowed-platforms": "Allowed platforms"
+ },
+ "smpp-provider": {
+ "smpp-version": "SMPP version",
+ "smpp-host": "SMPP host",
+ "smpp-host-required": "SMPP host is required",
+ "smpp-port": "SMPP port",
+ "smpp-port-required": "SMPP port is required",
+ "system-id": "System ID",
+ "system-id-required": "System ID is required",
+ "password": "Password",
+ "password-required": "Password is required",
+ "type-settings": "Type settings",
+ "source-settings": "Source settings",
+ "destination-settings": "Destination settings",
+ "additional-settings": "Additional settings",
+ "system-type": "System type",
+ "bind-type": "Bind type",
+ "service-type": "Service type",
+ "source-address": "Source address",
+ "source-ton": "Source TON",
+ "source-npi": "Source NPI",
+ "destination-ton": "Destination TON (Type of Number)",
+ "destination-npi": "Destination NPI (Numbering Plan Identification)",
+ "address-range": "Address range",
+ "coding-scheme": "Coding scheme",
+ "bind-type-tx": "Transmitter",
+ "bind-type-rx": "Receiver",
+ "bind-type-trx": "Transciever",
+ "ton-unknown": "Unknown",
+ "ton-international": "International",
+ "ton-national": "National",
+ "ton-network-specific": "Network Specific",
+ "ton-subscriber-number": "Subscriber Number",
+ "ton-alphanumeric": "Alphanumeric",
+ "ton-abbreviated": "Abbreviated",
+ "npi-unknown": "0 - Unknown",
+ "npi-isdn": "1 - ISDN/telephone numbering plan (E163/E164)",
+ "npi-data-numbering-plan": "3 - Data numbering plan (X.121)",
+ "npi-telex-numbering-plan": "4 - Telex numbering plan (F.69)",
+ "npi-land-mobile": "6 - Land Mobile (E.212)",
+ "npi-national-numbering-plan": "8 - National numbering plan",
+ "npi-private-numbering-plan": "9 - Private numbering plan",
+ "npi-ermes-numbering-plan": "10 - ERMES numbering plan (ETSI DE/PS 3 01-3)",
+ "npi-internet": "13 - Internet (IP)",
+ "npi-wap-client-id": "18 - WAP Client Id (to be defined by WAP Forum)",
+ "scheme-smsc": "0 - SMSC Default Alphabet (ASCII for short and long code and to GSM for toll-free)",
+ "scheme-ia5": "1 - IA5 (ASCII for short and long code, Latin 9 for toll-free (ISO-8859-9))",
+ "scheme-octet-unspecified-2": "2 - Octet Unspecified (8-bit binary)",
+ "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)",
+ "scheme-octet-unspecified-4": "4 - Octet Unspecified (8-bit binary)",
+ "scheme-jis": "5 - JIS (X 0208-1990)",
+ "scheme-cyrillic": "6 - Cyrillic (ISO-8859-5)",
+ "scheme-latin-hebrew": "7 - Latin/Hebrew (ISO-8859-8)",
+ "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)",
+ "scheme-pictogram-encoding": "9 - Pictogram Encoding",
+ "scheme-music-codes": "10 - Music Codes (ISO-2022-JP)",
+ "scheme-extended-kanji-jis": "13 - Extended Kanji JIS (X 0212-1990)",
+ "scheme-korean-graphic-character-set": "14 - Korean Graphic Character Set (KS C 5601/KS X 1001)"
}
},
"alarm": {
diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json
index 51d08e8939..2a6127f672 100644
--- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json
+++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json
@@ -107,8 +107,33 @@
"general-policy": "Общая политика",
"max-failed-login-attempts": "Максимальное количество неудачных попыток входа в систему, прежде чем учетная запись заблокирована",
"minimum-max-failed-login-attempts-range": "Максимальное количество неудачных попыток входа в систему не может быть отрицательным",
- "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту"
- },
+ "user-lockout-notification-email": "В случае блокировки учетной записи пользователя отправьте уведомление на электронную почту",
+ "smpp-provider": {
+ "smpp-version": "SMPP версия",
+ "smpp-host": "SMPP хост",
+ "smpp-host-required": "SMPP хост обязателен.",
+ "smpp-port": "SMPP порт",
+ "smpp-port-required": "SMPP порт обязателен.",
+ "system-id": "ИД системи",
+ "system-id-required": "ИД системи обязателен.",
+ "password": "Пароль",
+ "password-required": "Пароль обязателен.",
+ "type-settings": "Настройки типов",
+ "source-settings": "Настройки источника",
+ "destination-settings": "Настройки назначения",
+ "additional-settings": "Дополнительные настройки",
+ "system-type": "Тип системы",
+ "bind-type": "Тип привязки",
+ "service-type": "Тип обслуживания",
+ "source-address": "Адрес источника",
+ "source-ton": "Тип номера источника",
+ "source-npi": "Идентификация плана нумерации источника",
+ "destination-ton": "Тип номера назничения",
+ "destination-npi": "Идентификация плана нумерации назначения",
+ "address-range": "Диапазон адресов",
+ "coding-scheme": "Схема кодирования"
+ }
+ },
"alarm": {
"alarm": "Оповещение",
"alarms": "Оповещения",
diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
index 003c7881ca..91039b302b 100644
--- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
+++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json
@@ -123,7 +123,32 @@
"general-policy": "Загальна політика",
"max-failed-login-attempts": "Максимальна кількість невдалих спроб входу, перш ніж обліковий запис заблоковано",
"minimum-max-failed-login-attempts-range": "Максимальна кількість невдалих спроб входу не може бути негативною",
- "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту"
+ "user-lockout-notification-email": "У разі блокування облікового запису користувача, надішліть сповіщення на електронну пошту",
+ "smpp-provider": {
+ "smpp-version": "SMPP верія",
+ "smpp-host": "SMPP хост",
+ "smpp-host-required": "Хост SMPP обов'язковий.",
+ "smpp-port": "SMPP порт",
+ "smpp-port-required": "Порт SMPP обов'язковий.",
+ "system-id": "Id системи",
+ "system-id-required": "Id системи обязателен.",
+ "password": "Пароль",
+ "password-required": "Пароль обязателен.",
+ "type-settings": "Налаштування типів",
+ "source-settings": "Налаштування джерела",
+ "destination-settings": "Налаштування призначення",
+ "additional-settings": "Додаткові налаштування",
+ "system-type": "Тип системи",
+ "bind-type": "Тип зв'язування",
+ "service-type": "Тип обслуговування",
+ "source-address": "Адреса джерела",
+ "source-ton": "Тип номера джерела",
+ "source-npi": "Идентификация плана нумерации джерела",
+ "destination-ton": "Тип номера призначення",
+ "destination-npi": "Ідентифікація плану нумерації призначення",
+ "address-range": "Діапазон адрес",
+ "coding-scheme": "Схема кодування"
+ }
},
"alarm": {
"alarm": "Сигнал тривоги",