51 changed files with 1969 additions and 815 deletions
@ -0,0 +1,27 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m.client; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@Data |
|||
public class LwM2mSoftwareUpdate { |
|||
private volatile String clientSwVersion; |
|||
private volatile String currentSwVersion; |
|||
private volatile UUID currentSwId; |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m.config; |
|||
|
|||
public interface LwM2MSecureServerConfig { |
|||
|
|||
Integer getId(); |
|||
|
|||
String getHost(); |
|||
|
|||
Integer getPort(); |
|||
|
|||
String getSecureHost(); |
|||
|
|||
Integer getSecurePort(); |
|||
|
|||
String getPublicX(); |
|||
|
|||
String getPublicY(); |
|||
|
|||
String getPrivateEncoded(); |
|||
|
|||
String getCertificateAlias(); |
|||
|
|||
} |
|||
@ -0,0 +1,162 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m.config; |
|||
|
|||
import com.google.common.io.Resources; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.leshan.server.model.LwM2mModelProvider; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import java.io.File; |
|||
import java.io.FileInputStream; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.net.URI; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
import java.security.KeyStore; |
|||
import java.security.KeyStoreException; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.security.cert.CertificateException; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") |
|||
public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { |
|||
|
|||
@Getter |
|||
@Setter |
|||
private LwM2mModelProvider modelProvider; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.timeout:}") |
|||
private Long timeout; |
|||
|
|||
@Getter |
|||
@Value("${transport.sessions.report_timeout}") |
|||
private long sessionReportTimeout; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.recommended_ciphers:}") |
|||
private boolean recommendedCiphers; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.recommended_supported_groups:}") |
|||
private boolean recommendedSupportedGroups; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.response_pool_size:}") |
|||
private int responsePoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.registered_pool_size:}") |
|||
private int registeredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.update_registered_pool_size:}") |
|||
private int updateRegisteredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.un_registered_pool_size:}") |
|||
private int unRegisteredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.security.key_store_type:}") |
|||
private String keyStoreType; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.security.key_store:}") |
|||
private String keyStorePathFile; |
|||
|
|||
@Getter |
|||
@Setter |
|||
private KeyStore keyStoreValue; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.security.key_store_password:}") |
|||
private String keyStorePassword; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.security.root_alias:}") |
|||
private String rootCertificateAlias; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.security.enable_gen_new_key_psk_rpk:}") |
|||
private Boolean enableGenNewKeyPskRpk; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.id:}") |
|||
private Integer id; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.bind_address:}") |
|||
private String host; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.bind_port:}") |
|||
private Integer port; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.bind_address:}") |
|||
private String secureHost; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.bind_port:}") |
|||
private Integer securePort; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.public_x:}") |
|||
private String publicX; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.public_y:}") |
|||
private String publicY; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.private_encoded:}") |
|||
private String privateEncoded; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.security.alias:}") |
|||
private String certificateAlias; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.log_max_length:}") |
|||
private int logMaxLength; |
|||
|
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
URI uri = null; |
|||
try { |
|||
uri = Resources.getResource(keyStorePathFile).toURI(); |
|||
log.error("URI: {}", uri); |
|||
File keyStoreFile = new File(uri); |
|||
InputStream inKeyStore = new FileInputStream(keyStoreFile); |
|||
keyStoreValue = KeyStore.getInstance(keyStoreType); |
|||
keyStoreValue.load(inKeyStore, keyStorePassword == null ? null : keyStorePassword.toCharArray()); |
|||
} catch (Exception e) { |
|||
log.warn("Unable to lookup LwM2M keystore. Reason: {}, {}" , uri, e.getMessage()); |
|||
// Absence of the key store should not block user from using plain LwM2M
|
|||
// throw new RuntimeException("Failed to lookup LwM2M keystore: " + (uri != null ? uri.toString() : ""), e);
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m.server; |
|||
|
|||
import org.thingsboard.server.common.data.TbTransportService; |
|||
|
|||
public interface LwM2MTransportService extends TbTransportService { |
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m.server; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import org.eclipse.leshan.server.californium.LeshanServer; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.transport.TransportContext; |
|||
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; |
|||
|
|||
@Component |
|||
@TbLwM2mTransportComponent |
|||
public class LwM2mTransportContext extends TransportContext { |
|||
|
|||
@Getter @Setter |
|||
private LeshanServer server; |
|||
|
|||
} |
|||
@ -1,217 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2021 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.transport.lwm2m; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.leshan.server.model.LwM2mModelProvider; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import java.io.File; |
|||
import java.io.FileInputStream; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
import java.security.KeyStore; |
|||
import java.security.KeyStoreException; |
|||
import java.security.NoSuchAlgorithmException; |
|||
import java.security.cert.CertificateException; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") |
|||
public class LwM2MTransportConfigServer { |
|||
|
|||
@Getter |
|||
private String KEY_STORE_DEFAULT_RESOURCE_PATH = "credentials"; |
|||
|
|||
@Getter |
|||
private String KEY_STORE_DEFAULT_FILE = "serverKeyStore.jks"; |
|||
|
|||
@Getter |
|||
private String APP_DIR = "common"; |
|||
|
|||
@Getter |
|||
private String TRANSPORT_DIR = "transport"; |
|||
|
|||
@Getter |
|||
private String LWM2M_DIR = "lwm2m"; |
|||
|
|||
@Getter |
|||
private String SRC_DIR = "src"; |
|||
|
|||
@Getter |
|||
private String MAIN_DIR = "main"; |
|||
|
|||
@Getter |
|||
private String RESOURCES_DIR = "resources"; |
|||
|
|||
@Getter |
|||
private String BASE_DIR_PATH = System.getProperty("user.dir"); |
|||
|
|||
@Getter |
|||
// private String PATH_DATA_MICROSERVICE = "/usr/share/tb-lwm2m-transport/data$";
|
|||
private String PATH_DATA = "data"; |
|||
|
|||
@Getter |
|||
@Setter |
|||
private LwM2mModelProvider modelProvider; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.timeout:}") |
|||
private Long timeout; |
|||
|
|||
@Getter |
|||
@Value("${transport.sessions.report_timeout}") |
|||
private long sessionReportTimeout; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.recommended_ciphers:}") |
|||
private boolean recommendedCiphers; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.recommended_supported_groups:}") |
|||
private boolean recommendedSupportedGroups; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.response_pool_size:}") |
|||
private int responsePoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.registered_pool_size:}") |
|||
private int registeredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.update_registered_pool_size:}") |
|||
private int updateRegisteredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.un_registered_pool_size:}") |
|||
private int unRegisteredPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.secure.key_store_type:}") |
|||
private String keyStoreType; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.secure.key_store_path_file:}") |
|||
private String keyStorePathFile; |
|||
|
|||
@Getter |
|||
@Setter |
|||
private KeyStore keyStoreValue; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.secure.key_store_password:}") |
|||
private String keyStorePasswordServer; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.secure.root_alias:}") |
|||
private String rootAlias; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.secure.enable_gen_new_key_psk_rpk:}") |
|||
private Boolean enableGenNewKeyPskRpk; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.id:}") |
|||
private Integer serverId; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.bind_address:}") |
|||
private String serverHost; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.bind_address_security:}") |
|||
private String serverHostSecurity; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.bind_port_no_sec:}") |
|||
private Integer serverPortNoSec; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.bind_port_security:}") |
|||
private Integer serverPortSecurity; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.public_x:}") |
|||
private String serverPublicX; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.public_y:}") |
|||
private String serverPublicY; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.private_encoded:}") |
|||
private String serverPrivateEncoded; |
|||
|
|||
@Getter |
|||
@Value("${transport.lwm2m.server.secure.alias:}") |
|||
private String serverAlias; |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
this.getInKeyStore(); |
|||
} |
|||
|
|||
private KeyStore getInKeyStore() { |
|||
try { |
|||
if (keyStoreValue != null && keyStoreValue.size() > 0) |
|||
return keyStoreValue; |
|||
} catch (KeyStoreException e) { |
|||
log.error("Uninitialized keystore [{}]", keyStoreValue.toString()); |
|||
} |
|||
Path keyStorePath = (keyStorePathFile != null && !keyStorePathFile.isEmpty()) ? Paths.get(keyStorePathFile) : |
|||
(new File(Paths.get(getBaseDirPath(), PATH_DATA, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE).toUri()).isFile()) ? |
|||
Paths.get(getBaseDirPath(), PATH_DATA, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE) : |
|||
Paths.get(getBaseDirPath(), APP_DIR, TRANSPORT_DIR, LWM2M_DIR, SRC_DIR, MAIN_DIR, RESOURCES_DIR, KEY_STORE_DEFAULT_RESOURCE_PATH, KEY_STORE_DEFAULT_FILE); |
|||
File keyStoreFile = new File(keyStorePath.toUri()); |
|||
if (keyStoreFile.isFile()) { |
|||
try { |
|||
InputStream inKeyStore = new FileInputStream(keyStoreFile); |
|||
keyStoreValue = KeyStore.getInstance(keyStoreType); |
|||
keyStoreValue.load(inKeyStore, keyStorePasswordServer == null ? null : keyStorePasswordServer.toCharArray()); |
|||
} catch (CertificateException | NoSuchAlgorithmException | IOException | KeyStoreException e) { |
|||
log.error("[{}] Unable to load KeyStore files server, folder is not a directory", e.getMessage()); |
|||
keyStoreValue = null; |
|||
} |
|||
log.info("[{}] Load KeyStore files server, folder is a directory", keyStoreFile.getAbsoluteFile()); |
|||
} else { |
|||
log.error("[{}] Unable to load KeyStore files server, is not a file", keyStoreFile.getAbsoluteFile()); |
|||
keyStoreValue = null; |
|||
} |
|||
return keyStoreValue; |
|||
} |
|||
|
|||
private String getBaseDirPath() { |
|||
Path FULL_FILE_PATH; |
|||
if (BASE_DIR_PATH.endsWith("bin")) { |
|||
FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("bin$", "")); |
|||
} else if (BASE_DIR_PATH.endsWith("conf")) { |
|||
FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.replaceAll("conf$", "")); |
|||
} else if (BASE_DIR_PATH.endsWith("application")) { |
|||
FULL_FILE_PATH = Paths.get(BASE_DIR_PATH.substring(0, BASE_DIR_PATH.length() - "application".length())); |
|||
} else { |
|||
FULL_FILE_PATH = Paths.get(BASE_DIR_PATH); |
|||
} |
|||
return FULL_FILE_PATH.toUri().getPath(); |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 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 { Inject, Injectable } from '@angular/core'; |
|||
import { WINDOW } from '@core/services/window.service'; |
|||
import { isDefined } from '@core/utils'; |
|||
import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models'; |
|||
import { from, of } from 'rxjs'; |
|||
import { Observable } from 'rxjs/internal/Observable'; |
|||
import { catchError } from 'rxjs/operators'; |
|||
|
|||
const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler'; |
|||
const mobileHandler = 'tbMobileHandler'; |
|||
|
|||
// @dynamic
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class MobileService { |
|||
|
|||
private readonly mobileApp; |
|||
private readonly mobileChannel; |
|||
|
|||
constructor(@Inject(WINDOW) private window: Window) { |
|||
const w = (this.window as any); |
|||
this.mobileChannel = w.flutter_inappwebview; |
|||
this.mobileApp = isDefined(this.mobileChannel); |
|||
} |
|||
|
|||
public isMobileApp(): boolean { |
|||
return this.mobileApp; |
|||
} |
|||
|
|||
public handleDashboardStateName(name: string) { |
|||
if (this.mobileApp) { |
|||
this.mobileChannel.callHandler(dashboardStateNameHandler, name); |
|||
} |
|||
} |
|||
|
|||
public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]): |
|||
Observable<WidgetMobileActionResult<T>> { |
|||
if (this.mobileApp) { |
|||
return from( |
|||
this.mobileChannel.callHandler(mobileHandler, type, ...args) as Promise<WidgetMobileActionResult<T>>).pipe( |
|||
catchError((err: Error) => { |
|||
return of({ |
|||
hasError: true, |
|||
error: err?.message ? err.message : `Failed to execute mobile action ${type}` |
|||
} as WidgetMobileActionResult<any>); |
|||
}) |
|||
); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2021 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. |
|||
|
|||
--> |
|||
<div [formGroup]="mobileActionFormGroup"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>widget-action.mobile.action-type</mat-label> |
|||
<mat-select required formControlName="type"> |
|||
<mat-option *ngFor="let actionType of mobileActionTypes" [value]="actionType"> |
|||
{{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} |
|||
</mat-option> |
|||
</mat-select> |
|||
<mat-error *ngIf="mobileActionFormGroup.get('type').hasError('required')"> |
|||
{{ 'widget-action.mobile.action-type-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<div [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value"> |
|||
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection || |
|||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ? |
|||
mobileActionFormGroup.get('type').value : ''"> |
|||
<tb-js-func |
|||
formControlName="getLocationFunction" |
|||
functionName="getLocation" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall"> |
|||
<tb-js-func |
|||
formControlName="getPhoneNumberFunction" |
|||
functionName="getPhoneNumber" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || |
|||
mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || |
|||
mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot ? |
|||
mobileActionFormGroup.get('type').value : ''"> |
|||
<tb-js-func |
|||
formControlName="processImageFunction" |
|||
functionName="processImage" |
|||
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="mobileActionType.scanQrCode"> |
|||
<tb-js-func |
|||
formControlName="processQrCodeFunction" |
|||
functionName="processQrCode" |
|||
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="mobileActionType.getLocation"> |
|||
<tb-js-func |
|||
formControlName="processLocationFunction" |
|||
functionName="processLocation" |
|||
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection || |
|||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation || |
|||
mobileActionFormGroup.get('type').value === mobileActionType.makePhoneCall ? |
|||
mobileActionFormGroup.get('type').value : ''"> |
|||
<tb-js-func |
|||
formControlName="processLaunchResultFunction" |
|||
functionName="processLaunchResult" |
|||
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</ng-template> |
|||
</div> |
|||
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value" |
|||
formControlName="handleEmptyResultFunction" |
|||
functionName="handleEmptyResult" |
|||
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value" |
|||
formControlName="handleErrorFunction" |
|||
functionName="handleError" |
|||
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" |
|||
[editorCompleter]="customActionEditorCompleter" |
|||
></tb-js-func> |
|||
</div> |
|||
@ -0,0 +1,253 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 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, QueryList, ViewChild, ViewChildren } from '@angular/core'; |
|||
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { |
|||
WidgetMobileActionDescriptor, |
|||
WidgetMobileActionType, widgetMobileActionTypeTranslationMap |
|||
} from '@shared/models/widget.models'; |
|||
import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; |
|||
import { JsFuncComponent } from '@shared/components/js-func.component'; |
|||
import { |
|||
getDefaultGetLocationFunction, getDefaultGetPhoneNumberFunction, |
|||
getDefaultHandleEmptyResultFunction, |
|||
getDefaultHandleErrorFunction, |
|||
getDefaultProcessImageFunction, |
|||
getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, |
|||
getDefaultProcessQrCodeFunction |
|||
} from '@home/components/widget/action/mobile-action-editor.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-mobile-action-editor', |
|||
templateUrl: './mobile-action-editor.component.html', |
|||
styleUrls: [], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => MobileActionEditorComponent), |
|||
multi: true |
|||
}] |
|||
}) |
|||
export class MobileActionEditorComponent implements ControlValueAccessor, OnInit { |
|||
|
|||
@ViewChildren(JsFuncComponent) jsFuncComponents: QueryList<JsFuncComponent>; |
|||
|
|||
mobileActionTypes = Object.keys(WidgetMobileActionType); |
|||
mobileActionTypeTranslations = widgetMobileActionTypeTranslationMap; |
|||
mobileActionType = WidgetMobileActionType; |
|||
|
|||
customActionEditorCompleter = CustomActionEditorCompleter; |
|||
|
|||
mobileActionFormGroup: FormGroup; |
|||
mobileActionTypeFormGroup: FormGroup; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private fb: FormBuilder) { |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.mobileActionFormGroup = this.fb.group({ |
|||
type: [null, Validators.required], |
|||
handleEmptyResultFunction: [null], |
|||
handleErrorFunction: [null] |
|||
}); |
|||
this.mobileActionFormGroup.get('type').valueChanges.subscribe((type: WidgetMobileActionType) => { |
|||
let action: WidgetMobileActionDescriptor = this.mobileActionFormGroup.value; |
|||
if (this.mobileActionTypeFormGroup) { |
|||
action = {...action, ...this.mobileActionTypeFormGroup.value}; |
|||
} |
|||
this.updateMobileActionType(type, action); |
|||
}); |
|||
this.mobileActionFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.mobileActionFormGroup.disable({emitEvent: false}); |
|||
if (this.mobileActionTypeFormGroup) { |
|||
this.mobileActionTypeFormGroup.disable({emitEvent: false}); |
|||
} |
|||
} else { |
|||
this.mobileActionFormGroup.enable({emitEvent: false}); |
|||
if (this.mobileActionTypeFormGroup) { |
|||
this.mobileActionTypeFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
writeValue(value: WidgetMobileActionDescriptor | null): void { |
|||
this.mobileActionFormGroup.patchValue({type: value?.type, |
|||
handleEmptyResultFunction: value?.handleEmptyResultFunction, |
|||
handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); |
|||
this.updateMobileActionType(value?.type, value); |
|||
} |
|||
|
|||
private updateModel() { |
|||
let descriptor: WidgetMobileActionDescriptor = null; |
|||
if (this.mobileActionFormGroup.valid && this.mobileActionTypeFormGroup.valid) { |
|||
descriptor = { ...this.mobileActionFormGroup.getRawValue(), ...this.mobileActionTypeFormGroup.getRawValue() }; |
|||
} |
|||
this.propagateChange(descriptor); |
|||
} |
|||
|
|||
private updateMobileActionType(type?: WidgetMobileActionType, action?: WidgetMobileActionDescriptor) { |
|||
const prevType = action?.type; |
|||
const targetType = type || prevType; |
|||
const changed = prevType !== type; |
|||
if (changed && targetType) { |
|||
let handleEmptyResultFunction = action?.handleEmptyResultFunction; |
|||
const defaultHandleEmptyResultFunction = getDefaultHandleEmptyResultFunction(targetType); |
|||
if (defaultHandleEmptyResultFunction !== handleEmptyResultFunction) { |
|||
handleEmptyResultFunction = getDefaultHandleEmptyResultFunction(type); |
|||
this.mobileActionFormGroup.patchValue({handleEmptyResultFunction}, {emitEvent: false}); |
|||
} |
|||
let handleErrorFunction = action?.handleErrorFunction; |
|||
const defaultHandleErrorFunction = getDefaultHandleErrorFunction(targetType); |
|||
if (defaultHandleErrorFunction !== handleErrorFunction) { |
|||
handleErrorFunction = getDefaultHandleErrorFunction(type); |
|||
this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup = this.fb.group({}); |
|||
if (type) { |
|||
switch (type) { |
|||
case WidgetMobileActionType.takePictureFromGallery: |
|||
case WidgetMobileActionType.takePhoto: |
|||
case WidgetMobileActionType.takeScreenshot: |
|||
let processImageFunction = action?.processImageFunction; |
|||
if (changed) { |
|||
const defaultProcessImageFunction = getDefaultProcessImageFunction(targetType); |
|||
if (defaultProcessImageFunction !== processImageFunction) { |
|||
processImageFunction = getDefaultProcessImageFunction(type); |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'processImageFunction', |
|||
this.fb.control(processImageFunction, []) |
|||
); |
|||
break; |
|||
case WidgetMobileActionType.mapDirection: |
|||
case WidgetMobileActionType.mapLocation: |
|||
let getLocationFunction = action?.getLocationFunction; |
|||
let processLaunchResultFunction = action?.processLaunchResultFunction; |
|||
if (changed) { |
|||
const defaultGetLocationFunction = getDefaultGetLocationFunction(); |
|||
if (defaultGetLocationFunction !== getLocationFunction) { |
|||
getLocationFunction = defaultGetLocationFunction; |
|||
} |
|||
const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType); |
|||
if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) { |
|||
processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type); |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'getLocationFunction', |
|||
this.fb.control(getLocationFunction, [Validators.required]) |
|||
); |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'processLaunchResultFunction', |
|||
this.fb.control(processLaunchResultFunction, []) |
|||
); |
|||
break; |
|||
case WidgetMobileActionType.scanQrCode: |
|||
let processQrCodeFunction = action?.processQrCodeFunction; |
|||
if (changed) { |
|||
const defaultProcessQrCodeFunction = getDefaultProcessQrCodeFunction(); |
|||
if (defaultProcessQrCodeFunction !== processQrCodeFunction) { |
|||
processQrCodeFunction = defaultProcessQrCodeFunction; |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'processQrCodeFunction', |
|||
this.fb.control(processQrCodeFunction, []) |
|||
); |
|||
break; |
|||
case WidgetMobileActionType.makePhoneCall: |
|||
let getPhoneNumberFunction = action?.getPhoneNumberFunction; |
|||
processLaunchResultFunction = action?.processLaunchResultFunction; |
|||
if (changed) { |
|||
const defaultGetPhoneNumberFunction = getDefaultGetPhoneNumberFunction(); |
|||
if (defaultGetPhoneNumberFunction !== getPhoneNumberFunction) { |
|||
getPhoneNumberFunction = defaultGetPhoneNumberFunction; |
|||
} |
|||
const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType); |
|||
if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) { |
|||
processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type); |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'getPhoneNumberFunction', |
|||
this.fb.control(getPhoneNumberFunction, [Validators.required]) |
|||
); |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'processLaunchResultFunction', |
|||
this.fb.control(processLaunchResultFunction, []) |
|||
); |
|||
break; |
|||
case WidgetMobileActionType.getLocation: |
|||
let processLocationFunction = action?.processLocationFunction; |
|||
if (changed) { |
|||
const defaultProcessLocationFunction = getDefaultProcessLocationFunction(); |
|||
if (defaultProcessLocationFunction !== processLocationFunction) { |
|||
processLocationFunction = defaultProcessLocationFunction; |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.addControl( |
|||
'processLocationFunction', |
|||
this.fb.control(processLocationFunction, [Validators.required]) |
|||
); |
|||
break; |
|||
} |
|||
} |
|||
this.mobileActionTypeFormGroup.valueChanges.subscribe(() => { |
|||
this.updateModel(); |
|||
}); |
|||
} |
|||
|
|||
public validateOnSubmit() { |
|||
for (const jsFuncComponent of this.jsFuncComponents.toArray()) { |
|||
jsFuncComponent.validateOnSubmit(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,317 @@ |
|||
///
|
|||
/// Copyright © 2016-2021 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 { WidgetMobileActionType } from '@shared/models/widget.models'; |
|||
|
|||
const processImageFunctionTemplate = |
|||
'// Function body to process image obtained as a result of mobile action (take photo, take image from gallery, etc.). \n' + |
|||
'// - imageUrl - image URL in base64 data format\n\n' + |
|||
'showImageDialog(\'--TITLE--\', imageUrl);\n' + |
|||
'//saveEntityImageAttribute(\'image\', imageUrl);\n' + |
|||
'\n' + |
|||
'function showImageDialog(title, imageUrl) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n' + |
|||
'\n' + |
|||
'function saveEntityImageAttribute(attributeName, imageUrl) {\n' + |
|||
' if (entityId) {\n' + |
|||
' let attributes = [{\n' + |
|||
' key: attributeName, value: imageUrl\n' + |
|||
' }];\n' + |
|||
' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' + |
|||
' function() {\n' + |
|||
' widgetContext.showSuccessToast(\'Image attribute saved!\');\n' + |
|||
' },\n' + |
|||
' function(error) {\n' + |
|||
' widgetContext.dialogs.alert(\'Image attribute save failed\', JSON.stringify(error));\n' + |
|||
' }\n' + |
|||
' );\n' + |
|||
' }\n' + |
|||
'}\n' + |
|||
'\n' + |
|||
'var\n' + |
|||
' imageDialogTemplate =\n' + |
|||
' \'<div aria-label="Image">\' +\n' + |
|||
' \'<form #theForm="ngForm">\' +\n' + |
|||
' \'<mat-toolbar fxLayout="row" color="primary">\' +\n' + |
|||
' \'<h2>{{title}}</h2>\' +\n' + |
|||
' \'<span fxFlex></span>\' +\n' + |
|||
' \'<button mat-icon-button (click)="close()">\' +\n' + |
|||
' \'<mat-icon>close</mat-icon>\' +\n' + |
|||
' \'</button>\' +\n' + |
|||
' \'</mat-toolbar>\' +\n' + |
|||
' \'<div mat-dialog-content>\' +\n' + |
|||
' \'<div class="mat-content mat-padding">\' +\n' + |
|||
' \'<div fxLayout="column" fxFlex>\' +\n' + |
|||
' \'<div style="padding-top: 20px;">\' +\n' + |
|||
' \'<img [src]="imageUrl" style="height: 300px;"/>\' +\n' + |
|||
' \'</div>\' +\n' + |
|||
' \'</div>\' +\n' + |
|||
' \'</div>\' +\n' + |
|||
' \'</div>\' +\n' + |
|||
' \'<div mat-dialog-actions fxLayout="row">\' +\n' + |
|||
' \'<span fxFlex></span>\' +\n' + |
|||
' \'<button mat-button (click)="close()" style="margin-right:20px;">Close</button>\' +\n' + |
|||
' \'</div>\' +\n' + |
|||
' \'</form>\' +\n' + |
|||
' \'</div>\';\n' + |
|||
'\n' + |
|||
'function ImageDialogController(instance) {\n' + |
|||
' let vm = instance;\n' + |
|||
' vm.title = vm.data.title;\n' + |
|||
' vm.imageUrl = vm.data.imageUrl;\n' + |
|||
' vm.close = function ()\n' + |
|||
' {\n' + |
|||
' vm.dialogRef.close(null);\n' + |
|||
' }\n' + |
|||
'}\n'; |
|||
|
|||
const processLaunchResultFunctionTemplate = |
|||
'// Optional function body to process result of attempt to launch external mobile application (for ex. map application or phone call application). \n' + |
|||
'// - launched - boolean value indicating if the external application was successfully launched.\n\n' + |
|||
'showLaunchStatusDialog(\'--TITLE--\', launched);\n' + |
|||
'\n' + |
|||
'function showLaunchStatusDialog(title, status) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.dialogs.alert(title, status ? \'Successfully launched\' : \'Failed to launch\').subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n'; |
|||
|
|||
const processQrCodeFunction = |
|||
'// Function body to process result of QR code scanning. \n' + |
|||
'// - code - scanned QR code\n' + |
|||
'// - format - scanned QR code format\n\n' + |
|||
'showQrCodeDialog(\'QR Code\', code, format);\n' + |
|||
'\n' + |
|||
'function showQrCodeDialog(title, code, format) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.dialogs.alert(title, \'Code: [\'+code+\']<br>Format: \' + format).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n'; |
|||
|
|||
const processLocationFunction = |
|||
'// Function body to process current location of the phone. \n' + |
|||
'// - latitude - phone location latitude\n' + |
|||
'// - longitude - phone location longitude\n\n' + |
|||
'showLocationDialog(\'Location\', latitude, longitude);\n' + |
|||
'// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' + |
|||
'\n' + |
|||
'function showImageDialog(title, imageUrl) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n' + |
|||
'\n' + |
|||
'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' + |
|||
' if (entityId) {\n' + |
|||
' let attributes = [\n' + |
|||
' { key: latitudeAttributeName, value: latitude },\n' + |
|||
' { key: longitudeAttributeName, value: longitude }\n' + |
|||
' ];\n' + |
|||
' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' + |
|||
' function() {\n' + |
|||
' widgetContext.showSuccessToast(\'Location attributes saved!\');\n' + |
|||
' },\n' + |
|||
' function(error) {\n' + |
|||
' widgetContext.dialogs.alert(\'Location attributes save failed\', JSON.stringify(error));\n' + |
|||
' }\n' + |
|||
' );\n' + |
|||
' }\n' + |
|||
'}\n' + |
|||
'\n' + |
|||
'\n' + |
|||
'function showLocationDialog(title, latitude, longitude) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.dialogs.alert(title, \'Latitude: \'+latitude+\'<br>Longitude: \' + longitude).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}'; |
|||
|
|||
const handleEmptyResultFunctionTemplate = |
|||
'// Optional function body to handle empty result. \n' + |
|||
'// Usually this happens when user cancels the action (for ex. by pressing phone back button). \n\n' + |
|||
'showEmptyResultDialog(\'--MESSAGE--\');\n' + |
|||
'\n' + |
|||
'function showEmptyResultDialog(message) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.dialogs.alert(\'Empty result\', message).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n'; |
|||
|
|||
const handleErrorFunctionTemplate = |
|||
'// Optional function body to handle error occurred while mobile action execution \n' + |
|||
'// - error - Error message\n\n' + |
|||
'showErrorDialog(\'--TITLE--\', error);\n' + |
|||
'\n' + |
|||
'function showErrorDialog(title, error) {\n' + |
|||
' setTimeout(function() {\n' + |
|||
' widgetContext.dialogs.alert(title, error).subscribe();\n' + |
|||
' }, 100);\n' + |
|||
'}\n'; |
|||
|
|||
const getLocationFunctionTemplate = |
|||
'// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' + |
|||
'// Usually location can be obtained from entity attributes/telemetry. \n\n' + |
|||
'return getLocationFromEntityAttributes();\n' + |
|||
'//return [30, 30]; // TEST LOCATION\n' + |
|||
'\n' + |
|||
'\n' + |
|||
'function getLocationFromEntityAttributes() {\n' + |
|||
' if (entityId) {\n' + |
|||
' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'latitude\', \'longitude\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' + |
|||
' var res = [0,0];\n' + |
|||
' if (attributeData && attributeData.length === 2) {\n' + |
|||
' res[0] = attributeData.filter(function (data) { return data.key === \'latitude\'})[0].value;\n' + |
|||
' res[1] = attributeData.filter(function (data) { return data.key === \'longitude\'})[0].value;\n' + |
|||
' }\n' + |
|||
' return res;\n' + |
|||
' }));\n' + |
|||
' } else {\n' + |
|||
' return [0,0];\n' + |
|||
' }\n' + |
|||
'}\n'; |
|||
|
|||
const getPhoneNumberFunctionTemplate = |
|||
'// Function body that should return phone number for further processing by mobile action.\n' + |
|||
'// Usually phone number can be obtained from entity attributes/telemetry. \n\n' + |
|||
'return getPhoneNumberFromEntityAttributes();\n' + |
|||
'//return 123456789; // TEST PHONE NUMBER\n' + |
|||
'\n' + |
|||
'\n' + |
|||
'function getPhoneNumberFromEntityAttributes() {\n' + |
|||
' if (entityId) {\n' + |
|||
' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'phone\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' + |
|||
' var res = 0;\n' + |
|||
' if (attributeData && attributeData.length === 1) {\n' + |
|||
' res = attributeData[0].value;\n' + |
|||
' }\n' + |
|||
' return res;\n' + |
|||
' }));\n' + |
|||
' } else {\n' + |
|||
' return 0;\n' + |
|||
' }\n' + |
|||
'}\n'; |
|||
|
|||
export function getDefaultProcessImageFunction(type: WidgetMobileActionType): string { |
|||
let title; |
|||
switch (type) { |
|||
case WidgetMobileActionType.takePictureFromGallery: |
|||
title = 'Gallery picture'; |
|||
break; |
|||
case WidgetMobileActionType.takePhoto: |
|||
title = 'Photo'; |
|||
break; |
|||
case WidgetMobileActionType.takeScreenshot: |
|||
title = 'Screenshot'; |
|||
break; |
|||
} |
|||
return processImageFunctionTemplate.replace('--TITLE--', title); |
|||
} |
|||
|
|||
export function getDefaultProcessLaunchResultFunction(type: WidgetMobileActionType): string { |
|||
let title; |
|||
switch (type) { |
|||
case WidgetMobileActionType.mapLocation: |
|||
title = 'Map location'; |
|||
break; |
|||
case WidgetMobileActionType.mapDirection: |
|||
title = 'Map direction'; |
|||
break; |
|||
case WidgetMobileActionType.makePhoneCall: |
|||
title = 'Phone call'; |
|||
break; |
|||
} |
|||
return processLaunchResultFunctionTemplate.replace('--TITLE--', title); |
|||
} |
|||
|
|||
export function getDefaultProcessQrCodeFunction() { |
|||
return processQrCodeFunction; |
|||
} |
|||
|
|||
export function getDefaultProcessLocationFunction() { |
|||
return processLocationFunction; |
|||
} |
|||
|
|||
export function getDefaultGetLocationFunction() { |
|||
return getLocationFunctionTemplate; |
|||
} |
|||
|
|||
export function getDefaultGetPhoneNumberFunction() { |
|||
return getPhoneNumberFunctionTemplate; |
|||
} |
|||
|
|||
export function getDefaultHandleEmptyResultFunction(type: WidgetMobileActionType): string { |
|||
let message = 'Mobile action was cancelled!'; |
|||
switch (type) { |
|||
case WidgetMobileActionType.takePictureFromGallery: |
|||
message = 'Take picture from gallery action was cancelled!'; |
|||
break; |
|||
case WidgetMobileActionType.takePhoto: |
|||
message = 'Take photo action was cancelled!'; |
|||
break; |
|||
case WidgetMobileActionType.mapDirection: |
|||
message = 'Open map directions was not invoked!'; |
|||
break; |
|||
case WidgetMobileActionType.mapLocation: |
|||
message = 'Open location on map was not invoked!'; |
|||
break; |
|||
case WidgetMobileActionType.scanQrCode: |
|||
message = 'Scan QR code action was canceled!'; |
|||
break; |
|||
case WidgetMobileActionType.makePhoneCall: |
|||
message = 'Phone call was not invoked!'; |
|||
break; |
|||
case WidgetMobileActionType.getLocation: |
|||
message = 'Get location action was canceled!'; |
|||
break; |
|||
case WidgetMobileActionType.takeScreenshot: |
|||
message = 'Take screenshot action was cancelled!'; |
|||
break; |
|||
} |
|||
return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message); |
|||
} |
|||
|
|||
export function getDefaultHandleErrorFunction(type: WidgetMobileActionType): string { |
|||
let title = 'Mobile action failed'; |
|||
switch (type) { |
|||
case WidgetMobileActionType.takePictureFromGallery: |
|||
title = 'Failed to take picture from gallery'; |
|||
break; |
|||
case WidgetMobileActionType.takePhoto: |
|||
title = 'Failed to take photo'; |
|||
break; |
|||
case WidgetMobileActionType.mapDirection: |
|||
title = 'Failed to open map directions'; |
|||
break; |
|||
case WidgetMobileActionType.mapLocation: |
|||
title = 'Failed to open map location'; |
|||
break; |
|||
case WidgetMobileActionType.scanQrCode: |
|||
title = 'Failed to scan QR code'; |
|||
break; |
|||
case WidgetMobileActionType.makePhoneCall: |
|||
title = 'Failed to make phone call'; |
|||
break; |
|||
case WidgetMobileActionType.getLocation: |
|||
title = 'Failed to get phone location'; |
|||
break; |
|||
case WidgetMobileActionType.takeScreenshot: |
|||
title = 'Failed to take screenshot'; |
|||
break; |
|||
} |
|||
return handleErrorFunctionTemplate.replace('--TITLE--', title); |
|||
} |
|||
Loading…
Reference in new issue