Browse Source

Merge pull request #5671 from ViacheslavKlimov/smpp-sms-sender

[3.4] SMPP SMS provider
pull/6519/head
Andrew Shvayka 4 years ago
committed by GitHub
parent
commit
b9b813385f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      application/pom.xml
  2. 4
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsSenderFactory.java
  3. 186
      application/src/main/java/org/thingsboard/server/service/sms/smpp/SmppSmsSender.java
  4. 121
      application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java
  5. 1
      application/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
  6. 117
      common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmppSmsProviderConfiguration.java
  7. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderConfiguration.java
  8. 3
      common/data/src/main/java/org/thingsboard/server/common/data/sms/config/SmsProviderType.java
  9. 6
      pom.xml
  10. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  11. 162
      ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html
  12. 137
      ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.ts
  13. 6
      ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html
  14. 244
      ui-ngx/src/app/shared/models/settings.models.ts
  15. 59
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  16. 29
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  17. 27
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json

4
application/pom.xml

@ -249,6 +249,10 @@
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
</dependency>
<dependency>
<groupId>org.opensmpp</groupId>
<artifactId>opensmpp-core</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>springfox-boot-starter</artifactId>

4
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());
}

186
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);
}
}
}

121
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<SmppSmsSender> 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);
}
}

1
application/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

@ -0,0 +1 @@
mock-maker-inline

117
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
}
}

4
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

3
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
}

6
pom.xml

@ -134,6 +134,7 @@
<!-- BLACKBOX TEST SCOPE -->
<testcontainers.version>1.16.0</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version>
<opensmpp.version>3.0.0</opensmpp.version>
</properties>
<modules>
@ -1875,6 +1876,11 @@
<version>${zeroturnaround.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opensmpp</groupId>
<artifactId>opensmpp-core</artifactId>
<version>${opensmpp.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

3
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,

162
ui-ngx/src/app/modules/home/components/sms/smpp-sms-provider-configuration.component.html

@ -0,0 +1,162 @@
<!--
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.
-->
<form [formGroup]="smppSmsProviderConfigurationFormGroup" style="padding-bottom: 16px;">
<div fxLayout="row wrap" fxLayoutGap="6px">
<mat-form-field fxFlex.lt-sm="100" fxFlex.gt-sm="15" class="mat-block">
<mat-label translate>admin.smpp-provider.smpp-version</mat-label>
<mat-select formControlName="protocolVersion">
<mat-option *ngFor="let smppVersion of smppVersions" [value]="smppVersion.value">
{{smppVersion.value}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.smpp-provider.smpp-host</mat-label>
<input required matInput formControlName="host">
<mat-error *ngIf="smppSmsProviderConfigurationFormGroup.get('host').hasError('required')">
{{'admin.smpp-provider.smpp-host-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex.lt-sm="100" fxFlex.gt-sm="30" class="mat-block">
<mat-label translate>admin.smpp-provider.smpp-port</mat-label>
<input required matInput type="number" formControlName="port">
<mat-error *ngIf="smppSmsProviderConfigurationFormGroup.get('port').hasError('required')">
{{'admin.smpp-provider.smpp-port-required' | translate}}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row wrap" fxLayoutGap="6px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.smpp-provider.system-id</mat-label>
<input required matInput formControlName="systemId">
<mat-error *ngIf="smppSmsProviderConfigurationFormGroup.get('systemId').hasError('required')">
{{'admin.smpp-provider.system-id-required' | translate}}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.smpp-provider.password</mat-label>
<input required matInput type="password" autocomplete="new-password" formControlName="password">
<tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="smppSmsProviderConfigurationFormGroup.get('password').hasError('required')">
{{'admin.smpp-provider.password-required' | translate}}
</mat-error>
</mat-form-field>
</div>
<mat-accordion multi>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
admin.smpp-provider.type-settings
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.system-type</mat-label>
<input matInput formControlName="systemType">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.bind-type</mat-label>
<mat-select formControlName="bindType">
<mat-option *ngFor="let bindType of bindTypes" [value]="bindType">
{{bindTypesTranslation.get(bindType) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.service-type</mat-label>
<input matInput formControlName="serviceType">
</mat-form-field>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
admin.smpp-provider.source-settings
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.source-address</mat-label>
<input matInput formControlName="sourceAddress">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.source-ton</mat-label>
<mat-select formControlName="sourceTon">
<mat-option *ngFor="let sourceTon of typeOfNumber" [value]="typeOfNumberMap.get(sourceTon).value">
{{typeOfNumberMap.get(sourceTon).name | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.source-npi</mat-label>
<mat-select formControlName="sourceNpi">
<mat-option *ngFor="let sourceNpi of numberingPlanIdentification" [value]="numberingPlanIdentificationMap.get(sourceNpi).value">
{{numberingPlanIdentificationMap.get(sourceNpi).name | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
admin.smpp-provider.destination-settings
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.destination-ton</mat-label>
<mat-select formControlName="destinationTon">
<mat-option *ngFor="let destinationTon of typeOfNumber" [value]="typeOfNumberMap.get(destinationTon).value">
{{typeOfNumberMap.get(destinationTon).name | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.destination-npi</mat-label>
<mat-select formControlName="destinationNpi">
<mat-option *ngFor="let destinationNpi of numberingPlanIdentification" [value]="numberingPlanIdentificationMap.get(destinationNpi).value">
{{numberingPlanIdentificationMap.get(destinationNpi).name | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title translate>
admin.smpp-provider.additional-settings
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.address-range</mat-label>
<input matInput formControlName="addressRange">
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>admin.smpp-provider.coding-scheme</mat-label>
<mat-select formControlName="codingScheme">
<mat-option *ngFor="let codingScheme of codingSchemes" [value]="codingSchemesMap.get(codingScheme).value">
{{codingSchemesMap.get(codingScheme).name | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</form>

137
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);
}
}

6
ui-ngx/src/app/modules/home/components/sms/sms-provider-configuration.component.html

@ -40,5 +40,11 @@
formControlName="configuration">
</tb-twilio-sms-provider-configuration>
</ng-template>
<ng-template [ngSwitchCase]="smsProviderType.SMPP">
<tb-smpp-sms-provider-configuration
[required]="required"
formControlName="configuration">
</tb-smpp-sms-provider-configuration>
</ng-template>
</div>
</div>

244
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, string>(
[
[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, string>([
[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, TypeDescriptor>([
[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, TypeDescriptor>([
[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, TypeDescriptor>([
[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<SmppSmsProviderConfiguration> & 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;

59
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": {

29
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": "Оповещения",

27
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": "Сигнал тривоги",

Loading…
Cancel
Save