105 changed files with 2106 additions and 442 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@ |
|||
/** |
|||
* 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.data.kv; |
|||
|
|||
import lombok.Data; |
|||
|
|||
@Data |
|||
public class TsKvLatestRemovingResult { |
|||
private String key; |
|||
private TsKvEntry data; |
|||
private boolean removed; |
|||
|
|||
public TsKvLatestRemovingResult(TsKvEntry data) { |
|||
this.key = data.getKey(); |
|||
this.data = data; |
|||
this.removed = true; |
|||
} |
|||
|
|||
public TsKvLatestRemovingResult(String key, boolean removed) { |
|||
this.key = key; |
|||
this.removed = removed; |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
import lombok.ToString; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; |
|||
|
|||
import java.util.HashSet; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
import static org.thingsboard.common.util.CollectionsUtil.diffSets; |
|||
|
|||
@Data |
|||
@NoArgsConstructor |
|||
@ToString(exclude = "toCancelRead") |
|||
@Slf4j |
|||
public class LwM2MModelConfig { |
|||
private String endpoint; |
|||
private Map<String, ObjectAttributes> attributesToAdd; |
|||
private Set<String> attributesToRemove; |
|||
private Set<String> toObserve; |
|||
private Set<String> toCancelObserve; |
|||
private Set<String> toRead; |
|||
@JsonIgnore |
|||
private Set<String> toCancelRead; |
|||
|
|||
public LwM2MModelConfig(String endpoint) { |
|||
this.endpoint = endpoint; |
|||
this.attributesToAdd = new ConcurrentHashMap<>(); |
|||
this.attributesToRemove = ConcurrentHashMap.newKeySet(); |
|||
this.toObserve = ConcurrentHashMap.newKeySet(); |
|||
this.toCancelObserve = ConcurrentHashMap.newKeySet(); |
|||
this.toRead = ConcurrentHashMap.newKeySet(); |
|||
this.toCancelRead = new HashSet<>(); |
|||
} |
|||
|
|||
public void merge(LwM2MModelConfig modelConfig) { |
|||
if (modelConfig.isEmpty() && modelConfig.getToCancelRead().isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
modelConfig.getAttributesToAdd().forEach((k, v) -> { |
|||
if (this.attributesToRemove.contains(k)) { |
|||
this.attributesToRemove.remove(k); |
|||
} else { |
|||
this.attributesToAdd.put(k, v); |
|||
} |
|||
}); |
|||
|
|||
modelConfig.getAttributesToRemove().forEach(k -> { |
|||
if (this.attributesToAdd.containsKey(k)) { |
|||
this.attributesToRemove.remove(k); |
|||
} else { |
|||
this.attributesToRemove.add(k); |
|||
} |
|||
}); |
|||
|
|||
this.toObserve.addAll(diffSets(this.toCancelObserve, modelConfig.getToObserve())); |
|||
this.toCancelObserve.addAll(diffSets(this.toObserve, modelConfig.getToCancelObserve())); |
|||
|
|||
this.toObserve.removeAll(modelConfig.getToCancelObserve()); |
|||
this.toCancelObserve.removeAll(modelConfig.getToObserve()); |
|||
|
|||
this.toRead.removeAll(modelConfig.getToObserve()); |
|||
this.toRead.removeAll(modelConfig.getToCancelRead()); |
|||
this.toRead.addAll(modelConfig.getToRead()); |
|||
} |
|||
|
|||
@JsonIgnore |
|||
public boolean isEmpty() { |
|||
return attributesToAdd.isEmpty() && toObserve.isEmpty() && toCancelObserve.isEmpty() && toRead.isEmpty(); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
|
|||
public interface LwM2MModelConfigService { |
|||
|
|||
void sendUpdates(LwM2mClient lwM2mClient); |
|||
|
|||
void sendUpdates(LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig); |
|||
|
|||
void persistUpdates(String endpoint); |
|||
|
|||
void removeUpdates(String endpoint); |
|||
} |
|||
@ -0,0 +1,235 @@ |
|||
/** |
|||
* 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.model; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; |
|||
import org.thingsboard.server.queue.util.AfterStartUp; |
|||
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.LwM2mDownlinkMsgHandler; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveRequest; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MObserveCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MObserveRequest; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadRequest; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; |
|||
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; |
|||
import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore; |
|||
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; |
|||
|
|||
import javax.annotation.PreDestroy; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentMap; |
|||
import java.util.concurrent.TimeoutException; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@TbLwM2mTransportComponent |
|||
public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { |
|||
|
|||
@Autowired |
|||
private TbLwM2MModelConfigStore modelStore; |
|||
|
|||
@Autowired |
|||
@Lazy |
|||
private LwM2mDownlinkMsgHandler downlinkMsgHandler; |
|||
@Autowired |
|||
@Lazy |
|||
private LwM2mUplinkMsgHandler uplinkMsgHandler; |
|||
@Autowired |
|||
@Lazy |
|||
private LwM2mClientContext clientContext; |
|||
|
|||
@Autowired |
|||
private LwM2MTelemetryLogService logService; |
|||
|
|||
private ConcurrentMap<String, LwM2MModelConfig> currentModelConfigs; |
|||
|
|||
@AfterStartUp(order = Integer.MAX_VALUE - 1) |
|||
private void init() { |
|||
List<LwM2MModelConfig> models = modelStore.getAll(); |
|||
log.debug("Fetched model configs: {}", models); |
|||
currentModelConfigs = models.stream() |
|||
.collect(Collectors.toConcurrentMap(LwM2MModelConfig::getEndpoint, m -> m)); |
|||
} |
|||
|
|||
@Override |
|||
public void sendUpdates(LwM2mClient lwM2mClient) { |
|||
LwM2MModelConfig modelConfig = currentModelConfigs.get(lwM2mClient.getEndpoint()); |
|||
if (modelConfig == null || modelConfig.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
doSend(lwM2mClient, modelConfig); |
|||
} |
|||
|
|||
public void sendUpdates(LwM2mClient lwM2mClient, LwM2MModelConfig newModelConfig) { |
|||
String endpoint = lwM2mClient.getEndpoint(); |
|||
LwM2MModelConfig modelConfig = currentModelConfigs.get(endpoint); |
|||
if (modelConfig == null || modelConfig.isEmpty()) { |
|||
modelConfig = newModelConfig; |
|||
currentModelConfigs.put(endpoint, modelConfig); |
|||
} else { |
|||
modelConfig.merge(newModelConfig); |
|||
} |
|||
|
|||
if (lwM2mClient.isAsleep()) { |
|||
modelStore.put(modelConfig); |
|||
} else { |
|||
doSend(lwM2mClient, modelConfig); |
|||
} |
|||
} |
|||
|
|||
private void doSend(LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig) { |
|||
log.trace("Send LwM2M Model updates: [{}]", modelConfig); |
|||
|
|||
String endpoint = lwM2mClient.getEndpoint(); |
|||
|
|||
Map<String, ObjectAttributes> attrToAdd = modelConfig.getAttributesToAdd(); |
|||
attrToAdd.forEach((id, attributes) -> { |
|||
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(id) |
|||
.attributes(attributes) |
|||
.timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); |
|||
downlinkMsgHandler.sendWriteAttributesRequest(lwM2mClient, request, |
|||
createDownlinkProxyCallback(() -> { |
|||
attrToAdd.remove(id); |
|||
if (modelConfig.isEmpty()) { |
|||
modelStore.remove(endpoint); |
|||
} |
|||
}, new TbLwM2MWriteAttributesCallback(logService, lwM2mClient, id)) |
|||
); |
|||
}); |
|||
|
|||
Set<String> attrToRemove = modelConfig.getAttributesToRemove(); |
|||
attrToRemove.forEach((id) -> { |
|||
TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(id) |
|||
.attributes(new ObjectAttributes()) |
|||
.timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); |
|||
downlinkMsgHandler.sendWriteAttributesRequest(lwM2mClient, request, |
|||
createDownlinkProxyCallback(() -> { |
|||
attrToRemove.remove(id); |
|||
if (modelConfig.isEmpty()) { |
|||
modelStore.remove(endpoint); |
|||
} |
|||
}, new TbLwM2MWriteAttributesCallback(logService, lwM2mClient, id)) |
|||
); |
|||
}); |
|||
|
|||
Set<String> toRead = modelConfig.getToRead(); |
|||
toRead.forEach(id -> { |
|||
TbLwM2MReadRequest request = TbLwM2MReadRequest.builder().versionedId(id) |
|||
.timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); |
|||
downlinkMsgHandler.sendReadRequest(lwM2mClient, request, |
|||
createDownlinkProxyCallback(() -> { |
|||
toRead.remove(id); |
|||
if (modelConfig.isEmpty()) { |
|||
modelStore.remove(endpoint); |
|||
} |
|||
}, new TbLwM2MReadCallback(uplinkMsgHandler, logService, lwM2mClient, id)) |
|||
); |
|||
}); |
|||
|
|||
Set<String> toObserve = modelConfig.getToObserve(); |
|||
toObserve.forEach(id -> { |
|||
TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(id) |
|||
.timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); |
|||
downlinkMsgHandler.sendObserveRequest(lwM2mClient, request, |
|||
createDownlinkProxyCallback(() -> { |
|||
toObserve.remove(id); |
|||
if (modelConfig.isEmpty()) { |
|||
modelStore.remove(endpoint); |
|||
} |
|||
}, new TbLwM2MObserveCallback(uplinkMsgHandler, logService, lwM2mClient, id)) |
|||
); |
|||
}); |
|||
|
|||
Set<String> toCancelObserve = modelConfig.getToCancelObserve(); |
|||
toCancelObserve.forEach(id -> { |
|||
TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(id) |
|||
.timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); |
|||
downlinkMsgHandler.sendCancelObserveRequest(lwM2mClient, request, |
|||
createDownlinkProxyCallback(() -> { |
|||
toCancelObserve.remove(id); |
|||
if (modelConfig.isEmpty()) { |
|||
modelStore.remove(endpoint); |
|||
} |
|||
}, new TbLwM2MCancelObserveCallback(logService, lwM2mClient, id)) |
|||
); |
|||
}); |
|||
} |
|||
|
|||
private <R, T> DownlinkRequestCallback<R, T> createDownlinkProxyCallback(Runnable processRemove, DownlinkRequestCallback<R, T> callback) { |
|||
return new DownlinkRequestCallback<>() { |
|||
@Override |
|||
public void onSuccess(R request, T response) { |
|||
processRemove.run(); |
|||
callback.onSuccess(request, response); |
|||
} |
|||
|
|||
@Override |
|||
public void onValidationError(String params, String msg) { |
|||
processRemove.run(); |
|||
callback.onValidationError(params, msg); |
|||
} |
|||
|
|||
@Override |
|||
public void onError(String params, Exception e) { |
|||
try { |
|||
if (e instanceof TimeoutException) { |
|||
return; |
|||
} |
|||
processRemove.run(); |
|||
} finally { |
|||
callback.onError(params, e); |
|||
} |
|||
} |
|||
|
|||
}; |
|||
} |
|||
|
|||
@Override |
|||
public void persistUpdates(String endpoint) { |
|||
LwM2MModelConfig modelConfig = currentModelConfigs.get(endpoint); |
|||
if (modelConfig != null && !modelConfig.isEmpty()) { |
|||
modelStore.put(modelConfig); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void removeUpdates(String endpoint) { |
|||
currentModelConfigs.remove(endpoint); |
|||
} |
|||
|
|||
@PreDestroy |
|||
private void destroy() { |
|||
currentModelConfigs.values().forEach(model -> { |
|||
if (model != null && !model.isEmpty()) { |
|||
modelStore.put(model); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -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.server.store; |
|||
|
|||
import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
public class TbDummyLwM2MModelConfigStore implements TbLwM2MModelConfigStore { |
|||
@Override |
|||
public List<LwM2MModelConfig> getAll() { |
|||
return Collections.emptyList(); |
|||
} |
|||
|
|||
@Override |
|||
public void put(LwM2MModelConfig modelConfig) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void remove(String endpoint) { |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
/** |
|||
* 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.store; |
|||
|
|||
import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface TbLwM2MModelConfigStore { |
|||
List<LwM2MModelConfig> getAll(); |
|||
|
|||
void put(LwM2MModelConfig modelConfig); |
|||
|
|||
void remove(String endpoint); |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
/** |
|||
* 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.store; |
|||
|
|||
import lombok.AllArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.redis.connection.RedisClusterConnection; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.Cursor; |
|||
import org.springframework.data.redis.core.ScanOptions; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
@AllArgsConstructor |
|||
public class TbRedisLwM2MModelConfigStore implements TbLwM2MModelConfigStore { |
|||
private static final String MODEL_EP = "MODEL#EP#"; |
|||
private final RedisConnectionFactory connectionFactory; |
|||
|
|||
@Override |
|||
public List<LwM2MModelConfig> getAll() { |
|||
try (var connection = connectionFactory.getConnection()) { |
|||
List<LwM2MModelConfig> configs = new ArrayList<>(); |
|||
ScanOptions scanOptions = ScanOptions.scanOptions().count(100).match(MODEL_EP + "*").build(); |
|||
List<Cursor<byte[]>> scans = new ArrayList<>(); |
|||
if (connection instanceof RedisClusterConnection) { |
|||
((RedisClusterConnection) connection).clusterGetNodes().forEach(node -> { |
|||
scans.add(((RedisClusterConnection) connection).scan(node, scanOptions)); |
|||
}); |
|||
} else { |
|||
scans.add(connection.scan(scanOptions)); |
|||
} |
|||
|
|||
scans.forEach(scan -> { |
|||
scan.forEachRemaining(key -> { |
|||
byte[] element = connection.get(key); |
|||
configs.add(JacksonUtil.fromBytes(element, LwM2MModelConfig.class)); |
|||
}); |
|||
}); |
|||
return configs; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void put(LwM2MModelConfig modelConfig) { |
|||
byte[] clientSerialized = JacksonUtil.writeValueAsBytes(modelConfig); |
|||
try (var connection = connectionFactory.getConnection()) { |
|||
connection.getSet(getKey(modelConfig.getEndpoint()), clientSerialized); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void remove(String endpoint) { |
|||
try (var connection = connectionFactory.getConnection()) { |
|||
connection.del(getKey(endpoint)); |
|||
} |
|||
} |
|||
|
|||
private byte[] getKey(String endpoint) { |
|||
return (MODEL_EP + endpoint).getBytes(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* 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.common.util; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.Set; |
|||
import java.util.stream.Collectors; |
|||
|
|||
public class CollectionsUtil { |
|||
public static boolean isEmpty(Collection<?> collection) { |
|||
return collection == null || collection.isEmpty(); |
|||
} |
|||
|
|||
public static boolean isNotEmpty(Collection<?> collection) { |
|||
return !isEmpty(collection); |
|||
} |
|||
|
|||
/** |
|||
* Returns new set with elements that are present in set B(new) but absent in set A(old). |
|||
*/ |
|||
public static <T> Set<T> diffSets(Set<T> a, Set<T> b) { |
|||
return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet()); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<tb-dashboard-page |
|||
[embedded]="true" |
|||
[syncStateWithQueryParam]="false" |
|||
[hideToolbar]="true" |
|||
[currentState]="currentState" |
|||
[dashboard]="dashboard" |
|||
[parentDashboard]="parentDashboard"> |
|||
</tb-dashboard-page> |
|||
@ -0,0 +1,93 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Dashboard } from '@shared/models/dashboard.models'; |
|||
import { StateObject } from '@core/api/widget-api.models'; |
|||
import { updateEntityParams, WidgetContext } from '@home/models/widget-component.models'; |
|||
import { deepClone, objToBase64 } from '@core/utils'; |
|||
import { IDashboardComponent } from '@home/models/dashboard-component.models'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { Subscription } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-dashboard-state', |
|||
templateUrl: './dashboard-state.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class DashboardStateComponent extends PageComponent implements OnInit, OnDestroy { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
stateId: string; |
|||
|
|||
@Input() |
|||
syncParentStateParams = false; |
|||
|
|||
@Input() |
|||
entityParamName: string; |
|||
|
|||
@Input() |
|||
entityId: EntityId; |
|||
|
|||
currentState: string; |
|||
|
|||
dashboard: Dashboard; |
|||
|
|||
parentDashboard: IDashboardComponent; |
|||
|
|||
private stateSubscription: Subscription; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private cd: ChangeDetectorRef) { |
|||
super(store); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.dashboard = deepClone(this.ctx.stateController.dashboardCtrl.dashboardCtx.getDashboard()); |
|||
this.updateCurrentState(); |
|||
this.parentDashboard = this.ctx.parentDashboard ? |
|||
this.ctx.parentDashboard : this.ctx.dashboard; |
|||
if (this.syncParentStateParams) { |
|||
this.stateSubscription = this.ctx.stateController.dashboardCtrl.dashboardCtx.stateChanged.subscribe(() => { |
|||
this.updateCurrentState(); |
|||
this.cd.markForCheck(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.stateSubscription) { |
|||
this.stateSubscription.unsubscribe(); |
|||
} |
|||
} |
|||
|
|||
private updateCurrentState(): void { |
|||
const stateObject: StateObject = {}; |
|||
const params = deepClone(this.ctx.stateController.getStateParams()); |
|||
updateEntityParams(params, this.entityParamName, this.entityId); |
|||
stateObject.params = params; |
|||
if (this.stateId) { |
|||
stateObject.id = this.stateId; |
|||
} |
|||
this.currentState = objToBase64([stateObject]); |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
<!-- |
|||
|
|||
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 class="tb-css" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" |
|||
tb-fullscreen |
|||
[fullscreen]="fullscreen" fxLayout="column"> |
|||
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-css-toolbar"> |
|||
<label class="tb-title no-padding" [ngClass]="{'tb-error': hasErrors}">{{ label }}</label> |
|||
<span fxFlex></span> |
|||
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyCss()"> |
|||
{{'js-func.tidy' | translate }} |
|||
</button> |
|||
<fieldset style="width: initial"> |
|||
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
|||
matTooltipPosition="above" |
|||
style="border-radius: 50%" |
|||
(click)="fullscreen = !fullscreen"> |
|||
<button type='button' mat-button mat-icon-button class="tb-mat-32"> |
|||
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
|||
</button> |
|||
</div> |
|||
</fieldset> |
|||
</div> |
|||
<div id="tb-css-panel" class="tb-css-content-panel" fxLayout="column"> |
|||
<div #cssEditor id="tb-css-input" [ngClass]="{'fill-height': fillHeight}"></div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,66 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
.tb-css { |
|||
position: relative; |
|||
|
|||
&.tb-disabled { |
|||
color: rgba(0, 0, 0, .38); |
|||
} |
|||
|
|||
&.fill-height { |
|||
height: 100%; |
|||
} |
|||
|
|||
.tb-css-content-panel { |
|||
height: calc(100% - 80px); |
|||
margin-left: 15px; |
|||
border: 1px solid #c0c0c0; |
|||
|
|||
#tb-css-input { |
|||
width: 100%; |
|||
min-width: 200px; |
|||
height: 100%; |
|||
|
|||
&:not(.fill-height) { |
|||
min-height: 200px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-css-toolbar { |
|||
& > * { |
|||
&:not(:last-child) { |
|||
margin-right: 4px; |
|||
} |
|||
} |
|||
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { |
|||
background: rgba(220, 220, 220, .35); |
|||
align-items: center; |
|||
vertical-align: middle; |
|||
min-width: 32px; |
|||
min-height: 15px; |
|||
padding: 4px; |
|||
font-size: .8rem; |
|||
line-height: 15px; |
|||
&:not(.tb-help-popup-button) { |
|||
color: #7b7b7b; |
|||
} |
|||
} |
|||
.tb-help-popup-button-loading { |
|||
background: #f3f3f3; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,207 @@ |
|||
///
|
|||
/// 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, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
ViewChild, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
|||
import { Ace } from 'ace-builds'; |
|||
import { getAce } from '@shared/models/ace/ace.models'; |
|||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { beautifyCss } from '@shared/models/beautify.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-css', |
|||
templateUrl: './css.component.html', |
|||
styleUrls: ['./css.component.scss'], |
|||
providers: [ |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CssComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => CssComponent), |
|||
multi: true, |
|||
} |
|||
], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class CssComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { |
|||
|
|||
@ViewChild('cssEditor', {static: true}) |
|||
cssEditorElmRef: ElementRef; |
|||
|
|||
private cssEditor: Ace.Editor; |
|||
private editorsResizeCaf: CancelAnimationFrame; |
|||
private editorResize$: ResizeObserver; |
|||
private ignoreChange = false; |
|||
|
|||
@Input() label: string; |
|||
|
|||
@Input() disabled: boolean; |
|||
|
|||
@Input() fillHeight: boolean; |
|||
|
|||
private requiredValue: boolean; |
|||
get required(): boolean { |
|||
return this.requiredValue; |
|||
} |
|||
@Input() |
|||
set required(value: boolean) { |
|||
this.requiredValue = coerceBooleanProperty(value); |
|||
} |
|||
|
|||
fullscreen = false; |
|||
|
|||
modelValue: string; |
|||
|
|||
hasErrors = false; |
|||
|
|||
private propagateChange = null; |
|||
|
|||
constructor(public elementRef: ElementRef, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
protected store: Store<AppState>, |
|||
private raf: RafService) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
const editorElement = this.cssEditorElmRef.nativeElement; |
|||
let editorOptions: Partial<Ace.EditorOptions> = { |
|||
mode: 'ace/mode/css', |
|||
showGutter: true, |
|||
showPrintMargin: true, |
|||
readOnly: this.disabled |
|||
}; |
|||
|
|||
const advancedOptions = { |
|||
enableSnippets: true, |
|||
enableBasicAutocompletion: true, |
|||
enableLiveAutocompletion: true |
|||
}; |
|||
|
|||
editorOptions = {...editorOptions, ...advancedOptions}; |
|||
getAce().subscribe( |
|||
(ace) => { |
|||
this.cssEditor = ace.edit(editorElement, editorOptions); |
|||
this.cssEditor.session.setUseWrapMode(true); |
|||
this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1); |
|||
this.cssEditor.setReadOnly(this.disabled); |
|||
this.cssEditor.on('change', () => { |
|||
if (!this.ignoreChange) { |
|||
this.updateView(); |
|||
} |
|||
}); |
|||
// @ts-ignore
|
|||
this.cssEditor.session.on('changeAnnotation', () => { |
|||
const annotations = this.cssEditor.session.getAnnotations(); |
|||
const hasErrors = annotations.filter(annotation => annotation.type === 'error').length > 0; |
|||
if (this.hasErrors !== hasErrors) { |
|||
this.hasErrors = hasErrors; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
}); |
|||
this.editorResize$ = new ResizeObserver(() => { |
|||
this.onAceEditorResize(); |
|||
}); |
|||
this.editorResize$.observe(editorElement); |
|||
} |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy(): void { |
|||
if (this.editorResize$) { |
|||
this.editorResize$.disconnect(); |
|||
} |
|||
} |
|||
|
|||
private onAceEditorResize() { |
|||
if (this.editorsResizeCaf) { |
|||
this.editorsResizeCaf(); |
|||
this.editorsResizeCaf = null; |
|||
} |
|||
this.editorsResizeCaf = this.raf.raf(() => { |
|||
this.cssEditor.resize(); |
|||
this.cssEditor.renderer.updateFull(); |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.cssEditor) { |
|||
this.cssEditor.setReadOnly(this.disabled); |
|||
} |
|||
} |
|||
|
|||
public validate(c: FormControl) { |
|||
return (!this.hasErrors) ? null : { |
|||
css: { |
|||
valid: false, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
beautifyCss() { |
|||
beautifyCss(this.modelValue, {indent_size: 4}).subscribe( |
|||
(res) => { |
|||
if (this.modelValue !== res) { |
|||
this.cssEditor.setValue(res ? res : '', -1); |
|||
this.updateView(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
writeValue(value: string): void { |
|||
this.modelValue = value; |
|||
if (this.cssEditor) { |
|||
this.ignoreChange = true; |
|||
this.cssEditor.setValue(this.modelValue ? this.modelValue : '', -1); |
|||
this.ignoreChange = false; |
|||
} |
|||
} |
|||
|
|||
updateView() { |
|||
const editorValue = this.cssEditor.getValue(); |
|||
if (this.modelValue !== editorValue) { |
|||
this.modelValue = editorValue; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue