From b40fa86bac2ed81fca5be45d08711288f4f8392a Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 13 Mar 2025 17:35:00 +0200 Subject: [PATCH 01/14] Refactoring and fixes for CF lifecycle events handling --- .../server/actors/app/AppActor.java | 1 - ...alculatedFieldManagerMessageProcessor.java | 2 +- .../server/actors/tenant/TenantActor.java | 31 +++--- ...faultTbCalculatedFieldConsumerService.java | 19 +--- .../queue/DefaultTbClusterService.java | 101 +++++------------- .../DefaultTbRuleEngineConsumerService.java | 3 +- .../server/cluster/TbClusterService.java | 2 - .../server/common/data/EntityType.java | 11 ++ .../cf/CalculatedFieldEntityLifecycleMsg.java | 3 - common/proto/src/main/proto/queue.proto | 8 +- .../discovery/event/PartitionChangeEvent.java | 1 - 11 files changed, 63 insertions(+), 119 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java index 50fa7e2f8d..904547a2b6 100644 --- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java @@ -116,7 +116,6 @@ public class AppActor extends ContextAwareActor { case CF_INIT_MSG: case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: //TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. // same for the Linked telemetry. onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 2d182510e9..fc48e9ad3e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -196,7 +196,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void onEntityUpdated(ComponentLifecycleMsg msg, TbCallback callback) { - if (msg.getOldProfileId() != null && msg.getOldProfileId() != msg.getProfileId()) { + if (msg.getOldProfileId() != null && !msg.getOldProfileId().equals(msg.getProfileId())) { cfEntityCache.update(tenantId, msg.getOldProfileId(), msg.getProfileId(), msg.getEntityId()); if (!isMyPartition(msg.getEntityId(), callback)) { return; diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index c1a0980623..846bde508d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.aware.DeviceAwareMsg; import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg; +import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; @@ -175,7 +176,6 @@ public class TenantActor extends RuleChainManagerActor { case CF_LINK_INIT_MSG: case CF_STATE_RESTORE_MSG: case CF_PARTITIONS_CHANGE_MSG: - case CF_ENTITY_LIFECYCLE_MSG: onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); break; case CF_TELEMETRY_MSG: @@ -315,19 +315,26 @@ public class TenantActor extends RuleChainManagerActor { onToDeviceActorMsg(new DeviceDeleteMsg(tenantId, deviceId), true); deletedDevices.add(deviceId); } - if (isRuleEngine && ruleChainsInitialized) { - TbActorRef target = getEntityActorRef(msg.getEntityId()); - if (target != null) { - if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { - RuleChain ruleChain = systemContext.getRuleChainService(). - findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); - if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { - visit(ruleChain, target); + if (isRuleEngine) { + if (ruleChainsInitialized) { + TbActorRef target = getEntityActorRef(msg.getEntityId()); + if (target != null) { + if (msg.getEntityId().getEntityType() == EntityType.RULE_CHAIN) { + RuleChain ruleChain = systemContext.getRuleChainService(). + findRuleChainById(tenantId, new RuleChainId(msg.getEntityId().getId())); + if (ruleChain != null && RuleChainType.CORE.equals(ruleChain.getType())) { + visit(ruleChain, target); + } } + target.tellWithHighPriority(msg); + } else { + log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); + } + } + if (cfActor != null) { + if (msg.getEntityId().getEntityType().isOneOf(EntityType.CALCULATED_FIELD, EntityType.DEVICE, EntityType.ASSET)) { + cfActor.tellWithHighPriority(new CalculatedFieldEntityLifecycleMsg(tenantId, msg)); } - target.tellWithHighPriority(msg); - } else { - log.debug("[{}] Invalid component lifecycle msg: {}", tenantId, msg); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 3232a9b89f..04aab6b39d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -28,15 +28,12 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.QueueConfig; -import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; -import org.thingsboard.server.gen.transport.TransportProtos.ComponentLifecycleMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -65,8 +62,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.util.ProtoUtils.fromProto; - @Service @TbRuleEngineComponent @Slf4j @@ -164,9 +159,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer forwardToActorSystem(toCfMsg.getTelemetryMsg(), callback); } else if (toCfMsg.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfMsg.getLinkedTelemetryMsg(), callback); - } else if (toCfMsg.hasComponentLifecycleMsg()) { - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfMsg.getComponentLifecycleMsg()); - forwardToActorSystem(toCfMsg.getComponentLifecycleMsg(), callback); } } catch (Throwable e) { log.warn("[{}] Failed to process message: {}", id, msg, e); @@ -215,11 +207,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) { ToCalculatedFieldNotificationMsg toCfNotification = msg.getValue(); - if (toCfNotification.hasComponentLifecycleMsg()) { - handleComponentLifecycleMsg(id, ProtoUtils.fromProto(toCfNotification.getComponentLifecycleMsg())); - log.trace("[{}] Forwarding component lifecycle message for processing {}", id, toCfNotification.getComponentLifecycleMsg()); - forwardToActorSystem(toCfNotification.getComponentLifecycleMsg(), callback); - } else if (toCfNotification.hasLinkedTelemetryMsg()) { + if (toCfNotification.hasLinkedTelemetryMsg()) { forwardToActorSystem(toCfNotification.getLinkedTelemetryMsg(), callback); } } @@ -237,11 +225,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer actorContext.tell(new CalculatedFieldLinkedTelemetryMsg(tenantId, entityId, linkedMsg, callback)); } - private void forwardToActorSystem(ComponentLifecycleMsgProto proto, TbCallback callback) { - var msg = fromProto(proto); - actorContext.tell(new CalculatedFieldEntityLifecycleMsg(msg.getTenantId(), msg, callback)); - } - private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) { return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB)); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index c7174469b0..92c48a5fed 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -94,10 +94,8 @@ import org.thingsboard.server.queue.common.MultipleTbQueueCallbackWrapper; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.TbRuleEngineProducerService; import org.thingsboard.server.queue.discovery.PartitionService; -import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; -import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbAssetProfileCache; @@ -146,10 +144,6 @@ public class DefaultTbClusterService implements TbClusterService { @Lazy private OtaPackageStateService otaPackageStateService; - @Autowired - @Lazy - private CalculatedFieldProcessingService calculatedFieldProcessingService; - private final TopicService topicService; private final TbDeviceProfileCache deviceProfileCache; private final TbAssetProfileCache assetProfileCache; @@ -369,13 +363,6 @@ public class DefaultTbClusterService implements TbClusterService { toRuleEngineMsgs.incrementAndGet(); // TODO: add separate counter when we will have new ServiceType.CALCULATED_FIELDS } - @Override - public void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback) { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId); - producerProvider.getCalculatedFieldsNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), callback); - toRuleEngineNfs.incrementAndGet(); - } - @Override public void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state) { log.trace("[{}] Processing {} state change event: {}", tenantId, entityId.getEntityType(), state); @@ -431,7 +418,6 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceDeleted(TenantId tenantId, Device device, TbQueueCallback callback) { DeviceId deviceId = device.getId(); gatewayNotificationsService.onDeviceDeleted(device); - handleCalculatedFieldEntityDeleted(tenantId, deviceId); broadcastEntityDeleteToTransport(tenantId, deviceId, device.getName(), callback); sendDeviceStateServiceEvent(tenantId, deviceId, false, false, true); broadcastEntityStateChangeEvent(tenantId, deviceId, ComponentLifecycleEvent.DELETED); @@ -440,7 +426,6 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetDeleted(TenantId tenantId, Asset asset, TbQueueCallback callback) { AssetId assetId = asset.getId(); - handleCalculatedFieldEntityDeleted(tenantId, assetId); broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED); } @@ -604,6 +589,7 @@ public class DefaultTbClusterService implements TbClusterService { || (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || entityType.equals(EntityType.ENTITY_VIEW) || entityType.equals(EntityType.NOTIFICATION_RULE) + || entityType.equals(EntityType.CALCULATED_FIELD) ) { TbQueueProducer> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer(); Set tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE); @@ -658,38 +644,28 @@ public class DefaultTbClusterService implements TbClusterService { public void onDeviceUpdated(Device entity, Device old) { var created = old == null; broadcastEntityChangeToTransport(entity.getTenantId(), entity.getId(), entity, null); - if (old != null) { + + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getDeviceProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); + } else { boolean deviceNameChanged = !entity.getName().equals(old.getName()); if (deviceNameChanged) { gatewayNotificationsService.onDeviceUpdated(entity, old); } boolean deviceProfileChanged = !entity.getDeviceProfileId().equals(old.getDeviceProfileId()); - if (deviceProfileChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getDeviceProfileId()) - .profileId(entity.getDeviceProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } if (deviceNameChanged || deviceProfileChanged) { pushMsgToCore(new DeviceNameOrTypeUpdateMsg(entity.getTenantId(), entity.getId(), entity.getName(), entity.getType()), null); } - } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getDeviceProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getDeviceProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); sendDeviceStateServiceEvent(entity.getTenantId(), entity.getId(), created, !created, false); otaPackageStateService.update(entity, old); } @@ -697,48 +673,29 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onAssetUpdated(Asset entity, Asset old) { var created = old == null; - if (old != null) { - boolean assetTypeChanged = !entity.getAssetProfileId().equals(old.getAssetProfileId()); - if (assetTypeChanged) { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.UPDATED) - .oldProfileId(old.getAssetProfileId()) - .profileId(entity.getAssetProfileId()) - .oldName(old.getName()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } + var msg = ComponentLifecycleMsg.builder() + .tenantId(entity.getTenantId()) + .entityId(entity.getId()) + .profileId(entity.getAssetProfileId()) + .name(entity.getName()); + if (created) { + msg.event(ComponentLifecycleEvent.CREATED); } else { - ComponentLifecycleMsg msg = ComponentLifecycleMsg.builder() - .tenantId(entity.getTenantId()) - .entityId(entity.getId()) - .event(ComponentLifecycleEvent.CREATED) - .profileId(entity.getAssetProfileId()) - .name(entity.getName()) - .build(); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); + msg.event(ComponentLifecycleEvent.UPDATED) + .oldProfileId(old.getAssetProfileId()) + .oldName(old.getName()); } - broadcastEntityStateChangeEvent(entity.getTenantId(), entity.getId(), created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); + broadcast(msg.build()); } @Override public void onCalculatedFieldUpdated(CalculatedField calculatedField, CalculatedField oldCalculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED)); - onCalculatedFieldLifecycleMsg(msg, callback); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), oldCalculatedField == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); } @Override public void onCalculatedFieldDeleted(CalculatedField calculatedField, TbQueueCallback callback) { - var msg = toProto(new ComponentLifecycleMsg(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED)); - onCalculatedFieldLifecycleMsg(msg, callback); - } - - private void onCalculatedFieldLifecycleMsg(ComponentLifecycleMsgProto msg, TbQueueCallback callback) { - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(msg).build(), callback); - broadcastToCore(ToCoreNotificationMsg.newBuilder().setComponentLifecycle(msg).build()); + broadcastEntityStateChangeEvent(calculatedField.getTenantId(), calculatedField.getId(), ComponentLifecycleEvent.DELETED); } @Override @@ -868,8 +825,4 @@ public class DefaultTbClusterService implements TbClusterService { } } - private void handleCalculatedFieldEntityDeleted(TenantId tenantId, EntityId entityId) { - ComponentLifecycleMsg msg = new ComponentLifecycleMsg(tenantId, entityId, ComponentLifecycleEvent.DELETED); - broadcastToCalculatedFields(ToCalculatedFieldNotificationMsg.newBuilder().setComponentLifecycleMsg(toProto(msg)).build(), TbQueueCallback.EMPTY); - } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index d522f11f7b..9cc743e510 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rpc.RpcError; -import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; @@ -234,7 +233,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< if (event.getEvent() == ComponentLifecycleEvent.DELETED) { List toRemove = consumers.keySet().stream() .filter(queueKey -> queueKey.getTenantId().equals(event.getTenantId())) - .collect(Collectors.toList()); + .toList(); toRemove.forEach(queueKey -> { removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(false)); }); diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 2a2d04e0fd..aed6eb4cf5 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -85,8 +85,6 @@ public interface TbClusterService extends TbQueueClusterService { void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); - void pushNotificationToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldNotificationMsg msg, TbQueueCallback callback); - void broadcastEntityStateChangeEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state); void onDeviceProfileChange(DeviceProfile deviceProfile, DeviceProfile oldDeviceProfile, TbQueueCallback callback); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 4ea66be1b4..1c56aa25dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -86,4 +86,15 @@ public enum EntityType { this.tableName = tableName; } + public boolean isOneOf(EntityType... types) { + if (types == null) { + return false; + } + for (EntityType type : types) { + if (this == type) { + return true; + } + } + return false; + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java index 099240f54d..1eb8f425ab 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cf/CalculatedFieldEntityLifecycleMsg.java @@ -16,19 +16,16 @@ package org.thingsboard.server.common.msg.cf; import lombok.Data; -import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; @Data public class CalculatedFieldEntityLifecycleMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final ComponentLifecycleMsg data; - private final TbCallback callback; @Override public MsgType getMsgType() { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 6ecdf13981..1b2edcc96d 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1674,14 +1674,12 @@ message ToEdgeEventNotificationMsg { } message ToCalculatedFieldMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldTelemetryMsgProto telemetryMsg = 2; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 3; + CalculatedFieldTelemetryMsgProto telemetryMsg = 1; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; } message ToCalculatedFieldNotificationMsg { - ComponentLifecycleMsgProto componentLifecycleMsg = 1; - CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 2; + CalculatedFieldLinkedTelemetryMsgProto linkedTelemetryMsg = 1; } /* Messages that are handled by ThingsBoard RuleEngine Service */ diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java index f165f60be7..32537edba5 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/event/PartitionChangeEvent.java @@ -24,7 +24,6 @@ import org.thingsboard.server.queue.discovery.QueueKey; import java.io.Serial; import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; From 972035373cdf2d888f4e7b2e6cbaf4219dcbdb17 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 14 Mar 2025 16:20:55 +0200 Subject: [PATCH 02/14] UI: Fixed description for filter scada symbols --- .../data/json/system/scada_symbols/long-bottom-filter.svg | 4 ++-- .../main/data/json/system/scada_symbols/long-top-filter.svg | 4 ++-- .../main/data/json/system/scada_symbols/short-top-filter.svg | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg index 689851529d..ed6eb25bf0 100644 --- a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg @@ -1,7 +1,7 @@ { - "title": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Long bottom filter", + "title": "Long bottom filter", + "description": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg index 9b287cbe46..d23059de37 100644 --- a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Title\nLong top filter\n", + "title": "Long top filter", + "description": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg index 0a77c78f00..0cb6da475c 100644 --- a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.", - "description": "Short top filter", + "title": "Short top filter", + "description": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], From b9327244531c2d8f54526bd88d741d69d6743b02 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva <irina.m2004@icloud.com> Date: Fri, 14 Mar 2025 16:37:57 +0200 Subject: [PATCH 03/14] added black box tests for cf --- .../server/msa/TestRestClient.java | 38 +- .../server/msa/cf/CalculatedFieldTest.java | 368 ++++++++++++++++++ .../msa/connectivity/HttpClientTest.java | 2 +- .../msa/connectivity/MqttClientTest.java | 6 +- .../connectivity/MqttGatewayClientTest.java | 8 +- 5 files changed, 409 insertions(+), 13 deletions(-) create mode 100644 msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java index d74438a428..aafe93f9f3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestRestClient.java @@ -28,6 +28,7 @@ import io.restassured.internal.ValidatableResponseImpl; import io.restassured.path.json.JsonPath; import io.restassured.response.ValidatableResponse; import io.restassured.specification.RequestSpecification; +import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -40,10 +41,12 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetProfile; +import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; @@ -146,6 +149,14 @@ public class TestRestClient { .as(ObjectNode.class); } + public CalculatedField postCalculatedField(CalculatedField calculatedField) { + return given().spec(requestSpec).body(calculatedField) + .post("/api/calculatedField") + .then() + .statusCode(HTTP_OK) + .extract() + .as(CalculatedField.class); + } public Device getDeviceByName(String deviceName) { return given().spec(requestSpec).pathParam("deviceName", deviceName) @@ -212,9 +223,16 @@ public class TestRestClient { .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); } - public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { + public ValidatableResponse deleteCalculatedFieldIfExists(CalculatedFieldId calculatedFieldId) { + return given().spec(requestSpec) + .delete("/api/calculatedField/{calculatedFieldId}", calculatedFieldId.getId()) + .then() + .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); + } + + public ValidatableResponse postTelemetryAttribute(EntityId entityId, String scope, JsonNode attribute) { return given().spec(requestSpec).body(attribute) - .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) + .post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityId.getEntityType(), entityId.getId(), scope) .then() .statusCode(HTTP_OK); } @@ -237,6 +255,15 @@ public class TestRestClient { .as(JsonNode.class); } + public JsonNode getAttributes(EntityId entityId, AttributeScope scope, String keys) { + return given().spec(requestSpec) + .get("/api/plugins/telemetry/{entityType}/{entityId}/values/attributes/{scope}?keys={keys}", entityId.getEntityType(), entityId.getId(), scope, keys) + .then() + .statusCode(HTTP_OK) + .extract() + .as(JsonNode.class); + } + public JsonNode getLatestTelemetry(EntityId entityId) { return given().spec(requestSpec) .get("/api/plugins/telemetry/" + entityId.getEntityType().name() + "/" + entityId.getId() + "/values/timeseries") @@ -640,6 +667,7 @@ public class TestRestClient { .then() .statusCode(anyOf(is(HTTP_OK), is(HTTP_BAD_REQUEST))); } + public void deleteDeviceProfileIfExists(DeviceProfile deviceProfile) { given().spec(requestSpec) .delete("/api/deviceProfile/" + deviceProfile.getId().getId().toString()) @@ -653,11 +681,11 @@ public class TestRestClient { .get("/api/tenant/devices?deviceName={deviceName}") .then() .statusCode(anyOf(is(HTTP_OK), is(HTTP_NOT_FOUND))); - if(((ValidatableResponseImpl) response).extract().response().getStatusCode()==HTTP_OK){ - return response.extract() + if (((ValidatableResponseImpl) response).extract().response().getStatusCode() == HTTP_OK) { + return response.extract() .as(Device.class); } else { - return null; + return null; } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java new file mode 100644 index 0000000000..63faed41ab --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -0,0 +1,368 @@ +/** + * Copyright © 2016-2025 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.msa.cf; + +import com.fasterxml.jackson.databind.JsonNode; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.debug.DebugSettings; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.id.AssetProfileId; +import org.thingsboard.server.common.data.id.DeviceProfileId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.msa.AbstractContainerTest; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; +import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; + +public class CalculatedFieldTest extends AbstractContainerTest { + + public final int TIMEOUT = 60; + public final int POLL_INTERVAL = 1; + + private final String deviceToken = "zmzURIVRsq3lvnTP2XBE"; + + private final String exampleScript = "var avgTemperature = temperature.mean(); // Get average temperature\n" + + " var temperatureK = (avgTemperature - 32) * (5 / 9) + 273.15; // Convert Fahrenheit to Kelvin\n" + + "\n" + + " // Estimate air pressure based on altitude\n" + + " var pressure = 101325 * Math.pow((1 - 2.25577e-5 * altitude), 5.25588);\n" + + "\n" + + " // Air density formula\n" + + " var airDensity = pressure / (287.05 * temperatureK);\n" + + "\n" + + " return {\n" + + " \"airDensity\": airDensity\n" + + " };"; + + private TenantId tenantId; + private UserId tenantAdminId; + private DeviceProfileId deviceProfileId; + private AssetProfileId assetProfileId; + private Device device; + private Asset asset; + + @BeforeClass + public void beforeClass() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + + tenantId = testRestClient.postTenant(EntityPrototypes.defaultTenantPrototype("Tenant")).getId(); + tenantAdminId = testRestClient.createUserAndLogin(defaultTenantAdmin(tenantId, "tenantAdmin@thingsboard.org"), "tenant"); + + deviceProfileId = testRestClient.postDeviceProfile(defaultDeviceProfile("Device Profile 1")).getId(); + device = testRestClient.postDevice(deviceToken, createDevice("Device 1", deviceProfileId)); + + assetProfileId = testRestClient.postAssetProfile(defaultAssetProfile("Asset Profile 1")).getId(); + asset = testRestClient.postAsset(createAsset("Asset 1", assetProfileId)); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + testRestClient.postTelemetryAttribute(device.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"deviceTemperature\":40}")); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.32}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":72.86}")); + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperatureInF\":73.58}")); + + testRestClient.postTelemetryAttribute(asset.getId(), DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"altitude\":1035}")); + } + + @BeforeMethod + public void beforeMethod() { + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + } + + @AfterClass + public void afterClass() { + testRestClient.resetToken(); + testRestClient.login("sysadmin@thingsboard.org", "sysadmin"); + testRestClient.deleteTenant(tenantId); + } + + @Test + public void testPerformInitialCalculationForSimpleType() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigArgument() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + Argument savedArgument = savedCalculatedField.getConfiguration().getArguments().get("T"); + savedArgument.setRefEntityKey(new ReferencedEntityKey("deviceTemperature", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF argument -> perform calculation with new argument").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("104.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigOutput() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + Output savedOutput = savedCalculatedField.getConfiguration().getOutput(); + savedOutput.setType(OutputType.ATTRIBUTES); + savedOutput.setScope(AttributeScope.SERVER_SCOPE); + savedOutput.setName("temperatureF"); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF output -> perform calculation with updated output").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode temperatureF = testRestClient.getAttributes(device.getId(), AttributeScope.SERVER_SCOPE, "temperatureF"); + assertThat(temperatureF).isNotNull(); + assertThat(temperatureF.get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testChangeConfigExpression() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + savedCalculatedField.setName("F to C"); + savedCalculatedField.getConfiguration().setExpression("(T - 32) / 1.8"); + testRestClient.postCalculatedField(savedCalculatedField); + + await().alias("update CF expression -> perform calculation with new expression").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("-3.89"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testTelemetryUpdated() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(); + + testRestClient.postTelemetry(deviceToken, JacksonUtil.toJsonNode("{\"temperature\":30}")); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("86.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testEntityIdIsProfile() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); + + await().alias("create CF -> perform initial calculation for device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(device.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("77.0"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + @Test + public void testEntityAddedAndDeleted() { + // login tenant admin + testRestClient.getAndSetUserToken(tenantAdminId); + + CalculatedField savedCalculatedField = createSimpleCalculatedField(deviceProfileId); + + String newDeviceToken = "mmmXRIVRsq9lbnTP2XBE"; + Device newDevice = testRestClient.postDevice(newDeviceToken, createDevice("Device 2", deviceProfileId)); + + await().alias("create device by profile -> perform initial calculation for new device by profile").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + // used default value since telemetry is not present + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); + }); + + DeviceProfile newDeviceProfile = testRestClient.postDeviceProfile(defaultDeviceProfile("Test Profile")); + newDevice.setDeviceProfileId(newDeviceProfile.getId()); + testRestClient.postDevice(newDeviceToken, newDevice); + + testRestClient.postTelemetry(newDeviceToken, JacksonUtil.toJsonNode("{\"temperature\":25}")); + + await().alias("update telemetry -> no updates").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + JsonNode fahrenheitTemp = testRestClient.getLatestTelemetry(newDevice.getId()); + assertThat(fahrenheitTemp).isNotNull(); + assertThat(fahrenheitTemp.get("fahrenheitTemp").get(0).get("value").asText()).isEqualTo("53.6"); + }); + + testRestClient.deleteCalculatedFieldIfExists(savedCalculatedField.getId()); + } + + private CalculatedField createSimpleCalculatedField() { + return createSimpleCalculatedField(device.getId()); + } + + private CalculatedField createSimpleCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("C to F" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + argument.setDefaultValue("12"); // not used because real telemetry value in db is present + config.setArguments(Map.of("T", argument)); + + config.setExpression("(T * 9/5) + 32"); + + Output output = new Output(); + output.setName("fahrenheitTemp"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(2); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return testRestClient.postCalculatedField(calculatedField); + } + + private CalculatedField createScriptCalculatedField() { + return createScriptCalculatedField(device.getId()); + } + + private CalculatedField createScriptCalculatedField(EntityId entityId) { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(entityId); + calculatedField.setType(CalculatedFieldType.SCRIPT); + calculatedField.setName("Air density" + RandomStringUtils.randomAlphabetic(5)); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + argument1.setRefEntityId(asset.getId()); + ReferencedEntityKey refEntityKey1 = new ReferencedEntityKey("altitude", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE); + argument1.setRefEntityKey(refEntityKey1); + config.setArguments(Map.of("altitude", argument1)); + Argument argument2 = new Argument(); + ReferencedEntityKey refEntityKey2 = new ReferencedEntityKey("temperatureInF", ArgumentType.TS_ROLLING, null); + argument2.setRefEntityKey(refEntityKey2); + config.setArguments(Map.of("temperature", argument2)); + + config.setExpression("return {\"airDensity\": 5};"); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + return testRestClient.postCalculatedField(calculatedField); + } + + private Device createDevice(String name, DeviceProfileId deviceProfileId) { + Device device = new Device(); + device.setName(name); + device.setType("default"); + device.setDeviceProfileId(deviceProfileId); + DeviceData deviceData = new DeviceData(); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + device.setDeviceData(deviceData); + return device; + } + + private Asset createAsset(String name, AssetProfileId assetProfileId) { + Asset asset = new Asset(); + asset.setName(name); + asset.setAssetProfileId(assetProfileId); + return asset; + } + +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java index b96ea386d2..4e28340f9d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/HttpClientTest.java @@ -77,7 +77,7 @@ public class HttpClientTest extends AbstractContainerTest { assertThat(accessToken).isNotNull(); JsonNode sharedAttribute = mapper.readTree(createPayload().toString()); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); JsonNode clientAttribute = mapper.readTree(createPayload().toString()); testRestClient.postAttribute(accessToken, clientAttribute); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 00e496327c..ebdfb4e3c9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -212,7 +212,7 @@ public class MqttClientTest extends AbstractContainerTest { String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); // Subscribe to attributes response mqttClient.on("v1/devices/me/attributes/response/+", listener, MqttQoS.AT_LEAST_ONCE).get(); @@ -255,7 +255,7 @@ public class MqttClientTest extends AbstractContainerTest { sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); JsonNode sharedAttribute = mapper.readTree(sharedAttributes.toString()); - testRestClient.postTelemetryAttribute(DataConstants.DEVICE, device.getId(), SHARED_SCOPE, sharedAttribute); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, sharedAttribute); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) @@ -265,7 +265,7 @@ public class MqttClientTest extends AbstractContainerTest { JsonObject updatedSharedAttributes = new JsonObject(); String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); - testRestClient.postTelemetryAttribute(DEVICE, device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); + testRestClient.postTelemetryAttribute(device.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get(sharedAttributeName).asText()) diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index b2335ae2f7..cc587fbbd5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -184,7 +184,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { mqttClient.on("v1/gateway/attributes/response", listener, MqttQoS.AT_LEAST_ONCE).get(); - testRestClient.postTelemetryAttribute(DataConstants.DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); var event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); JsonObject requestData = new JsonObject(); @@ -266,7 +266,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Subscribe for attribute update event mqttClient.on("v1/gateway/attributes", listener, MqttQoS.AT_LEAST_ONCE).get(); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent sharedAttributeEvent = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); // Catch attribute update event @@ -299,7 +299,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewaySharedAttributeValue.addProperty("device", createdDevice.getName()); gatewaySharedAttributeValue.add("data", sharedAttributes); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(sharedAttributes.toString())); MqttEvent event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) @@ -314,7 +314,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { gatewayUpdatedSharedAttributeValue.addProperty("device", createdDevice.getName()); gatewayUpdatedSharedAttributeValue.add("data", updatedSharedAttributes); - testRestClient.postTelemetryAttribute(DEVICE, createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); + testRestClient.postTelemetryAttribute(createdDevice.getId(), SHARED_SCOPE, mapper.readTree(updatedSharedAttributes.toString())); event = listener.getEvents().poll(10 * timeoutMultiplier, TimeUnit.SECONDS); assertThat(mapper.readValue(Objects.requireNonNull(event).getMessage(), JsonNode.class).get("data").get(sharedAttributeName).asText()) .isEqualTo(updatedSharedAttributeValue); From d2abecdfae1d101bacf506c1ceaca59fc7f54115 Mon Sep 17 00:00:00 2001 From: Igor Kulikov <ikulikov@thingsboard.io> Date: Fri, 14 Mar 2025 18:47:25 +0200 Subject: [PATCH 04/14] UI: tailwindcss: set experimental flag optimizeUniversalDefaults to true (to not generate unncessary css variables). --- ui-ngx/tailwind.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui-ngx/tailwind.config.js b/ui-ngx/tailwind.config.js index 5933ab8a14..e84ba47abc 100644 --- a/ui-ngx/tailwind.config.js +++ b/ui-ngx/tailwind.config.js @@ -180,5 +180,8 @@ module.exports = { preflight: false }, plugins: [], + experimental: { + optimizeUniversalDefaults: true + } } From ab8d906791fcdcca0d73b41a580bb8556b7f7b51 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko <Dgerik2015@gmail.com> Date: Mon, 17 Mar 2025 07:35:25 +0200 Subject: [PATCH 05/14] UI: Hotfix for header action --- .../widget/widget-container.component.html | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index 67664beb84..f6258551fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -104,16 +104,6 @@ <ng-template #widgetHeaderActionButton let-action="action" let-last="last"> @switch (action.buttonType) { - @case (widgetHeaderActionButtonType.icon) { - <button mat-icon-button - [style]="action.customButtonStyle" - [class.!hidden]="isEdit" - (click)="action.onAction($event)" - matTooltip="{{ action.displayName }}" - matTooltipPosition="above"> - <tb-icon [style.color]="action.buttonColor">{{ action.icon }}</tb-icon> - </button> - } @case (widgetHeaderActionButtonType.miniFab) { <button mat-mini-fab [style]="action.customButtonStyle" @@ -175,5 +165,15 @@ <span [style.color]="action.buttonColor">{{ action.displayName }}</span> </button> } + @default { + <button mat-icon-button + [style]="action.customButtonStyle" + [class.!hidden]="isEdit" + (click)="action.onAction($event)" + matTooltip="{{ action.displayName }}" + matTooltipPosition="above"> + <tb-icon [style.color]="action.buttonColor">{{ action.icon }}</tb-icon> + </button> + } } </ng-template> From a6e93d7be37588bf026cd4603163ae7a242df0e0 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva <irina.m2004@icloud.com> Date: Mon, 17 Mar 2025 09:37:23 +0200 Subject: [PATCH 06/14] updated cf help page --- .../merge-functions/merge_all_output.md | 25 +++ .../merge-functions/merge_all_usage.md | 5 + .../examples/merge-functions/merge_input.md | 48 ++++++ .../examples/merge-functions/merge_output.md | 25 +++ .../examples/merge-functions/merge_usage.md | 5 + .../en_US/calculated-field/expression_fn.md | 157 +++--------------- 6 files changed, 131 insertions(+), 134 deletions(-) create mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_output.md create mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_usage.md create mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_input.md create mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_output.md create mode 100644 ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_usage.md diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_output.md b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_output.md new file mode 100644 index 0000000000..b3b15f8f3d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_output.md @@ -0,0 +1,25 @@ +#### Output: + +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "values": [76.0, 46.0, 1023.0] + }, { + "ts": 1741357056144, + "values": [76.0, 46.0, 1026.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0, 1026.0] + }, { + "ts": 1741357147391, + "values": [77.0, 46.0, 1025.0] + }] + } +} +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_usage.md b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_usage.md new file mode 100644 index 0000000000..89d3f0c84f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_all_usage.md @@ -0,0 +1,5 @@ +#### Usage: + +```javascript +var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_input.md b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_input.md new file mode 100644 index 0000000000..f41b4ae2da --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_input.md @@ -0,0 +1,48 @@ +#### Assuming the following arguments and their values: + +```json +{ + "humidity": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356882759, + "value": 43 + }, { + "ts": 1741356918779, + "value": 46 + }] + }, + "pressure": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741357047945, + "value": 1023 + }, { + "ts": 1741357056144, + "value": 1026 + }, { + "ts": 1741357147391, + "value": 1025 + }] + }, + "temperature": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "value": 76 + }, { + "ts": 1741357063689, + "value": 77 + }] + } +} +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_output.md b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_output.md new file mode 100644 index 0000000000..545ef9994c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_output.md @@ -0,0 +1,25 @@ +#### Output: + +```json +{ + "mergedData": { + "timeWindow": { + "startTs": 1741356332086, + "endTs": 1741357232086 + }, + "values": [{ + "ts": 1741356874943, + "values": [76.0, "NaN"] + }, { + "ts": 1741356882759, + "values": [76.0, 43.0] + }, { + "ts": 1741356918779, + "values": [76.0, 46.0] + }, { + "ts": 1741357063689, + "values": [77.0, 46.0] + }] + } +} +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_usage.md b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_usage.md new file mode 100644 index 0000000000..a126196ee9 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/calculated-field/examples/merge-functions/merge_usage.md @@ -0,0 +1,5 @@ +#### Usage: + +```javascript +var mergedData = temperature.merge(humidity, { ignoreNaN: false }); +``` diff --git a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md index f46923ebd3..373c50e2c6 100644 --- a/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md +++ b/ui-ngx/src/assets/help/en_US/calculated-field/expression_fn.md @@ -1,9 +1,8 @@ ## Calculated Field TBEL Script Function -The **calculate()** function is a user-defined script that enables custom calculations using [TBEL](\${siteBaseUrl}/docs\${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. +The **calculate()** function is a user-defined script that enables custom calculations using [TBEL](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/tbel/) on telemetry and attribute data. It receives arguments configured in the calculated field setup, along with an additional `ctx` object that provides access to all arguments. - ### Function Signature ```javascript @@ -18,8 +17,6 @@ There are three types of arguments supported in the calculated field configurati These arguments are single values and may be of type: boolean, int64 (long), double, string, or JSON. -Attribute and Latest telemetry are single value arguments that may be one of: boolean, int64 (long), double, string and JSON. - **Example: Convert Temperature from Fahrenheit to Celsius** ```javascript @@ -95,25 +92,24 @@ for(var i = 0; i < temperature.values.size; i++) { sum += temperature.values[i].value; } // use built-in function to calculate the sum -sum = t.sum(); +sum = temperature.sum(); ``` ##### Built-in Methods for Rolling Arguments Time series rolling arguments support built-in functions for calculations. These functions accept an optional `ignoreNaN` boolean parameter. -| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | -|------------|-----------------------------------------------------|---------------------------------------------| -| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `mean()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | -| `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | -| `count()` | Counts values, ignoring NaN values. | Counts all values, including NaN. | -| `last()` | Returns the most recent value, skipping NaN values. | Returns the last value, even if it is NaN. | -| `first()` | Returns the oldest value, skipping NaN values. | Returns the first value, even if it is NaN. | -| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | - +| Method | Default Behavior (`ignoreNaN = true`) | Alternative (`ignoreNaN = false`) | +|-----------------|-----------------------------------------------------|---------------------------------------------| +| `max()` | Returns the highest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `min()` | Returns the lowest value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `mean(), avg()` | Computes the average value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `std()` | Calculates the standard deviation, ignoring NaN. | Returns NaN if any NaN values exist. | +| `median()` | Returns the median value, ignoring NaN values. | Returns NaN if any NaN values exist. | +| `count()` | Counts values, ignoring NaN values. | Counts all values, including NaN. | +| `last()` | Returns the most recent value, skipping NaN values. | Returns the last value, even if it is NaN. | +| `first()` | Returns the oldest value, skipping NaN values. | Returns the first value, even if it is NaN. | +| `sum()` | Computes the total sum, ignoring NaN values. | Returns NaN if any NaN values exist. | Usage example: @@ -141,7 +137,7 @@ function calculate(ctx, altitude, temperature) { var airDensity = pressure / (287.05 * temperatureK); return { - "airDensity": airDensity + "airDensity": toFixed(airDensity, 2) }; } ``` @@ -150,123 +146,16 @@ function calculate(ctx, altitude, temperature) { Time series rolling arguments can be **merged** to align timestamps across multiple datasets. -| Method | Description | Parameters | Returns | -|:-----------------------------|:--------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------| -| `merge(other, settings)` | Merges with another rolling argument. Aligns timestamps and filling missing values with the previous available value. | <ul><li>`other` (another rolling argument)</li><li>`settings` (optional) - configuration object, supports:<ul><br/><li>`ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.</li><li>`timeWindow` (object, default {}) - defines a custom time window for filtering merged values.</li></ul></li></ul>| Merged object with timeWindow and aligned values. | -| `mergeAll(others, settings)` | Merges with multiple rolling arguments. Aligns timestamps and filling missing values with the previous available value. | <ul><li>`others` (array of rolling arguments)</li><li>`settings` (optional) - configuration object, supports:<ul><br/><li>`ignoreNaN` (boolean, default true) - controls whether NaN values should be ignored.</li><li>`timeWindow` (object, default {}) - defines a custom time window for filtering merged values.</li></ul></li></ul>| Merged object with timeWindow and aligned values.| - -Assuming the following arguments and their values: - -```json -{ - "humidity": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356882759, - "value": 43 - }, { - "ts": 1741356918779, - "value": 46 - }] - }, - "pressure": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741357047945, - "value": 1023 - }, { - "ts": 1741357056144, - "value": 1026 - }, { - "ts": 1741357147391, - "value": 1025 - }] - }, - "temperature": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356874943, - "value": 76 - }, { - "ts": 1741357063689, - "value": 77 - }] - } -} -``` - -**Usage:** - -```javascript -var mergedData = temperature.merge(humidity, { ignoreNaN: false }); -``` - -**Output:** - -```json -{ - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741356874943, - "values": [76.0, "NaN"] - }, { - "ts": 1741356882759, - "values": [76.0, 43.0] - }, { - "ts": 1741356918779, - "values": [76.0, 46.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0] - }] - } -} -``` - -**Usage:** +| Method | Description | Returns | Example | +|:-----------------------------|:--------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `merge(other, settings)` | Merges with another rolling argument. Aligns timestamps and filling missing values with the previous available value. | Merged object with `timeWindow` and aligned values. | <span tb-help-popup="calculated-field/examples/merge-functions/merge_input" tb-help-popup-placement="top" trigger-text="Input"></span> <br> <span tb-help-popup="calculated-field/examples/merge-functions/merge_usage" tb-help-popup-placement="top" trigger-text="Usage"></span> <br> <span tb-help-popup="calculated-field/examples/merge-functions/merge_output" tb-help-popup-placement="top" trigger-text="Output"></span> | +| `mergeAll(others, settings)` | Merges multiple rolling arguments. Aligns timestamps and filling missing values with the previous available value. | Merged object with `timeWindow` and aligned values. | <span tb-help-popup="calculated-field/examples/merge-functions/merge_input" tb-help-popup-placement="top" trigger-text="Input"></span> <br> <span tb-help-popup="calculated-field/examples/merge-functions/merge_all_usage" tb-help-popup-placement="top" trigger-text="Usage"></span> <br> <span tb-help-popup="calculated-field/examples/merge-functions/merge_all_output" tb-help-popup-placement="top" trigger-text="Output"></span> | -```javascript -var mergedData = temperature.mergeAll([humidity, pressure], { ignoreNaN: true }); -``` - -**Output:** - -```json -{ - "mergedData": { - "timeWindow": { - "startTs": 1741356332086, - "endTs": 1741357232086 - }, - "values": [{ - "ts": 1741357047945, - "values": [76.0, 46.0, 1023.0] - }, { - "ts": 1741357056144, - "values": [76.0, 46.0, 1026.0] - }, { - "ts": 1741357063689, - "values": [77.0, 46.0, 1026.0] - }, { - "ts": 1741357147391, - "values": [77.0, 46.0, 1025.0] - }] - } -} -``` +##### Parameters +| Parameter | Description | +|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `other` or `others` | Another rolling argument or array of rolling arguments to merge with. | +| `settings`(optional) | Configuration object that supports: <ul><li>`ignoreNaN` - controls whether NaN values should be ignored.</li> <li>`timeWindow` - defines a custom time window.</li></ul> | **Example: Freezer temperature analysis** From 4f4ff40d6fe34a7be1064942da9175eb23069f88 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva <irina.m2004@icloud.com> Date: Mon, 17 Mar 2025 12:37:47 +0200 Subject: [PATCH 07/14] fixed issue when argument is tenant --- .../service/cf/DefaultCalculatedFieldQueueService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java index ba4be6ace6..8289e4db42 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldQueueService.java @@ -128,6 +128,9 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS private void checkEntityAndPushToQueue(TenantId tenantId, EntityId entityId, Predicate<CalculatedFieldCtx> mainEntityFilter, Predicate<CalculatedFieldCtx> linkedEntityFilter, Supplier<ToCalculatedFieldMsg> msg, FutureCallback<Void> callback) { + if (EntityType.TENANT.equals(entityId.getEntityType())) { + tenantId = (TenantId) entityId; + } boolean send = checkEntityForCalculatedFields(tenantId, entityId, mainEntityFilter, linkedEntityFilter); if (send) { clusterService.pushMsgToCalculatedFields(tenantId, entityId, msg.get(), wrap(callback)); @@ -221,6 +224,10 @@ public class DefaultCalculatedFieldQueueService implements CalculatedFieldQueueS private CalculatedFieldTelemetryMsgProto.Builder buildTelemetryMsgProto(TenantId tenantId, EntityId entityId, List<CalculatedFieldId> calculatedFieldIds, UUID tbMsgId, TbMsgType tbMsgType) { CalculatedFieldTelemetryMsgProto.Builder telemetryMsg = CalculatedFieldTelemetryMsgProto.newBuilder(); + if (EntityType.TENANT.equals(entityId.getEntityType())) { + tenantId = (TenantId) entityId; + } + telemetryMsg.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); telemetryMsg.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); From cb4c5fce93d94182943d530ae5e07fd9737db4d1 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko <ybondarenko@thingsboard.io> Date: Mon, 17 Mar 2025 12:41:01 +0100 Subject: [PATCH 08/14] revert accidentally deleted api-usage clearing --- .../server/queue/usagestats/DefaultTbApiUsageReportClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java index 3cea112327..715020dc7c 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java @@ -123,6 +123,7 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient { .setValue(value); statsMsg.addValues(statsItem.build()); }); + statsForKey.clear(); } Map<TopicPartitionInfo, List<UsageStatsServiceMsg>> reportStatsPerTpi = new HashMap<>(); From e97accadab5b8056775b5dff8b716771ecd4d278 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov <viacheslavklimov11@gmail.com> Date: Mon, 17 Mar 2025 16:30:31 +0200 Subject: [PATCH 09/14] Fixes for CF consumers and repartitioning; refactoring --- .../AbstractCalculatedFieldStateService.java | 35 ++++-- .../cf/CalculatedFieldStateService.java | 7 +- .../KafkaCalculatedFieldStateService.java | 28 ++--- .../RocksDBCalculatedFieldStateService.java | 21 ++-- ...faultTbCalculatedFieldConsumerService.java | 42 +++++-- .../EdqsEntityQueryControllerTest.java | 2 +- .../controller/TenantControllerTest.java | 50 +++++--- .../server/edqs/processor/EdqsProcessor.java | 3 + .../edqs/state/KafkaEdqsStateService.java | 13 +- .../PartitionedQueueConsumerManager.java | 21 +++- .../common/consumer/QueueStateService.java | 98 --------------- .../queue/common/consumer/QueueTaskType.java | 2 +- .../consumer/TbQueueConsumerManagerTask.java | 7 ++ .../state/DefaultQueueStateService.java | 27 +++++ .../common/state/KafkaQueueStateService.java | 81 +++++++++++++ .../queue/common/state/QueueStateService.java | 114 ++++++++++++++++++ .../queue/discovery/HashPartitionService.java | 68 ++++++----- .../server/queue/discovery/QueueKey.java | 3 - .../server/queue/kafka/TbKafkaAdmin.java | 2 +- 19 files changed, 420 insertions(+), 204 deletions(-) delete mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index 0163d65d98..91c08ab6e0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -19,14 +19,20 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldStateRestoreMsg; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.exception.CalculatedFieldStateException; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.common.state.QueueStateService; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @@ -35,12 +41,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF @Autowired private ActorSystemContext actorSystemContext; - protected PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer; - - @Override - public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) { - this.eventConsumer = eventConsumer; - } + protected QueueStateService<TbProtoQueueMsg<ToCalculatedFieldMsg>, TbProtoQueueMsg<CalculatedFieldStateProto>> stateService; @Override public final void persistState(CalculatedFieldEntityCtxId stateId, CalculatedFieldState state, TbCallback callback) { @@ -69,4 +70,24 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF actorSystemContext.tell(new CalculatedFieldStateRestoreMsg(id, state)); } + @Override + public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + stateService.update(queueKey, partitions); + } + + @Override + public void delete(Set<TopicPartitionInfo> partitions) { + stateService.delete(partitions); + } + + @Override + public Set<TopicPartitionInfo> getPartitions() { + return stateService.getPartitions().values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } + + @Override + public void stop() { + stateService.stop(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java index 109f13f183..d0b34f18e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldStateService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.exception.CalculatedFieldStateException; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; @@ -34,7 +35,11 @@ public interface CalculatedFieldStateService { void removeState(CalculatedFieldEntityCtxId stateId, TbCallback callback); - void restore(Set<TopicPartitionInfo> partitions); + void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions); + + void delete(Set<TopicPartitionInfo> partitions); + + Set<TopicPartitionInfo> getPartitions(); void stop(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index 557768e9c9..76cd2cfcf3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -30,12 +30,13 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgHeaders; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.state.KafkaQueueStateService; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; -import org.thingsboard.server.queue.common.consumer.QueueStateService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @@ -43,10 +44,12 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.*; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToUuid; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.stringToBytes; +import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.uuidToBytes; @Service @RequiredArgsConstructor @@ -56,22 +59,19 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta private final TbRuleEngineQueueFactory queueFactory; private final PartitionService partitionService; + private final TbQueueAdmin queueAdmin; @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; - private PartitionedQueueConsumerManager<TbProtoQueueMsg<CalculatedFieldStateProto>> stateConsumer; private TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>> stateProducer; - private QueueStateService<TbProtoQueueMsg<ToCalculatedFieldMsg>, TbProtoQueueMsg<CalculatedFieldStateProto>> queueStateService; private final AtomicInteger counter = new AtomicInteger(); @Override public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) { - super.init(eventConsumer); - var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_STATES_QUEUE_NAME); - this.stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<CalculatedFieldStateProto>>create() + PartitionedQueueConsumerManager<TbProtoQueueMsg<CalculatedFieldStateProto>> stateConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<CalculatedFieldStateProto>>create() .queueKey(queueKey) .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) @@ -94,13 +94,13 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta } }) .consumerCreator((config, partitionId) -> queueFactory.createCalculatedFieldStateConsumer()) + .queueAdmin(queueAdmin) .consumerExecutor(eventConsumer.getConsumerExecutor()) .scheduler(eventConsumer.getScheduler()) .taskExecutor(eventConsumer.getTaskExecutor()) .build(); + super.stateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); this.stateProducer = (TbKafkaProducerTemplate<TbProtoQueueMsg<CalculatedFieldStateProto>>) queueFactory.createCalculatedFieldStateProducer(); - this.queueStateService = new QueueStateService<>(); - this.queueStateService.init(stateConsumer, super.eventConsumer); } @Override @@ -132,11 +132,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta doPersist(stateId, null, callback); } - @Override - public void restore(Set<TopicPartitionInfo> partitions) { - queueStateService.update(partitions); - } - private void putStateId(TbQueueMsgHeaders headers, CalculatedFieldEntityCtxId stateId) { headers.put("tenantId", uuidToBytes(stateId.tenantId().getId())); headers.put("cfId", uuidToBytes(stateId.cfId().getId())); @@ -153,8 +148,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Override public void stop() { - stateConsumer.stop(); - stateConsumer.awaitStop(); + super.stop(); stateProducer.stop(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index a508eecada..0eaa506dfd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -22,7 +22,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.common.state.DefaultQueueStateService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -37,7 +42,10 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS private final CfRocksDb cfRocksDb; - private boolean initialized; + @Override + public void init(PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer) { + super.stateService = new DefaultQueueStateService<>(eventConsumer); + } @Override protected void doPersist(CalculatedFieldEntityCtxId stateId, CalculatedFieldStateProto stateMsgProto, TbCallback callback) { @@ -52,8 +60,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS } @Override - public void restore(Set<TopicPartitionInfo> partitions) { - if (!this.initialized) { + public void restore(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + if (stateService.getPartitions().isEmpty()) { cfRocksDb.forEach((key, value) -> { try { processRestoredState(CalculatedFieldStateProto.parseFrom(value)); @@ -61,13 +69,8 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS log.error("[{}] Failed to process restored state", key, e); } }); - this.initialized = true; } - eventConsumer.update(partitions); - } - - @Override - public void stop() { + super.restore(queueKey, partitions); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 04aab6b39d..fe42684b2b 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -18,24 +18,31 @@ package org.thingsboard.server.service.queue; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.actors.calculatedField.CalculatedFieldLinkedTelemetryMsg; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; @@ -54,6 +61,7 @@ import org.thingsboard.server.service.queue.processing.IdMsgPair; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -73,10 +81,9 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer private long packProcessingTimeout; private final TbRuleEngineQueueFactory queueFactory; + private final TbQueueAdmin queueAdmin; private final CalculatedFieldStateService stateService; - private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer; - public DefaultTbCalculatedFieldConsumerService(TbRuleEngineQueueFactory tbQueueFactory, ActorSystemContext actorContext, TbDeviceProfileCache deviceProfileCache, @@ -87,10 +94,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer ApplicationEventPublisher eventPublisher, JwtSettingsService jwtSettingsService, CalculatedFieldCache calculatedFieldCache, + TbQueueAdmin queueAdmin, CalculatedFieldStateService stateService) { super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.queueFactory = tbQueueFactory; + this.queueAdmin = queueAdmin; this.stateService = stateService; } @@ -99,12 +108,13 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer super.init("tb-cf"); var queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME); - this.eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create() + PartitionedQueueConsumerManager<TbProtoQueueMsg<ToCalculatedFieldMsg>> eventConsumer = PartitionedQueueConsumerManager.<TbProtoQueueMsg<ToCalculatedFieldMsg>>create() .queueKey(queueKey) .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) + .queueAdmin(queueAdmin) .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) @@ -124,9 +134,12 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void onTbApplicationEvent(PartitionChangeEvent event) { - var partitions = event.getCfPartitions(); try { - stateService.restore(partitions); + event.getNewPartitions().forEach((queueKey, partitions) -> { + if (queueKey.getQueueName().equals(DataConstants.CF_QUEUE_NAME)) { + stateService.restore(queueKey, partitions); + } + }); // eventConsumer's partitions will be updated by stateService // Cleanup old entities after corresponding consumers are stopped. @@ -212,6 +225,21 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer } } + @EventListener + public void handleComponentLifecycleEvent(ComponentLifecycleMsg event) { + if (event.getEntityId().getEntityType() == EntityType.TENANT) { + if (event.getEvent() == ComponentLifecycleEvent.DELETED) { + Set<TopicPartitionInfo> partitions = stateService.getPartitions(); + if (CollectionUtils.isEmpty(partitions)) { + return; + } + stateService.delete(partitions.stream() + .filter(tpi -> tpi.getTenantId().isPresent() && tpi.getTenantId().get().equals(event.getTenantId())) + .collect(Collectors.toSet())); + } + } + } + private void forwardToActorSystem(CalculatedFieldTelemetryMsgProto msg, TbCallback callback) { var tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB()); var entityId = EntityIdFactory.getByTypeAndUuid(msg.getEntityType(), new UUID(msg.getEntityIdMSB(), msg.getEntityIdLSB())); @@ -232,9 +260,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer @Override protected void stopConsumers() { super.stopConsumers(); - eventConsumer.stop(); - eventConsumer.awaitStop(); - stateService.stop(); + stateService.stop(); // eventConsumer will be stopped by stateService } } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index ce5221ab89..153ec2d26f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -35,7 +35,7 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { // "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=10.7.1.254:9092", +// "queue.kafka.bootstrap.servers=10.7.2.107:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api.supported=true", "queue.edqs.api.auto_enable=true", diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index 3f2434c4ec..4fd613dc1d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -664,34 +664,38 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); - return !tpi.getTenantId().get().isSysTenantId(); - }); - TopicPartitionInfo tpi = new TopicPartitionInfo(MAIN_QUEUE_TOPIC, tenantId, 0, false); - String isolatedTopic = tpi.getFullTopicName(); - TbMsg expectedMsg = publishTbMsg(tenantId, tpi); + List<TopicPartitionInfo> isolatedTpis = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + List<TopicPartitionInfo> newTpis = new ArrayList<>(); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId)); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId)); + return newTpis; + }, newTpis -> newTpis.stream().allMatch(newTpi -> newTpi.getTenantId().get().equals(tenantId))); + TbMsg expectedMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); awaitTbMsg(tbMsg -> tbMsg.getId().equals(expectedMsg.getId()), 10000); // to wait for consumer start loginSysAdmin(); tenantProfile.setIsolatedTbRuleEngine(false); tenantProfile.getProfileData().setQueueConfiguration(Collections.emptyList()); tenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); - await().atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId) - .getTenantId().get().isSysTenantId()); + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + TopicPartitionInfo newTpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId); + assertThat(newTpi.getTenantId()).hasValue(TenantId.SYS_TENANT_ID); + newTpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId); + assertThat(newTpi.getTenantId()).hasValue(TenantId.SYS_TENANT_ID); + }); List<UUID> submittedMsgs = new ArrayList<>(); long timeLeft = TimeUnit.SECONDS.toMillis(7); // based on topic-deletion-delay int msgs = 100; for (int i = 1; i <= msgs; i++) { - TbMsg tbMsg = publishTbMsg(tenantId, tpi); + TbMsg tbMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); submittedMsgs.add(tbMsg.getId()); Thread.sleep(timeLeft / msgs); } await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - verify(queueAdmin, times(1)).deleteTopic(eq(isolatedTopic)); + TopicPartitionInfo tpi = isolatedTpis.get(0); + // we only expect deletion of Rule Engine topic. for CF - the topic is left as is because queue draining is not supported + verify(queueAdmin, times(1)).deleteTopic(eq(tpi.getFullTopicName())); }); await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { @@ -719,12 +723,16 @@ public class TenantControllerTest extends AbstractControllerTest { savedDifferentTenant.setTenantProfileId(tenantProfile.getId()); savedDifferentTenant = saveTenant(savedDifferentTenant); TenantId tenantId = differentTenantId; - await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { - assertThat(partitionService.getMyPartitions(new QueueKey(ServiceType.TB_RULE_ENGINE, tenantId))).isNotNull(); - }); - TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId); - assertThat(tpi.getTenantId()).hasValue(tenantId); - TbMsg tbMsg = publishTbMsg(tenantId, tpi); + List<TopicPartitionInfo> isolatedTpis = await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { + List<TopicPartitionInfo> newTpis = new ArrayList<>(); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, MAIN_QUEUE_NAME, tenantId, tenantId)); + newTpis.add(partitionService.resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, tenantId)); + return newTpis; + }, newTpis -> newTpis.stream().allMatch(newTpi -> { + return newTpi.getTenantId().get().equals(tenantId) && + newTpi.isMyPartition(); + })); + TbMsg tbMsg = publishTbMsg(tenantId, isolatedTpis.get(0)); await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext).tell(argThat(msg -> { return msg instanceof QueueToRuleEngineMsg && ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); @@ -738,7 +746,9 @@ public class TenantControllerTest extends AbstractControllerTest { assertThatThrownBy(() -> partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, tenantId)) .isInstanceOf(TenantNotFoundException.class); - verify(queueAdmin).deleteTopic(eq(tpi.getFullTopicName())); + isolatedTpis.forEach(tpi -> { + verify(queueAdmin).deleteTopic(eq(tpi.getFullTopicName())); + }); }); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index c5696c08f7..e18c8171af 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -53,6 +53,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -91,6 +92,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>, private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; private final EdqsStateService stateService; + private final TbQueueAdmin queueAdmin; private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer; private TbQueueResponseTemplate<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> responseTemplate; @@ -141,6 +143,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>, consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) + .queueAdmin(queueAdmin) .consumerExecutor(consumersExecutor) .taskExecutor(taskExecutor) .scheduler(scheduler) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 80b6eebf5c..6d938d2976 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -30,10 +30,12 @@ import org.thingsboard.server.edqs.processor.EdqsProducer; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; -import org.thingsboard.server.queue.common.consumer.QueueStateService; +import org.thingsboard.server.queue.common.state.KafkaQueueStateService; +import org.thingsboard.server.queue.common.state.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; @@ -54,6 +56,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; + private final TbQueueAdmin queueAdmin; private final TopicService topicService; @Autowired @Lazy private EdqsProcessor edqsProcessor; @@ -89,13 +92,13 @@ public class KafkaEdqsStateService implements EdqsStateService { consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) + .queueAdmin(queueAdmin) .consumerExecutor(eventConsumer.getConsumerExecutor()) .taskExecutor(eventConsumer.getTaskExecutor()) .scheduler(eventConsumer.getScheduler()) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - queueStateService = new QueueStateService<>(); - queueStateService.init(stateConsumer, eventConsumer); + queueStateService = new KafkaQueueStateService<>(eventConsumer, stateConsumer); eventsToBackupConsumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdqsMsg>>builder() .name("edqs-events-to-backup-consumer") @@ -149,11 +152,11 @@ public class KafkaEdqsStateService implements EdqsStateService { @Override public void process(Set<TopicPartitionInfo> partitions) { - if (queueStateService.getPartitions() == null) { + if (queueStateService.getPartitions().isEmpty()) { eventsToBackupConsumer.subscribe(); eventsToBackupConsumer.launch(); } - queueStateService.update(partitions); + queueStateService.update(new QueueKey(ServiceType.EDQS), partitions); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index 1d68cc9c4e..f25a98adf4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -20,9 +20,11 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.AddPartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeletePartitionsTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.RemovePartitionsTask; import org.thingsboard.server.queue.discovery.QueueKey; @@ -36,17 +38,19 @@ import java.util.function.Consumer; public class PartitionedQueueConsumerManager<M extends TbQueueMsg> extends MainQueueConsumerManager<M, QueueConfig> { private final ConsumerPerPartitionWrapper consumerWrapper; + private final TbQueueAdmin queueAdmin; @Getter private final String topic; @Builder(builderMethodName = "create") // not to conflict with super.builder() public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor<M, QueueConfig> msgPackProcessor, - BiFunction<QueueConfig, Integer, TbQueueConsumer<M>> consumerCreator, + BiFunction<QueueConfig, Integer, TbQueueConsumer<M>> consumerCreator, TbQueueAdmin queueAdmin, ExecutorService consumerExecutor, ScheduledExecutorService scheduler, ExecutorService taskExecutor, Consumer<Throwable> uncaughtErrorHandler) { super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler); this.topic = topic; this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; + this.queueAdmin = queueAdmin; } @Override @@ -57,6 +61,17 @@ public class PartitionedQueueConsumerManager<M extends TbQueueMsg> extends MainQ } else if (task instanceof RemovePartitionsTask removePartitionsTask) { log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions()); consumerWrapper.removePartitions(removePartitionsTask.partitions()); + } else if (task instanceof DeletePartitionsTask deletePartitionsTask) { + log.info("[{}] Removing partitions and deleting topics: {}", queueKey, deletePartitionsTask.partitions()); + consumerWrapper.removePartitions(deletePartitionsTask.partitions()); + deletePartitionsTask.partitions().forEach(tpi -> { + String topic = tpi.getFullTopicName(); + try { + queueAdmin.deleteTopic(topic); + } catch (Throwable t) { + log.error("Failed to delete topic {}", topic, t); + } + }); } } @@ -72,4 +87,8 @@ public class PartitionedQueueConsumerManager<M extends TbQueueMsg> extends MainQ addTask(new RemovePartitionsTask(partitions)); } + public void delete(Set<TopicPartitionInfo> partitions) { + addTask(new DeletePartitionsTask(partitions)); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java deleted file mode 100644 index 8870ff2a2c..0000000000 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright © 2016-2025 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.queue.common.consumer; - -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.queue.TbQueueMsg; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; - -@Slf4j -public class QueueStateService<E extends TbQueueMsg, S extends TbQueueMsg> { - - private PartitionedQueueConsumerManager<S> stateConsumer; - private PartitionedQueueConsumerManager<E> eventConsumer; - - @Getter - private Set<TopicPartitionInfo> partitions; - private final Set<TopicPartitionInfo> partitionsInProgress = ConcurrentHashMap.newKeySet(); - private boolean initialized; - - private final ReadWriteLock partitionsLock = new ReentrantReadWriteLock(); - - public void init(PartitionedQueueConsumerManager<S> stateConsumer, PartitionedQueueConsumerManager<E> eventConsumer) { - this.stateConsumer = stateConsumer; - this.eventConsumer = eventConsumer; - } - - public void update(Set<TopicPartitionInfo> newPartitions) { - newPartitions = withTopic(newPartitions, stateConsumer.getTopic()); - var writeLock = partitionsLock.writeLock(); - writeLock.lock(); - Set<TopicPartitionInfo> oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); - Set<TopicPartitionInfo> addedPartitions; - Set<TopicPartitionInfo> removedPartitions; - try { - addedPartitions = new HashSet<>(newPartitions); - addedPartitions.removeAll(oldPartitions); - removedPartitions = new HashSet<>(oldPartitions); - removedPartitions.removeAll(newPartitions); - this.partitions = newPartitions; - } finally { - writeLock.unlock(); - } - - if (!removedPartitions.isEmpty()) { - stateConsumer.removePartitions(removedPartitions); - eventConsumer.removePartitions(withTopic(removedPartitions, eventConsumer.getTopic())); - } - - if (!addedPartitions.isEmpty()) { - partitionsInProgress.addAll(addedPartitions); - stateConsumer.addPartitions(addedPartitions, partition -> { - var readLock = partitionsLock.readLock(); - readLock.lock(); - try { - partitionsInProgress.remove(partition); - log.info("Finished partition {} (still in progress: {})", partition, partitionsInProgress); - if (partitionsInProgress.isEmpty()) { - log.info("All partitions processed"); - } - if (this.partitions.contains(partition)) { - eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); - } - } finally { - readLock.unlock(); - } - }); - } - initialized = true; - } - - public Set<TopicPartitionInfo> getPartitionsInProgress() { - return initialized ? partitionsInProgress : null; - } - -} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java index 93601146e9..84cb3c9382 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java @@ -20,6 +20,6 @@ import java.io.Serializable; public enum QueueTaskType implements Serializable { UPDATE_PARTITIONS, UPDATE_CONFIG, DELETE, - ADD_PARTITIONS, REMOVE_PARTITIONS + ADD_PARTITIONS, REMOVE_PARTITIONS, DELETE_PARTITIONS } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java index 3380dd7e31..e0dd9b808b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java @@ -60,4 +60,11 @@ public interface TbQueueConsumerManagerTask { } } + record DeletePartitionsTask(Set<TopicPartitionInfo> partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.REMOVE_PARTITIONS; + } + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java new file mode 100644 index 0000000000..be019caaa7 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/DefaultQueueStateService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 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.queue.common.state; + +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; + +public class DefaultQueueStateService<E extends TbQueueMsg, S extends TbQueueMsg> extends QueueStateService<E, S> { + + public DefaultQueueStateService(PartitionedQueueConsumerManager<E> eventConsumer) { + super(eventConsumer); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java new file mode 100644 index 0000000000..9adc6bb996 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/KafkaQueueStateService.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2025 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.queue.common.state; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; + +import java.util.Set; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + +@Slf4j +public class KafkaQueueStateService<E extends TbQueueMsg, S extends TbQueueMsg> extends QueueStateService<E, S> { + + private final PartitionedQueueConsumerManager<S> stateConsumer; + + public KafkaQueueStateService(PartitionedQueueConsumerManager<E> eventConsumer, PartitionedQueueConsumerManager<S> stateConsumer) { + super(eventConsumer); + this.stateConsumer = stateConsumer; + } + + @Override + protected void addPartitions(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + Set<TopicPartitionInfo> statePartitions = withTopic(partitions, stateConsumer.getTopic()); + partitionsInProgress.addAll(statePartitions); + stateConsumer.addPartitions(statePartitions, statePartition -> { + var readLock = partitionsLock.readLock(); + readLock.lock(); + try { + partitionsInProgress.remove(statePartition); + log.info("Finished partition {} (still in progress: {})", statePartition, partitionsInProgress); + if (partitionsInProgress.isEmpty()) { + log.info("All partitions processed"); + } + + TopicPartitionInfo eventPartition = statePartition.withTopic(eventConsumer.getTopic()); + if (this.partitions.get(queueKey).contains(eventPartition)) { + eventConsumer.addPartitions(Set.of(eventPartition)); + } + } finally { + readLock.unlock(); + } + }); + } + + @Override + protected void removePartitions(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + super.removePartitions(queueKey, partitions); + stateConsumer.removePartitions(withTopic(partitions, stateConsumer.getTopic())); + } + + @Override + protected void deletePartitions(Set<TopicPartitionInfo> partitions) { + super.deletePartitions(partitions); + stateConsumer.delete(withTopic(partitions, stateConsumer.getTopic())); + } + + @Override + public void stop() { + super.stop(); + stateConsumer.stop(); + stateConsumer.awaitStop(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java new file mode 100644 index 0000000000..29426fab63 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/state/QueueStateService.java @@ -0,0 +1,114 @@ +/** + * Copyright © 2016-2025 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.queue.common.state; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.discovery.QueueKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + +@Slf4j +public abstract class QueueStateService<E extends TbQueueMsg, S extends TbQueueMsg> { + + protected final PartitionedQueueConsumerManager<E> eventConsumer; + + @Getter + protected final Map<QueueKey, Set<TopicPartitionInfo>> partitions = new HashMap<>(); + protected final Set<TopicPartitionInfo> partitionsInProgress = ConcurrentHashMap.newKeySet(); + protected boolean initialized; + + protected final ReadWriteLock partitionsLock = new ReentrantReadWriteLock(); + + protected QueueStateService(PartitionedQueueConsumerManager<E> eventConsumer) { + this.eventConsumer = eventConsumer; + } + + public void update(QueueKey queueKey, Set<TopicPartitionInfo> newPartitions) { + newPartitions = withTopic(newPartitions, eventConsumer.getTopic()); + var writeLock = partitionsLock.writeLock(); + writeLock.lock(); + Set<TopicPartitionInfo> oldPartitions = this.partitions.getOrDefault(queueKey, Collections.emptySet()); + Set<TopicPartitionInfo> addedPartitions; + Set<TopicPartitionInfo> removedPartitions; + try { + addedPartitions = new HashSet<>(newPartitions); + addedPartitions.removeAll(oldPartitions); + removedPartitions = new HashSet<>(oldPartitions); + removedPartitions.removeAll(newPartitions); + this.partitions.put(queueKey, newPartitions); + } finally { + writeLock.unlock(); + } + + if (!removedPartitions.isEmpty()) { + removePartitions(queueKey, removedPartitions); + } + + if (!addedPartitions.isEmpty()) { + addPartitions(queueKey, addedPartitions); + } + initialized = true; + } + + protected void addPartitions(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + eventConsumer.addPartitions(partitions); + } + + protected void removePartitions(QueueKey queueKey, Set<TopicPartitionInfo> partitions) { + eventConsumer.removePartitions(partitions); + } + + public void delete(Set<TopicPartitionInfo> partitions) { + if (partitions.isEmpty()) { + return; + } + var writeLock = partitionsLock.writeLock(); + writeLock.lock(); + try { + this.partitions.values().forEach(tpis -> tpis.removeAll(partitions)); + } finally { + writeLock.unlock(); + } + deletePartitions(partitions); + } + + protected void deletePartitions(Set<TopicPartitionInfo> partitions) { + eventConsumer.delete(withTopic(partitions, eventConsumer.getTopic())); + } + + public Set<TopicPartitionInfo> getPartitionsInProgress() { + return initialized ? partitionsInProgress : null; + } + + public void stop() { + eventConsumer.stop(); + eventConsumer.awaitStop(); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 345b44e764..3a76d50825 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -52,7 +52,10 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.thingsboard.server.common.data.DataConstants.CF_QUEUE_NAME; +import static org.thingsboard.server.common.data.DataConstants.CF_STATES_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.EDGE_QUEUE_NAME; import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @@ -159,16 +162,7 @@ public class HashPartitionService implements PartitionService { List<QueueRoutingInfo> queueRoutingInfoList = getQueueRoutingInfos(); queueRoutingInfoList.forEach(queue -> { QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queue); - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.put(cfQueueKey, queue.getPartitions()); - partitionTopicsMap.put(cfQueueKey, cfEventTopic); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.put(cfQueueStatesKey, queue.getPartitions()); - partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); - } - partitionTopicsMap.put(queueKey, queue.getQueueTopic()); - partitionSizesMap.put(queueKey, queue.getPartitions()); + updateQueue(queueKey, queue.getQueueTopic(), queue.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queue)); }); } @@ -215,16 +209,7 @@ public class HashPartitionService implements PartitionService { QueueRoutingInfo queueRoutingInfo = new QueueRoutingInfo(queueUpdateMsg); TenantId tenantId = queueRoutingInfo.getTenantId(); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueRoutingInfo.getQueueName(), tenantId); - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.put(cfQueueKey, queueRoutingInfo.getPartitions()); - partitionTopicsMap.put(cfQueueKey, cfEventTopic); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.put(cfQueueStatesKey, queueRoutingInfo.getPartitions()); - partitionTopicsMap.put(cfQueueStatesKey, cfStateTopic); - } - partitionTopicsMap.put(queueKey, queueRoutingInfo.getQueueTopic()); - partitionSizesMap.put(queueKey, queueRoutingInfo.getPartitions()); + updateQueue(queueKey, queueRoutingInfo.getQueueTopic(), queueRoutingInfo.getPartitions()); queueConfigs.put(queueKey, new QueueConfig(queueRoutingInfo)); if (!tenantId.isSysTenantId()) { tenantRoutingInfoMap.remove(tenantId); @@ -235,9 +220,15 @@ public class HashPartitionService implements PartitionService { @Override public void removeQueues(List<TransportProtos.QueueDeleteMsg> queueDeleteMsgs) { List<QueueKey> queueKeys = queueDeleteMsgs.stream() - .map(queueDeleteMsg -> { + .flatMap(queueDeleteMsg -> { TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); - return new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); + if (queueKey.getQueueName().equals(MAIN_QUEUE_NAME)) { + return Stream.of(queueKey, queueKey.withQueueName(CF_QUEUE_NAME), + queueKey.withQueueName(CF_STATES_QUEUE_NAME)); + } else { + return Stream.of(queueKey); + } }).toList(); queueKeys.forEach(queueKey -> { removeQueue(queueKey); @@ -252,25 +243,38 @@ public class HashPartitionService implements PartitionService { @Override public void removeTenant(TenantId tenantId) { List<QueueKey> queueKeys = partitionSizesMap.keySet().stream() - .filter(queueKey -> tenantId.equals(queueKey.getTenantId())).toList(); + .filter(queueKey -> tenantId.equals(queueKey.getTenantId())) + .flatMap(queueKey -> { + if (queueKey.getQueueName().equals(MAIN_QUEUE_NAME)) { + return Stream.of(queueKey, queueKey.withQueueName(CF_QUEUE_NAME), + queueKey.withQueueName(CF_STATES_QUEUE_NAME)); + } else { + return Stream.of(queueKey); + } + }) + .toList(); queueKeys.forEach(this::removeQueue); evictTenantInfo(tenantId); } + private void updateQueue(QueueKey queueKey, String topic, int partitions) { + partitionTopicsMap.put(queueKey, topic); + partitionSizesMap.put(queueKey, partitions); + if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { + QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); + partitionTopicsMap.put(cfQueueKey, cfEventTopic); + partitionSizesMap.put(cfQueueKey, partitions); + QueueKey cfStatesQueueKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); + partitionTopicsMap.put(cfStatesQueueKey, cfStateTopic); + partitionSizesMap.put(cfStatesQueueKey, partitions); + } + } + private void removeQueue(QueueKey queueKey) { myPartitions.remove(queueKey); partitionTopicsMap.remove(queueKey); partitionSizesMap.remove(queueKey); queueConfigs.remove(queueKey); - - if (DataConstants.MAIN_QUEUE_NAME.equals(queueKey.getQueueName())) { - QueueKey cfQueueKey = queueKey.withQueueName(DataConstants.CF_QUEUE_NAME); - partitionSizesMap.remove(cfQueueKey); - partitionTopicsMap.remove(cfQueueKey); - QueueKey cfQueueStatesKey = queueKey.withQueueName(DataConstants.CF_STATES_QUEUE_NAME); - partitionSizesMap.remove(cfQueueStatesKey); - partitionTopicsMap.remove(cfQueueStatesKey); - } } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java index 6720a9d71e..1709003ada 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/QueueKey.java @@ -23,9 +23,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; -import static org.thingsboard.server.common.data.DataConstants.CF_QUEUE_NAME; -import static org.thingsboard.server.common.data.DataConstants.CF_STATES_QUEUE_NAME; - @Data @AllArgsConstructor public class QueueKey { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java index f393a27ddf..4ae744be67 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaAdmin.java @@ -91,7 +91,7 @@ public class TbKafkaAdmin implements TbQueueAdmin { @Override public void deleteTopic(String topic) { Set<String> topics = getTopics(); - if (topics.contains(topic)) { + if (topics.remove(topic)) { settings.getAdminClient().deleteTopics(Collections.singletonList(topic)); } else { try { From 28a5f847ac2e147949074480aa5217afa3443d0a Mon Sep 17 00:00:00 2001 From: Igor Kulikov <ikulikov@thingsboard.io> Date: Mon, 17 Mar 2025 17:26:10 +0200 Subject: [PATCH 10/14] UI: Update thermostats demo dashboard with new map widgets. --- .../json/demo/dashboards/thermostats.json | 1539 ++++++++++------- .../lib/maps/data-layer/map-data-layer.ts | 6 +- .../home/components/widget/lib/maps/map.ts | 44 +- 3 files changed, 989 insertions(+), 600 deletions(-) diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 43a11a8709..19710b9792 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -17,7 +17,10 @@ "realtimeType": 1, "interval": 1000, "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, @@ -27,7 +30,11 @@ "startTimeMs": 1694085177686, "endTimeMs": 1694171577686 }, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", @@ -407,7 +414,10 @@ "realtimeType": 1, "interval": 1000, "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, @@ -417,7 +427,11 @@ "startTimeMs": 1694085177686, "endTimeMs": 1694171577686 }, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "AVG", @@ -460,595 +474,132 @@ "id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f", "typeFullFqn": "system.input_widgets.update_multiple_attributes" }, - "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { - "type": "latest", - "sizeX": 13, - "sizeY": 6, + "eda8a397-0959-690c-405c-11e2c9b2bc7e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", + "name": "", + "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", "dataKeys": [ { "name": "temperature", "type": "timeseries", - "label": "temperature", - "color": "#2196f3", - "settings": {}, - "_hash": 0.1371919646686739, + "label": "Temperature", + "color": "#EF5350", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": true, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 14, + "fillAreaSettings": { + "type": "gradient", + "opacity": 0.4, + "gradient": { + "start": 60, + "end": 10 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.5973804076994531, + "units": "°C", "decimals": 1, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#4caf50", - "settings": {}, - "_hash": 0.043177186765847475, - "decimals": 0, - "postFuncBody": "return value || \"\";" - }, - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#f44336", - "settings": {}, - "_hash": 0.5548964320315584 - }, - { - "name": "latitude", - "type": "attribute", - "label": "latitude", - "color": "#ffc107", - "settings": {}, - "_hash": 0.1803778014971602 - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.30926987994082844 + "aggregationType": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null } ], - "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name=\"navigate_to_details\">Thermostat details</link-act><br/>", - "markerImageSize": 48, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "colorFunction": "\n", - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "defaultCenterPosition": [ - 0, - 0 - ], - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "useLabelFunction": true, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'", - "defaultZoomLevel": 14, - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;" - }, - "title": "Thermostat maps", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "headerButton": [], - "tooltipAction": [ - { - "id": "bef25673-b37a-8821-bc0f-5d6dd3680f24", - "name": "navigate_to_details", - "icon": "more_horiz", - "type": "openDashboardState", - "targetDashboardStateId": "chart", - "setEntityId": true - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb", - "typeFullFqn": "system.maps_v2.openstreetmap" - }, - "00fb2742-ba1f-7e43-673f-d6c08b72ed06": { - "type": "latest", - "sizeX": 24, - "sizeY": 12, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#2196f3", - "settings": {}, - "_hash": 0.3640193654284214 - }, + "latestDataKeys": [ { - "name": "latitude", + "name": "temperatureAlarmThreshold", "type": "attribute", - "label": "latitude", + "label": "temperatureAlarmThreshold", "color": "#4caf50", - "settings": {}, - "_hash": 0.49020393887695923 - }, - { - "name": "temperature", - "type": "timeseries", - "label": "temperature", - "color": "#f44336", - "settings": {}, - "_hash": 0.5885892766009955, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#ffc107", - "settings": {}, - "_hash": 0.21077893588180707, - "postFuncBody": "return value || \"\";" - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.34722983638504346 + "settings": { + "__thresholdKey": true + }, + "_hash": 0.7120450032526351 } - ], - "entityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e" + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 - }, - "quickInterval": "CURRENT_DAY" + "realtimeType": 0, + "timewindowMs": 3600000, + "quickInterval": "CURRENT_DAY", + "interval": 30000 }, "aggregation": { "type": "AVG", "limit": 25000 - } + }, + "timezone": null }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "rgba(0, 0, 0, 0)", "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", - "markerImageSize": 34, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "defaultCenterPosition": "0,0", - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "defaultZoomLevel": 12, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'", - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "useLabelFunction": true, - "provider": "openstreet-map", - "draggableMarker": true - }, - "title": "New Markers Placement - OpenStreetMap", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "tooltipAction": [ - { - "name": "delete", - "icon": "more_horiz", - "type": "custom", - "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06", - "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" - }, - "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { - "type": "latest", - "sizeX": 6, - "sizeY": 6, - "config": { - "datasources": [ - { - "type": "entity", - "dataKeys": [ - { - "name": "longitude", - "type": "attribute", - "label": "longitude", - "color": "#2196f3", - "settings": {}, - "_hash": 0.3640193654284214 - }, - { - "name": "latitude", - "type": "attribute", - "label": "latitude", - "color": "#4caf50", - "settings": {}, - "_hash": 0.49020393887695923 - }, - { - "name": "temperature", - "type": "timeseries", - "label": "temperature", - "color": "#f44336", - "settings": {}, - "_hash": 0.5885892766009955, - "postFuncBody": "return value || \"\";" - }, - { - "name": "humidity", - "type": "timeseries", - "label": "humidity", - "color": "#ffc107", - "settings": {}, - "_hash": 0.21077893588180707, - "postFuncBody": "return value || \"\";" - }, - { - "name": "active", - "type": "attribute", - "label": "active", - "color": "#607d8b", - "settings": {}, - "_hash": 0.34722983638504346 - } - ], - "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547" - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1694085177686, - "endTimeMs": 1694171577686 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "8px", - "settings": { - "fitMapBounds": true, - "latKeyName": "latitude", - "lngKeyName": "longitude", - "showLabel": true, - "label": "${entityName}", - "tooltipPattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", - "markerImageSize": 34, - "useColorFunction": false, - "markerImages": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ], - "useMarkerImageFunction": true, - "color": "#fe7569", - "mapProvider": "OpenStreetMap.HOT", - "showTooltip": true, - "autocloseTooltip": true, - "defaultCenterPosition": "0,0", - "customProviderTileUrl": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "showTooltipAction": "click", - "polygonKeyName": "coordinates", - "polygonOpacity": 0.5, - "polygonStrokeOpacity": 1, - "polygonStrokeWeight": 1, - "zoomOnClick": true, - "showCoverageOnHover": true, - "animate": true, - "maxClusterRadius": 80, - "removeOutsideVisibleBounds": true, - "defaultZoomLevel": 5, - "labelFunction": "var color;\nif(dsData[dsIndex].active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'", - "markerImageFunction": "var res;\nif(dsData[dsIndex].active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "useLabelFunction": true, - "provider": "openstreet-map", - "draggableMarker": true, - "editablePolygon": true - }, - "title": "New Markers Placement - OpenStreetMap", - "dropShadow": true, - "enableFullscreen": false, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "useDashboardTimewindow": true, - "showLegend": false, - "widgetStyle": {}, - "actions": { - "tooltipAction": [ - { - "name": "delete", - "icon": "more_horiz", - "type": "custom", - "customFunction": "var entityDatasource = widgetContext.mapInstance.datasources.filter(\n function(entity) {\n return entity.entityId === entityId.id\n });\n\nwidgetContext.mapInstance.saveMarkerLocation(entityDatasource[0], null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", - "id": "54c293c4-9ca6-e34f-dc6a-0271944c1c66" - } - ] - }, - "showTitleIcon": false, - "titleIcon": null, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "displayTimewindow": true - }, - "id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf", - "typeFullFqn": "system.input_widgets.markers_placement_openstreetmap" - }, - "eda8a397-0959-690c-405c-11e2c9b2bc7e": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": "", - "entityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", - "dataKeys": [ - { - "name": "temperature", - "type": "timeseries", - "label": "Temperature", - "color": "#EF5350", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": true, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 14, - "fillAreaSettings": { - "type": "gradient", - "opacity": 0.4, - "gradient": { - "start": 60, - "end": 10 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.5973804076994531, - "units": "°C", - "decimals": 1, - "aggregationType": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - }, - "latestDataKeys": [ - { - "name": "temperatureAlarmThreshold", - "type": "attribute", - "label": "temperatureAlarmThreshold", - "color": "#4caf50", - "settings": { - "__thresholdKey": true - }, - "_hash": 0.7120450032526351 - } - ] - } - ], - "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 3600000, - "quickInterval": "CURRENT_DAY", - "interval": 30000 - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "rgba(0, 0, 0, 0)", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", + "padding": "0px", "settings": { "showLegend": true, "legendConfig": { @@ -1419,9 +970,6 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -1705,17 +1253,802 @@ "row": 0, "col": 0, "id": "ac90f089-197f-b767-82c3-2668844265a2" - } - }, - "states": { - "default": { - "name": "Thermostats", - "root": true, - "layouts": { - "main": { - "widgets": { - "f33c746c-0dfc-c212-395b-b448c8a17209": { - "sizeX": 11, + }, + "5186d1e3-f076-e062-5129-e4dd8e4adfb0": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + }, + "tooltip": { + "show": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name=\"navigate_to_details\">Thermostat details</link-act><br/>", + "patternFunction": null, + "trigger": "click", + "autoclose": true, + "offsetX": 0, + "offsetY": -1, + "tagActions": [ + { + "name": "navigate_to_details", + "type": "openDashboardState", + "targetDashboardStateId": "chart", + "setEntityId": true, + "stateEntityParamName": null, + "openRightLayout": false, + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "image", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "iconContainer": "iconContainer1", + "icon": "mdi:lightbulb-on", + "size": 48, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerImage": { + "type": "function", + "image": "/assets/markers/shape1.svg", + "imageSize": 34, + "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", + "images": [ + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" + ] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": 14, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "5186d1e3-f076-e062-5129-e4dd8e4adfb0" + }, + "1bbd4b8d-db42-ed7c-70c2-3d32c771843d": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "68a058e1-fdda-8482-715b-3ae4a488568e", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + }, + "tooltip": { + "show": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", + "patternFunction": null, + "trigger": "click", + "autoclose": true, + "offsetX": 0, + "offsetY": -1, + "tagActions": [ + { + "name": "delete", + "type": "custom", + "customFunction": "widgetContext.mapInstance.saveMarkerLocation(additionalParams, null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [ + "add", + "move", + "remove" + ], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "image", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "iconContainer": "iconContainer1", + "icon": "mdi:lightbulb-on", + "size": 48, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerImage": { + "type": "function", + "image": "/assets/markers/shape1.svg", + "imageSize": 34, + "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", + "images": [ + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" + ] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": null, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "1bbd4b8d-db42-ed7c-70c2-3d32c771843d" + }, + "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81": { + "typeFullFqn": "system.map", + "type": "latest", + "sizeX": 8.5, + "sizeY": 6, + "config": { + "datasources": [], + "timewindow": { + "displayValue": "", + "selectedTab": 0, + "realtime": { + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1741884755143, + "endTimeMs": 1741971155143 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 25000 + } + }, + "showTitle": false, + "backgroundColor": "rgba(0, 0, 0, 0)", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "mapType": "geoMap", + "layers": [ + { + "label": "{i18n:widgets.maps.layer.roadmap}", + "provider": "openstreet", + "layerType": "OpenStreetMap.Mapnik" + }, + { + "label": "{i18n:widgets.maps.layer.satellite}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery" + }, + { + "label": "{i18n:widgets.maps.layer.hybrid}", + "provider": "openstreet", + "layerType": "Esri.WorldImagery", + "referenceLayer": "openstreetmap_hybrid" + } + ], + "imageSource": null, + "markers": [ + { + "dsType": "entity", + "dsLabel": "", + "dsDeviceId": null, + "dsEntityAliasId": "12ae98c7-1ea2-52cf-64d5-763e9d993547", + "dsFilterId": null, + "additionalDataKeys": [ + { + "name": "temperature", + "type": "timeseries", + "label": "temperature", + "color": "#2196f3", + "settings": {}, + "_hash": 0.570889787682481, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "humidity", + "type": "timeseries", + "label": "humidity", + "color": "#2196f3", + "settings": {}, + "_hash": 0.13597394595782442, + "aggregationType": "NONE", + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": true, + "postFuncBody": "return value || \"\";" + }, + { + "name": "active", + "type": "attribute", + "label": "active", + "color": "#2196f3", + "settings": {}, + "_hash": 0.21080919932756603 + } + ], + "label": { + "show": true, + "type": "function", + "pattern": "${entityName}", + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + }, + "tooltip": { + "show": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", + "patternFunction": null, + "trigger": "click", + "autoclose": true, + "offsetX": 0, + "offsetY": -1, + "tagActions": [ + { + "name": "delete", + "type": "custom", + "customFunction": "widgetContext.mapInstance.saveMarkerLocation(additionalParams, null, null).subscribe(function success() {\n widgetContext.updateAliases();\n});", + "openInSeparateDialog": false, + "openInPopover": false + } + ] + }, + "click": { + "type": "doNothing" + }, + "groups": null, + "edit": { + "enabledActions": [ + "add", + "move", + "remove" + ], + "attributeScope": "SERVER_SCOPE", + "snappable": false + }, + "xKey": { + "name": "latitude", + "label": "latitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "yKey": { + "name": "longitude", + "label": "longitude", + "type": "attribute", + "settings": {}, + "color": "#2196f3" + }, + "markerType": "image", + "markerShape": { + "shape": "markerShape1", + "size": 34, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerIcon": { + "iconContainer": "iconContainer1", + "icon": "mdi:lightbulb-on", + "size": 48, + "color": { + "type": "constant", + "color": "#307FE5" + } + }, + "markerImage": { + "type": "function", + "image": "/assets/markers/shape1.svg", + "imageSize": 34, + "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", + "images": [ + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", + "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" + ] + }, + "markerOffsetX": 0.5, + "markerOffsetY": 1, + "markerClustering": { + "enable": false, + "zoomOnClick": true, + "maxZoom": null, + "maxClusterRadius": 80, + "zoomAnimation": true, + "showCoverageOnHover": true, + "spiderfyOnMaxZoom": false, + "chunkedLoad": false, + "lazyLoad": true, + "useClusterMarkerColorFunction": false, + "clusterMarkerColorFunction": null + } + } + ], + "polygons": [], + "circles": [], + "additionalDataSources": [], + "controlsPosition": "topleft", + "zoomActions": [ + "scroll", + "doubleClick", + "controlButtons" + ], + "scales": [], + "dragModeButton": false, + "fitMapBounds": true, + "useDefaultCenterPosition": false, + "defaultCenterPosition": "0,0", + "defaultZoomLevel": 5, + "mapPageSize": 16384, + "mapActionButtons": [], + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "8px" + }, + "title": "Map", + "useDashboardTimewindow": true, + "displayTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, + "widgetStyle": {}, + "widgetCss": "", + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "pageSize": 1024, + "noDataDisplayMessage": "", + "configMode": "advanced", + "titleFont": null, + "titleColor": null, + "margin": "0px", + "borderRadius": "0px", + "iconSize": "24px", + "titleIcon": "map", + "iconColor": "#1F6BDD", + "actions": {} + }, + "row": 0, + "col": 0, + "id": "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81" + } + }, + "states": { + "default": { + "name": "Thermostats", + "root": true, + "layouts": { + "main": { + "widgets": { + "f33c746c-0dfc-c212-395b-b448c8a17209": { + "sizeX": 11, "sizeY": 11, "row": 0, "col": 0, @@ -1730,11 +2063,12 @@ "mobileOrder": 2, "mobileHeight": 5 }, - "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": { + "5186d1e3-f076-e062-5129-e4dd8e4adfb0": { "sizeX": 13, "sizeY": 6, "row": 5, "col": 11, + "resizable": true, "mobileOrder": 3, "mobileHeight": 5 } @@ -1748,7 +2082,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1759,9 +2094,10 @@ "layouts": { "main": { "widgets": { - "00fb2742-ba1f-7e43-673f-d6c08b72ed06": { + "1bbd4b8d-db42-ed7c-70c2-3d32c771843d": { "sizeX": 24, - "sizeY": 12, + "sizeY": 11, + "resizable": true, "row": 0, "col": 0 } @@ -1775,7 +2111,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1794,14 +2131,6 @@ "mobileOrder": 3, "mobileHeight": 5 }, - "0a430429-9078-9ae6-2b67-e4a15a2bf8bf": { - "sizeX": 6, - "sizeY": 6, - "row": 6, - "col": 0, - "mobileOrder": 4, - "mobileHeight": 6 - }, "eda8a397-0959-690c-405c-11e2c9b2bc7e": { "sizeX": 18, "sizeY": 6, @@ -1817,6 +2146,13 @@ "col": 6, "mobileOrder": 2, "mobileHeight": 6 + }, + "c9da2d6b-4ba9-1ac8-63b8-68ab49b13c81": { + "sizeX": 6, + "sizeY": 6, + "resizable": true, + "row": 6, + "col": 0 } }, "gridSettings": { @@ -1828,7 +2164,8 @@ "mobileAutoFillHeight": false, "mobileRowHeight": 70, "margin": 10, - "outerMargin": true + "outerMargin": true, + "layoutType": "default" } } } @@ -1861,7 +2198,6 @@ "timewindow": { "displayValue": "", "selectedTab": 0, - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, "realtime": { @@ -1889,7 +2225,14 @@ "showEntitiesSelect": true, "showDashboardTimewindow": true, "showDashboardExport": true, - "toolbarAlwaysOpen": true + "toolbarAlwaysOpen": true, + "titleColor": "rgba(0,0,0,0.870588)", + "showDashboardLogo": false, + "dashboardLogoUrl": null, + "hideToolbar": false, + "showFilters": true, + "showUpdateDashboardImage": true, + "dashboardCss": "" }, "filters": {} }, @@ -1901,7 +2244,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "publicResourceKey": "DXvJsh8m6v4NOcO6AHXXm9kQzfrVgisT", + "publicResourceKey": "sGU7afgyBvT0hq6eDQgqk5ksOdlb4B31", "mediaType": "image/svg+xml", "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiNmNDQzMzZ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+", "public": true @@ -1912,7 +2255,7 @@ "type": "IMAGE", "subType": "IMAGE", "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg", - "publicResourceKey": "HtfNoQ7FAZKdeH4m3WGodwajcsscKrTR", + "publicResourceKey": "AzoEf73Kz37X2btYi8Lmev7ZL9d0krVi", "mediaType": "image/svg+xml", "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg==", "public": true diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts index ad1f8c17e9..63d77c48b2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/map-data-layer.ts @@ -172,7 +172,7 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay protected datasource: TbMapDatasource; - protected mapDataId = guid(); + protected mapDataId: string; protected dataLayerContainer: L.FeatureGroup; @@ -281,6 +281,10 @@ export abstract class TbMapDataLayer<S extends MapDataLayerSettings = MapDataLay return false; } + public hasData(data: FormattedData<TbMapDatasource>): boolean { + return data.$datasource.mapDataIds.includes(this.mapDataId); + } + protected createDataLayerContainer(): L.FeatureGroup { return L.featureGroup([], {snapIgnore: true}); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index 054eb5943e..6672941045 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -41,7 +41,7 @@ import { } from '@core/utils'; import { DeepPartial } from '@shared/models/common'; import L from 'leaflet'; -import { forkJoin, Observable, of } from 'rxjs'; +import { EMPTY, forkJoin, Observable, of } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; import '@home/components/widget/lib/maps/leaflet/leaflet-tb'; import { @@ -142,6 +142,7 @@ export abstract class TbMap<S extends BaseMapSettings> { protected inputSettings: DeepPartial<S>, protected containerElement: HTMLElement) { this.ctx.actionsApi.placeMapItem = this.placeMapItem.bind(this); + (this.ctx as any).mapInstance = this; this.settings = mergeDeepIgnoreArray({} as S, this.defaultSettings(), this.inputSettings as S); $(containerElement).empty(); @@ -1101,6 +1102,47 @@ export abstract class TbMap<S extends BaseMapSettings> { return this.dragMode; } + public saveMarkerLocation(data: FormattedData<TbMapDatasource>, lat?: number, lng?: number): Observable<any> { + const targetDataLayer = this.latestDataLayers.find(dl => dl.dataLayerType() === 'markers' && dl.hasData(data)); + if (targetDataLayer) { + let location: L.LatLng = null; + if (isDefinedAndNotNull(lat) && isDefinedAndNotNull(lng)) { + location = new L.LatLng(lat, lng); + } + return (targetDataLayer as TbMarkersDataLayer).saveMarkerLocation(data, location); + } else { + return EMPTY; + } + } + + public savePolygonLocation(data: FormattedData<TbMapDatasource>, coordinates?: TbPolygonCoordinates): Observable<any> { + const targetDataLayer = this.latestDataLayers.find(dl => dl.dataLayerType() === 'polygons' && dl.hasData(data)); + if (targetDataLayer) { + return (targetDataLayer as TbPolygonsDataLayer).savePolygonCoordinates(data, coordinates); + } else { + return EMPTY; + } + } + + public saveLocation(data: FormattedData<TbMapDatasource>, values: {[key: string]: any}): Observable<any> { + const datasource = data.$datasource; + let dataKeys = datasource.dataKeys; + if (datasource.latestDataKeys) { + dataKeys = dataKeys.concat(datasource.latestDataKeys); + } + const itemData: DataKeyValuePair[] = []; + for (const dataKeyName of Object.keys(values)) { + const dataKey = dataKeys.find(key => key.name === dataKeyName); + if (dataKey) { + itemData.push({ + dataKey, + value: values[dataKeyName] + }); + } + } + return this.saveItemData(datasource, itemData, AttributeScope.SERVER_SCOPE); + } + public saveItemData(datasource: TbMapDatasource, data: DataKeyValuePair[], attributeScope: AttributeScope): Observable<any> { const attributeService = this.ctx.$injector.get(AttributeService); const attributes: AttributeData[] = []; From 51fcb16d1cb26cf8a6e66867ce12a00d681b1eca Mon Sep 17 00:00:00 2001 From: dashevchenko <dshevchenko@thingsboard.io> Date: Mon, 17 Mar 2025 17:54:10 +0200 Subject: [PATCH 11/14] do not send update to edqs if rows were not updated --- .../server/dao/timeseries/BaseTimeseriesService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 19ce8e8993..a1137853ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -195,7 +195,9 @@ public class BaseTimeseriesService implements TimeseriesService { } if (saveLatest) { latestFutures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), version -> { - edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + if (version != null) { + edqsService.onUpdate(tenantId, ObjectType.LATEST_TS_KV, new LatestTsKv(entityId, tsKvEntry, version)); + } return version; }, MoreExecutors.directExecutor())); } From 643b2f5c925dac1756aa4a2ee768389f72661251 Mon Sep 17 00:00:00 2001 From: Igor Kulikov <ikulikov@thingsboard.io> Date: Tue, 18 Mar 2025 10:57:25 +0200 Subject: [PATCH 12/14] UI: Update new Map widgets images, description and tags. Optimize 'Thermostats' demo dashboard. --- .../json/demo/dashboards/thermostats.json | 118 +++++++----------- .../data/json/system/widget_bundles/maps.json | 4 +- .../json/system/widget_types/image_map.json | 26 +++- .../data/json/system/widget_types/map.json | 32 +++-- .../json/system/widget_types/route_map.json | 36 ++++-- .../json/system/widget_types/trip_map.json | 37 ++++-- 6 files changed, 140 insertions(+), 113 deletions(-) diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 19710b9792..40866db9ef 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -1366,17 +1366,17 @@ "show": true, "type": "function", "pattern": "${entityName}", - "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px; position: relative; bottom: 6px;\">' + \n '${entityLabel}' + \n '</span>'" }, "tooltip": { "show": true, - "type": "pattern", - "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name=\"navigate_to_details\">Thermostat details</link-act><br/>", - "patternFunction": null, "trigger": "click", "autoclose": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name=\"navigate_to_details\">Thermostat details</link-act><br/>", "offsetX": 0, "offsetY": -1, + "patternFunction": null, "tagActions": [ { "name": "navigate_to_details", @@ -1413,7 +1413,7 @@ "settings": {}, "color": "#2196f3" }, - "markerType": "image", + "markerType": "icon", "markerShape": { "shape": "markerShape1", "size": 34, @@ -1423,23 +1423,20 @@ } }, "markerIcon": { - "iconContainer": "iconContainer1", - "icon": "mdi:lightbulb-on", "size": 48, "color": { - "type": "constant", - "color": "#307FE5" - } + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" }, "markerImage": { "type": "function", - "image": "/assets/markers/shape1.svg", - "imageSize": 34, - "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "images": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ] + "imageFunction": "return {};", + "images": [] }, "markerOffsetX": 0.5, "markerOffsetY": 1, @@ -1627,17 +1624,17 @@ "show": true, "type": "function", "pattern": "${entityName}", - "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px; position: relative; bottom: 6px;\">' + \n '${entityLabel}' + \n '</span>'" }, "tooltip": { "show": true, - "type": "pattern", - "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", - "patternFunction": null, "trigger": "click", "autoclose": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", "offsetX": 0, "offsetY": -1, + "patternFunction": null, "tagActions": [ { "name": "delete", @@ -1675,7 +1672,7 @@ "settings": {}, "color": "#2196f3" }, - "markerType": "image", + "markerType": "icon", "markerShape": { "shape": "markerShape1", "size": 34, @@ -1685,23 +1682,20 @@ } }, "markerIcon": { - "iconContainer": "iconContainer1", - "icon": "mdi:lightbulb-on", "size": 48, "color": { - "type": "constant", - "color": "#307FE5" - } + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" }, "markerImage": { "type": "function", - "image": "/assets/markers/shape1.svg", - "imageSize": 34, - "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "images": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ] + "imageFunction": "return {};", + "images": [] }, "markerOffsetX": 0.5, "markerOffsetY": 1, @@ -1889,17 +1883,17 @@ "show": true, "type": "function", "pattern": "${entityName}", - "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px\">' + \n '${entityLabel}' + \n '</span>'" + "patternFunction": "var color;\nif(data.active !== \"true\"){\n color = 'rgb(255, 0, 0)';\n} else {\n color = 'rgb(39, 134, 34)';\n}\nreturn '<span style=\"border: solid ' + color + '; border-radius: 10px; color: ' + color + '; background-color: #fff; padding: 3px 5px; font-size: 14px; position: relative; bottom: 6px;\">' + \n '${entityLabel}' + \n '</span>'" }, "tooltip": { "show": true, - "type": "pattern", - "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", - "patternFunction": null, "trigger": "click", "autoclose": true, + "type": "pattern", + "pattern": "<b>${entityName}</b><br/><br/><b>Temperature:</b> ${temperature:1} °C<br/><b>Humidity:</b> ${humidity:0} %<br/><br/><link-act name='delete'>Delete</link-act>", "offsetX": 0, "offsetY": -1, + "patternFunction": null, "tagActions": [ { "name": "delete", @@ -1937,7 +1931,7 @@ "settings": {}, "color": "#2196f3" }, - "markerType": "image", + "markerType": "icon", "markerShape": { "shape": "markerShape1", "size": 34, @@ -1947,23 +1941,20 @@ } }, "markerIcon": { - "iconContainer": "iconContainer1", - "icon": "mdi:lightbulb-on", "size": 48, "color": { - "type": "constant", - "color": "#307FE5" - } + "type": "function", + "color": "rgb(255, 0, 0)", + "range": null, + "colorFunction": "if (data.active !== \"true\"){\n return 'rgb(255, 0, 0)';\n} else {\n return 'rgb(39, 134, 34)';\n}" + }, + "iconContainer": "iconContainer1", + "icon": "thermostat" }, "markerImage": { "type": "function", - "image": "/assets/markers/shape1.svg", - "imageSize": 34, - "imageFunction": "var res;\nif(data.active !== \"true\"){\n\tvar res = {\n\t url: images[0],\n\t size: 48\n\t}\n} else {\n var res = {\n\t url: images[1],\n\t size: 48\n\t}\n}\nreturn res;", - "images": [ - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "tb-image;/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg" - ] + "imageFunction": "return {};", + "images": [] }, "markerOffsetX": 0.5, "markerOffsetY": 1, @@ -2237,28 +2228,5 @@ "filters": {} }, "name": "Thermostats", - "resources": [ - { - "link": "/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "title": "\"Thermostats\" dashboard widget \"Thermostat maps\" marker image 0", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_0.svg", - "publicResourceKey": "sGU7afgyBvT0hq6eDQgqk5ksOdlb4B31", - "mediaType": "image/svg+xml", - "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiNmNDQzMzZ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+", - "public": true - }, - { - "link": "/api/images/system/thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg", - "title": "\"Thermostats\" dashboard widget \"Thermostat maps\" marker image 1", - "type": "IMAGE", - "subType": "IMAGE", - "fileName": "thermostats_dashboard_widget_thermostat_maps_marker_image_1.svg", - "publicResourceKey": "AzoEf73Kz37X2btYi8Lmev7ZL9d0krVi", - "mediaType": "image/svg+xml", - "data": "PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc0NDA4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjAiIHk9IjAiIHZpZXdCb3g9IjAgMCAxNTAgMTUwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48c3R5bGU+LnN0MntmaWxsOiMyNzg2MjJ9PC9zdHlsZT48ZyBpZD0ibGF5ZXIxIj48ZyBpZD0icGF0aDY4ODEtMy01LTUtMS04LTQtNC03LTgiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xNDYuNDM4IC0yNzYuMDI4KSIgb3BhY2l0eT0iLjg5MiI+PHJhZGlhbEdyYWRpZW50IGlkPSJTVkdJRF8xXyIgY3g9IjMwODUuMjE1IiBjeT0iMzE3OC40NTgiIHI9IjQ5LjkwMSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjc5MyAuMDA3NiAtLjUwOSAuNTYxMiAtMjMyLjYyOSAtMTQxMS43MjUpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9Ii4xODgiLz48L3JhZGlhbEdyYWRpZW50PjxwYXRoIGQ9Ik0yODUuNiAzODguNWMxMC4zLTEyLjQgNC40LTIyLjQtMTQuNC0yMi40LTE4LjkgMC00Mi40IDEwLTUzLjkgMjIuNC0xNi44IDE4IC40IDIzLjUtLjIgMzUtLjEgMS44IDMuOSAxLjggNyAwIDE5LjgtMTEuNSA0Ni41LTE3IDYxLjUtMzUiIGZpbGw9InVybCgjU1ZHSURfMV8pIi8+PC9nPjxwYXRoIGlkPSJwYXRoNjg4MS0zLTUtNS0xLTgtNC00IiBjbGFzcz0ic3QyIiBkPSJNMTI0LjcgNjkuMWMtLjktMjcuNS0yMi4zLTQ5LjgtNDkuOC00OS44cy00OSAyMi4zLTQ5LjggNDkuOGMtMS4zIDQwLjEgMzAuNyA1Mi4yIDQ0LjcgNzggMi4yIDQgOCA0IDEwLjEgMCAxNC4xLTI1LjggNDYuMS0zNy45IDQ0LjgtNzgiLz48L2c+PGcgaWQ9Imc0OTI4Ij48Y2lyY2xlIGlkPSJwYXRoNDk3OCIgY2xhc3M9InN0MiIgY3g9Ijc0LjkiIGN5PSI2OS4xIiByPSI0OS45Ii8+PGcgaWQ9Imc0OTE1Ij48cGF0aCBpZD0icGF0aDY4ODMtMi0zLTUtMi00LTktNC05IiBkPSJNNzQuOCAxMDYuNGMtMjAuNiAwLTM3LjQtMTYuNy0zNy40LTM3LjQgMC0yMC42IDE2LjctMzcuNCAzNy40LTM3LjQgMjAuNiAwIDM3LjQgMTYuNyAzNy40IDM3LjRzLTE2LjcgMzcuNC0zNy40IDM3LjQiIGZpbGw9IiNmZmYiLz48L2c+PC9nPjxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik05NS45IDQ2LjZWNDloLTEwdi0yLjVsMTAgLjF6bS0yIDUuM2gtOHYyLjVoOHYtMi41em0tOCA3LjloNnYtMi41aC02djIuNXptNCAyLjloLTR2Mi41aDR2LTIuNXptLTQgNy44aDJWNjhoLTJ2Mi41em0xLjUgMTRjMCA2LjktNS41IDEyLjUtMTIuMyAxMi41cy0xMi4zLTUuNi0xMi4zLTEyLjVjMC00LjUgMi4zLTguNSA2LjEtMTAuN1Y0NS41YzAtMy41IDIuOC02LjMgNi4yLTYuM3M2LjIgMi44IDYuMiA2LjN2MjguM2MzLjggMi4yIDYuMSA2LjMgNi4xIDEwLjd6bS0yLjQgMGMwLTMuOC0yLjEtNy4yLTUuNC04LjlsLS43LS4zVjQ1LjVjMC0yLjEtMS43LTMuOC0zLjgtMy44LTIuMSAwLTMuOCAxLjctMy44IDMuOHYyOS44bC0uNy4zYy0zLjMgMS43LTUuNCA1LjEtNS40IDguOSAwIDUuNSA0LjQgMTAgOS45IDEwUzg1IDkwIDg1IDg0LjV6bS0yLjEgMGMwIDQuNC0zLjUgOC03LjggOHMtNy44LTMuNi03LjgtOGMwLTMuNiAyLjQtNi44IDUuOC03LjdsLjUtLjFWNjEuNWgzLjF2MTUuMmwuNS4xYzMuMyAxIDUuNyA0LjEgNS43IDcuN3ptLTcuNC01LjNjLS4yLS44LTEtMS40LTEuOS0xLjItMyAuNy01IDMuMy01IDYuNCAwIC45LjcgMS42IDEuNiAxLjZzMS42LS43IDEuNi0xLjZjMC0xLjYgMS4xLTMgMi42LTMuMy43LS4yIDEuMy0xIDEuMS0xLjl6Ii8+PC9zdmc+Cg==", - "public": true - } - ] + "resources": [] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/maps.json b/application/src/main/data/json/system/widget_bundles/maps.json index d0d0741e21..74b4870de1 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -2,9 +2,9 @@ "widgetsBundle": { "alias": "maps_v2", "title": "Maps", - "image": "tb-image:bWFwc19zeXN0ZW1fYnVuZGxlX2ltYWdlLnBuZw==:Ik1hcHMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=:SU1BR0U=;data:image/png;base64,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", + "image": "tb-image:bWFwcy13aWRnZXQtYnVuZGxlLnBuZw==:Ik1hcHMiIHN5c3RlbSBidW5kbGUgaW1hZ2U=:SU1BR0U=;data:image/png;base64,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", "scada": false, - "description": "Visualize the latest location or trip of the devices or other entities on the indoor or outdoor maps.", + "description": "Visualize the latest location or trip of devices and entities on indoor and outdoor maps using markers, polygons, and circles for enhanced spatial representation.", "order": 6000, "name": "Maps" }, diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 2b7d56957b..2a4fe71696 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -2,8 +2,8 @@ "fqn": "image_map", "name": "Image Map", "deprecated": false, - "image": "tb-image;/api/images/system/image_map_system_widget_image.png", - "description": "Displays the indoor or relative location of the entities on the image map. Useful to display floor maps, smart parking, etc. Entity coordinates are expected to be in the range from 0 to 1. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "image": "tb-image;/api/images/system/image-map-widget.png", + "description": "Displays the indoor or relative location of entities on an image map, making it ideal for floor plans, smart parking, and more. Entity coordinates are expected to range from 0 to 1. Supports markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "latest", "sizeX": 8.5, @@ -21,14 +21,14 @@ }, "resources": [ { - "link": "/api/images/system/image_map_system_widget_image.png", + "link": "/api/images/system/image-map-widget.png", "title": "\"Image Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "image_map_system_widget_image.png", - "publicResourceKey": "otJxNhSbraXccAZhnPzmfOIdEXra5Hf5", + "fileName": "image-map-widget.png", + "publicResourceKey": "UJKMEwPHCOL8rQk6qr9pRRHNnD90yeTA", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true }, { @@ -45,6 +45,20 @@ ], "scada": false, "tags": [ + "plan", + "zone", + "parking", + "location", + "coordinates", + "indoor", + "image", + "marker", + "geofence", + "placement", + "polygon", + "circle", + "layer", + "tiles", "building", "interior", "venue", diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index 9f246a215a..b9069d8af6 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -2,8 +2,8 @@ "fqn": "map", "name": "Map", "deprecated": false, - "image": "tb-image;/api/images/system/openstreet_map_system_widget_image.png", - "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Some providers require additional licenses. Highly customizable via custom markers, marker tooltips, and widget actions. ", + "image": "tb-image;/api/images/system/map-widget.png", + "description": "Displays the location of the entities on Map. Allows to choose among existing tile providers or configure own server. Supports markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "latest", "sizeX": 8.5, @@ -21,24 +21,36 @@ }, "resources": [ { - "link": "/api/images/system/openstreet_map_system_widget_image.png", - "title": "\"OpenStreet Map\" system widget image", + "link": "/api/images/system/map-widget.png", + "title": "\"Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "openstreet_map_system_widget_image.png", - "publicResourceKey": "Uyd9JmVKUcCF6PchzgNnAOjx9WvVN5ZE", + "fileName": "map-widget.png", + "publicResourceKey": "scAsnySDiQSGXiKpt69cZ9jxZh0zl3eL", "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC91BMVEXx7uip0t7////18/Cq0t708fDyyUzw7Oa92bHy8e5vz5fw7efx7+7y7+rx7uzz8+/9/fz08e3z8Oz8+/rBrLzx7OoAAAD5+PXPv8r6+fjOvsnVyNHv6ub39vTYzNPt6eW/2rPSw83t7OnRwcz39fLx7+rq5eLp4+Hr6uev1N/28/Hs5+Tn4N/Lusbo5+TaztTd09fL38DGtcPu6url5OPf1djJt8TY19Tu6+Xl3t7W4umslKrXytHa5efI3ePh2doXFxbh4N+52N7h7Nioj6bp4+Xf7tWcnbbm3uLb0dfMy8fEscCbmpjMu8htbW3g19nTxc/s5ujg2Nzf3drVxs7Crr6AgH/U1NGTkpD08/Lb2tikiqODhIJYWFcuLi6wr61ycnD01su+qbqOjYxISUji7fHR4efr7uPO3uLk3N3R0M17enmvma7g6Oiy1uDd1NrGxcG6vrOtrKmmpaLY6NC0nrGioaBOT07i2ty6preVdpSJi4pkZWQNDQ3r8/Xp7ODl7d3z0cR0dXRoaWjc0NSnssbCwb/E3Lm4uLdcXVxTU1I1NTP2+Pbb6+/j29+3orSihp/W6O3c6NXEs8K1tbGXlpWLaoqIh4YmJiXc3dqousyzsrB8fXxhYWDy5N3Hztbz3NOnw9L0wLAeHx7S0c/1zL28u7mpqKbm6um+2uKozdvVztbNzcu5s8SZepeegpw+Pz3y6eLG2NypyNbV5Muju8u5wrSen5ycgJuRcZHt9Oq/vrx3eHfm6+W0vsv1x7eehZ/IxsWepLvr7u7C3ub2sp+afpnQ197S4sfN4cOrrMH07uvDvszj6OHAxbz1rJblv0/k29ugr8OppaXU2+KTnJ7JxdGzqLywobb1uaadpKbVtVC5ydKCYIGCzqF+c05tR2y80LKB0KJtln68o1RNRTLy8eHIrVqIe1Sy5cjQvMimsrXMpKt91KH1pY6mk1ZYUkLRzMjoxLvxz2hvZkq0mEXHtJ+ol5LAso7UuGetmFi0nleznVd4V/r5AAAo9UlEQVR42rSXa2vTUBjHTzSgw7C80IH3S0VtUIuXQSUlkdVpvcx5H5udOnWJQZco4r0l6uamGwatlw3mNO6F2qxWoXOoMN20xYoUhNHaWeYn8JXfwCftujaus8PLD5q2aUjOL//nOTlFE66HQp4QjkuswnGswuL2gB0foXvA4Xjfg/1Cj8MB+35D4yfsr+lub2xsbG/v7MLyQoWuT0DXFVmW2TlzOE4cEu9LvBx4CyYZOt+DSrdB471jIM/ZP4HI39PV3dnZ3t6u23R35VG5jtSEqLrcruJ3vc3umgYYeX15sxrAjSqZkXeBxvtOLA+fDEf8vRDY5NEJoUQs6q5vqm9qqi8m9raocrUgCGdVO/6rSk/XsAZI5b3yJ8Mh/ygdkBlbh0qKuN3lTU1tBIhE5WZd5DtEYlTRc+jqcQCNWH7SlfU/dPR0cgSOXDGmvDfMhZs6CMLakpBrwoLQkqktQ9sD0OX5GVqAWAkDKA37H0A4o7JBZ2Niwt2b4J72EoSpudJirejo+B6g8NF0j89D0pit5xBCAsyEIo79J/RoGoH0rIajgMDYB3o6PVyAgNoqtpi9QwHwyEHPbz0olhbNKMmKrUuRjmAXWczA/9IBEGunkvCKSOiYvF6NwnPRCH2OjQHOT0Fpzm1diFLw4rgjoai/U8IRlY0AJmZ8DKDfsVH0xGJ6SgzKMHP6MjSCgI0FPIZT7wpHYZoHvmWseB6aS9O34wZRBngw4XN7YA5HjskiVkwk4JpohGW7Z0M4GdgxPTw8i7Msy7E0E1IidNpLlkOwi9Y0mo7S0h+IhCgNqiwIJkzO0urOWVix4NSXhkB2L0RGcowFlyTJ4xlkmaDAKtwFmmGYOk8KTZZZjhsK0cAXP80D48sF8Z4UctXdQkIsJnSCoZy9nutx3ZOIncQwMxph4RWDhkmwS/ivHknv5/2rSKI0wtGvowzgUbxRIeyROX9c5AA6TPu/gE0UXuMSUZSAAgusgBQ0gcJRK2xNZktwdH0NpDsEpwwjC9OfP/f1PXqIksw6tBhlYdbsAD7aA0TObyVnfbP5tl9gGH9dnTPo/XDhwytZHRyMcBGOsfU7W6O06uz30zQ1PpHm/UPy/dp6AjhQWVFmKampvEcQXg03kl4E46wgaFmnpi8XPejrO7js60QY3dJD04wdYk+SZaFJAgIKnaZpW0jfjVm+VT65X/XGS4NfSvnwazkyGGH8fs4/GImAiJ+PttJ0nqlPknSRt0rLNVmuPcxYqgvftb27VnGvur4EnBASDQuuLsfwEtiuVvfG1CCbOgdb0vegSGfS4hlgsnj5NpTFm6SG2SRIhjTAY8dzm+08ucnn89l85teyHI8H6+I0/QxEGDEe4fpVVW2V+3lwA5HMgI3gFKXp92WKWUPdSoPCK2cDDFFrLWs7sbnygPVJeROIANld35murEDM9b2joYrTKVqyZElRiqMlc2aQS+YhA2JS5A18EiQ826PwvM1mu0EuBBHnerIORMJeZ4RWB+VVg6JoC3OlX1S/Kl8AkWfDPSIxesBSCjuGm0yG7EGkXGGVfWGx6qzlHYhUW8xmc8OuwlSJs5leH56z8IDLFRBqybt7uXD5UhBZPK9o7cm2ebX7Dsx4UlPzBGUxJSWiMQjgKX0swyK7ncSOjTfJ03oiBWTp9k2tQW/panLLbdmvi0Q5Z1xtTYrE47SWvgUZjkyevHIdMjCkfAcRNxOs2kcQZyoqKs1PKw+XmRYhIDuUgWERSnW5rsUSVRVrObXF+vJJ072yXe6ywh/uO0t3ua3WhhOGXrcHTgVARfSadC+E0oGs2nwRkTvJTTd8x30kuee5bW5BED74dpLFdT7vgmDQ6VT7eYUejPM0reFGD2DdZODqrUkog1dRFFZggwVV+tRrssDGYiEKM7/bjb0uCa7DHR17qyp2gUjJMfgLcHhz89PCve4TRdceW601bYYbtwG4aDbDuidpkTFZc+nSkQIyjYnMQQE9DMN41yAjLyYnmf/xIhqBU36yYnY/SYVxHH/2tIZnB/DRAxyPcdIIkMKXLBLZyii2XszsPaNZSFJYrs3M1UU5IF2QuLaoFq50DXpbs82K2WaZ66JN1qy1bqp1031X/QX9zgHkRehtfTblyJFz9nl+39/vAY7D/qpBOrmATg4mch1KoxHH17O0yJev46a39gPmY4Gh2osHzUalcXSjXNnao7x7xmy+fR+l8W4Wcc9L0KkjHfJ4AVook1umg3tLE1BSKoFcp0MaSHlmK9C2Wb8/BlLuyqIU8fTpq02vvjx/jQrTJCRry6NktLqedjXeeWu/1W5uHZKahkxVamWncUzpaK9Stj54fSCzRTbP403cri+UPukFPOgv8Mz6E2WIw0Ga9DW6mg4amBvoF3Q9e7wFRBI0da2nabm9+H5L7YjdTnc69MpDMLXUt+B3AC5TVZFTEcDmRgJ0LEanYwfQf6ERL8pLJT0/65uabkiZXvQLtikUn+ZFNq2nARlF2QGpMgdLTVZPJrM1H+36EPon3LOVBTzSs2t9U9OrG78qiGSpQqFYXduV2l8NTpkO0QJI+FHecc5rXFlSAyK54aLnV9Xvn0X/gCe/Rn3II8ncfbuaju/98QQVgN7QoADWSiTrVyQqcs8JvQgqCXT0+5RGVUNDxYYdDTtWZY8tL0oR8vtt6K+gPd5QX31RGn+8Mj2wcjlFI6cT5aVq3TWFwOpGiURiEL9RGPQpE2NFVEHUG2NCY3e1paEBFl9ddTR7ALvTf4Tc6C9o7KvPrYTfLfR8pT8eaiwQn4UijdNzc9AcIvtPSBIYpiNOpeZj9/mNFKDTlVLUE6pCDx6SbqOlbBUSNiCjog6hrIn73/L0By2Wez/QmNPXiBZlg3pJEqcPUVRn9305BYCMgOlSj16pPqSXW2BB5ILJqqNH69QoP6WLVpai37KI4f0LNWZj6A+4gzIxzM3VqtVCe5SlLSI+OEPRrQZR4Mzhg1KquaWjdnSoR/mio6PFYj58u1ncTCv05etQPmQsazIRTvarQAlLyp9p61tYDg2vTYnyK1EhxiQoA9HDuF+hqEtI3JmJOCVi1SjUTVEQqdt329rO1F4YQIdeXt+rH2XNA5abzXKoiMiHfRaUB2Y8fL49vJH9TXPEpwYmF4q4O69zqev04MJ1vZc28UUED3WdQrE0WYwZZ2quUbLzYq+7Tra1HTaN3kfoQKDafO6ca8Biab2MUtTlK0kpUS1muI1WIuMJZhZBgeChBJVyBHMlMo5hCGFliBomRBSpHx7WkuFQUVGsmHDe2WKMOUIYTLTMYhUmJYWaBJZ9TDx44hsbmwGP6v2K8kZR415GB1HoNiVwDkRaqA4zQp2B6uZRPQKM9wdS/7luF51HhFNxbAnTRhjDOWsrt5IZD1oPsCvJ9XD4PMPzly+Fw3c5d/Hh/g6XINI3fvBC/9D0VGyk91w3V8z2tuGA42X/YdZgVUVP8igvzsTKO+84xc/fTghWg2K1EZ7zzUhQJrr20d7eZipw5nWnlOpuRujWgUvo+Y0Hl1BPj8NFCwMHRvySXSgPpD3YA6vPclEDDgxgTZTCjsv47sQw6RzFi1UBDNUiEy3EEBVF3kXn7AcnSMx0/dvz6CR38B0+G5bi7rOYVzF4EcrHE2eyF3z3EhnyVdco9lcLHmMoB5v/dO+3kOH6SajL3goo4TjY9Jw1o723HBbLduSx2TxoydK8M4sxj6raOXL/Isfx/cyNM5yMNOOhcb6U6ecgLzKsKtb0M37oEVGkfcrP9U+Pt/iZmOsyc34Sn+3ltIc6GKxijXljNTbzRJLkFBKw7P/+XTF4xxeBVOXgqRfxUiI+lIVl+3a3DchfERnLcQx70YVvRCcA/mIPj1ZyOFy7CJGgFEQQVk2NBJmitEjRcNDs6LBh7+1RLIg4WO3IAI9V/J6a8rLBnYN1looTg8tWr0YCEh+MpCQPRZENMK++RyIwbxcSS4jEKJHPWQsyWFYesgmgijK0kJW1ASIrYVT8rfOYYIxbHJyO2LHLrJWRfntSRGMlGRWpJNbp5iFOG3rgSIkcusBiFVMi0VcvB5aUA2JzRCIz98ZSHoZTD+G5tcLcpcegWfIQzxApLc1SXVtes1/08MJxzY6yteqcZPFWEybNVmy3Sgl3HZvCDGlz4deXCQm4cFIET0wyVLJHgsXD40HCRc3spGmYCCIPpFtHWhnQxotS99wpfklLV2g00/XxeGMyWKc0CMHYVSQjXlikT8jVm3soA3VZ+doaUcQtXNlYVbakAmXBbgxGrRMGjjt0xHWkjSGXwhAwljyPBod4LikyZQhaXYcTIucmjlj3bhyZDgZd76bcIPL87Brl3laGa7fOT61k9o0ajSYWj8caJYKK+iF4rAOP1FvYwtGyUb7POSXbINTYBni8YAJU76pD2WgJIZgHI4gWK5NxmCFaYR+BBxnBDGLwcKWXMLCTxMVoYTs3N3k6BK01UtmHiYlgbQm8spTBuVNLqIDbTf/k1UxCmwijOP74hmGM48RZYjMz2RyzSZqYGNtYsRBFBWMsiTV1xYpVkKhQd1slqCDuWHFBQZDiehFFEHdREUUREY8ePHgRFBT0oFffJGkS25ombfR3mTLNYX553/t/bybDsLNuMWxWMIUeJ1taUn4rDMqJnIh774CFl0wwmAUoAnkRYCEQhT66NLsfQBTzjV96cHOQg82O6ONHrfw4Kp9aHz/evn1qEw6QK0+dOriMLeTGX4dFDH8dxoljYoi1xlrQJZZmBibcnlyL9L7N5bLgKnTIyZOA7MbwLRLuAh3WG23WEoGAH4YgNyueWrn2xK3JW9/Mu1+4FTwBCzcug4qxqiZTfBogDofVE9O0NJTSxFFiU1Zk4arnNwHx2SxNkKMlJ/Jn96sJFsAfTGqaFmIdzUkYgpd40bdPncv11v21tyYfOIDVOIC3UVVRj4N7xAMFmGnBL1+0wurouWenKOGV3iF+fz0gLltneILbjQ9v3BCWBwyKKiIHIhrSqF9bIMBAefZgOc6dO+iFEhioFg/O7XL/1vCHUinQUW/Uj6MoytCwZ+vL+2n0iPgpm2uOMU9QTkN/QiridwBjtQKCIl4YymSMJC2lYUT44yZTMwsDkesBuR6jaRTp2DnbMLZ7+vLjTz70jLOZrm+Y0LVt9jGjca4sswNLjECRfYEElIWTSBaJg2poOgEl4HbeFh30Y7IfgO4wUaJIUbNpY482dtv09Ovgts7O6bHuFzHn7E4jYsGSDIE3UL5JGIXMPJzJ7FhGJKicR4/elIrsw2cMfhhcJIEi2zooUaBau2mqp3vs8cBrF3p07uw8HmhtPTnOiDTIchMMhWopt+IX4Ph7pqHhzIyZxAWVsufEiROziqlqwTavh8FxWgCong4KV1ak20D19EzY2bhKOI4i05tbdY77maSqtmgsDEWXKaxHwrTgtP610lHI4TMEOXOYSJXV4hHArD271xZr6iy2+UDssheo4IY5Ri9WZAp1nN6/wdPbq4ssj+kezhcak4hEIo0wNCmTiqNec2NzC85ixdi3M4AQkmkgSEOGEKiACf2/OQfGroX9ewrIFlagLDdehCkqfv0kRXU9gN7ebW1zjcnpptZW+frOKMNYIpHKojGhOXUDv7fLEg5EcA7XN5wo5ETqCJI9wDDwhk1t+6AMduwSUTAaqTwCB+d7zUV8OKc5WiJQIYXSs9PqZVl2as4U1ELkXdzU1gXlcCTzmx2XBXQunLehDwsMnmR0uiIwDJj6kCwn07UQCWJceaE8jpQ8INEeM3+QTkZg+IxMpFHGMmObtziGXg32gbvE5VIPj1P2/gcR9tOnficSzlCyLQxenEpSDFRAWusfSntLRXTR/yDyqZ9I1OlMNnoAAHfzYKVzwL4OVQt6oMi380WReBP8B5EB1XA252819KG9chwJWS32ivhNKHjEAP67CN6pYTHy4NBeFQ5Myjyc75svrxFta/7fIp5ocyPTL9KrIq32JYPgKywtU3cHC5XiBWDjDhvFDV+EjaY0jOwRwah9NaRW5TysjUm5ufIFyt3Tn+PZcBulCxiqFAlpWrAeRojcJyJc2JvPKzkBlWPArNxptsXiKBMMtLTSXW2BcYbqRILJKAsjpbC0uMcXGMQvq16oAio4+0Xc1hPp7Ok0d3dY5m4IB09qNFeNCJvywohhVRb6eJ/NK1mur0rEeO+GOd5B++Jh85NOmt5w7NixJ7TYJ8J//vr1Mz+YSDBaaPRpUAsRKIr0okhKlh1Vicy9122Oh32+cHzBDZoedwNFjhdFPo9GPvcX8dpjWqDvfslRk6xnSkT2PseH+E5ZZqsScdyzmONx2neXOTYbRa4fi22zF5fWV13kK4r8oeF02qdZg2qoCRxpexBqQsn3f/PtBcabwlavBgPEx5mbps++YaJNcUysyIblbWOKzT46yx8iCacW8uTmEVWWcbaoDSVJKwpvmX2pd1AVHIDZZXBb02NoUaTzcGVEEqkuK+RgPR4WakXUUrwm4f2FVCpVfaK7DQZaRxBF0WgURTf8XSSN6fJPsMoh6EO48CMlp2BYcBwUKSOiqvCPiKr2wp74/LXVb2JhePw/EU7IwvV76yKoqumCCFhMVkBE6h+JpGsggg9hERuZKAlQxOWaq1r7RHywz1TfhH/xY5XCD2wuroYinhqI8O1rVqxYsYTsmOmCAiK/UdkHxYpAoxqJqAumHuYhhzLTaKuhCFsTESMvSRKffXeCJz79jQsXL9URyQ0g8fiP86tAUAixeTsJkfQzvNlMFl3hfbUTgWS6FiI2UeSURQvJoi1H7pAr7Rd38VPx/YmlAnC2Q+fOHvn+WuBPXzx7ULJNXU8mrt98sX0ZuZjJLFFqKWKHCmDKi0wkCL9iMVmxY5Iy8axEFm0n9AxCOODougfSzMxrsvohr6y5Sq4dJq11T8m6i4R/eIXYoHoYZkQVSQyeVyLn1kXqEJroIosVV8PFMUQ6qhydgR66CBH5hz/5OpvgmzmDXFpBFrZLVqVOkhZdGY7H1M2blw0m4uiAinA6AOGQUg96yhjawPHtz/SCSDkRN794R/tm3ocikBMB8/xfR/Eg4uUvXkEa2hWLUqcMU+SOJJ0qFRH6UqtCEW2BWzTQBQwGEQDoYz3dMSPNt08wTaHprMg1xYb9rqzfRVAEtQ1L6wgnLTrFZxSRmpAhWRHegyLKsEUUci6TE8mcI0QEndZkCCqB/l3LmcY0VkVx/OTdPPHZ9rWviAUKrV2gTSltbVltFYhtUqAUStkEBATiAigIsqpRwaCIASIDSAbUKIqJymhiIiOuH9yNSzQmxn2Lxpi4G7cPnkdboFCgrfGXmTedxwzc/z3n3HvufefdpJCC5Co2+Al7O/tsVfkYoVr0Wjog5PYXUmSTXorqf5pKuYGSXMzQF59jkqTfIKQahiSS5o6gEFHiOSkXN/dTsphTFXQtNdXa8kVAyBctJ4NPSKrkaRAN5WfTttlTc7202+mcoQOJNTD0S70+3yYKKa2Q443hLhTSW57Y3NIy7fNJvMUP9zK075y66RtOStJlzdP3NyfKdoX48ou9iRAlUoJw0m1FlP5+1BFUghVd3LYQI0QnpIcuXSw129kZZypNz9pstSiE852uLbfP2HxUYsUgTVfgHEFJXHMafqKYmcXyPImNaUfXSpSkMMxzEr6ej0O3w1+QSCXKaSpqi6BXohCGoxkc5amVL8/a4cu67W9SkAfRgYvnuUWbq2mm2umjy10224aLFtGlp2trnW73mMNRNcpUjKJRNmiD3Uo3mRXyWWH5IMM8n4RCbLRezby1f4OqqjRDBFHCkIZ60tgsIoiEavlkV8gn05Rk+8XfaIVY6blBuqrJZR3F1rpttk0bzdDZm0Tbk9o753DMa+i5UppWDDKGnhn6JYViPpndyPPZTvtSnpQwQl6IFsIQVFXGECCk4BKTfwj81y0QisJI34V/+ibg3z6LDmetcAx3MgbpvLN9vJDZeRoZnffRTfPzVrnjlI+2C2m6dtBssNO0U6Fo6rW95PO5Ts+k++RMkr4AhYT3v041EsO0KyULyk7Qta2tPX1QiDHZAdHS5LK9hEJKafO8i1Y4nVaWzuDo2VNOZx4Gx8bzGOtuVJba3j6If/S+NFZLC+U06xq023tKGb1ef0BIrio8PJUPP/roGzo4DCJ8bBn6vGtrT6aHu1YLJbHE8MjFTfMDsA8HXRt2vA0/S0FKHw5DI0SbkpJCSTDU1QeFKFRZQTNsF7Y1Pvro+/73HzUeahLD+DLkL62tDTVST+8N9qcpX3IpRE2gdTzBhvKtYhn6KFKoXdLpF/cJuVKVBojp5XuWcwSA5lACgPxQm4jo7mUoeXptzdNI5RTvmOQLLBp0jwjgf8QgoSRbbdPF5xRPt01ihv+AFsJ57f2SEpPgWZ57nn320YcB0b0hh0Mgt/mhYNnbfYJIsKz3k6COu4ZwQvxfSaeo21paPv3qwzM+/OrTlpbbcK2yX8g2zwZpDI7Jb6QdOnAZoIjwaCnqyeIvUconXxYP8SnK/wjqSPTe9cMZIX64y5tIpe8T8qhRmZkFAlPJyy+/p4cA1fOZcCjLqgyRFBc+hKJK2u4655y72kr42vf/EwOVOPz122fs8vbXw/uru1+bgQhYj8iZ/DU7ldp8ti3By8WlCfA/gpWg3q/PCOdrLyURhQn56AI4yIj10UNtYlFACCKToBQZUdbIs+B/AovJn6JuuwvtEcbb9+9bED76PlwYSYpqVAcRKdJE2HXNUmkqBfD/cOetEkkLxsc+fmiRUHtN8j7AQ1dHaoPVmhBZR1Wk+1iBIC+AmCgwRrc1fO211FZLqPm//LKjpGWSMkCAtNL33w8ciRLJJGcr4SACjSbyolyXnKyAWDBq+AIdXRSCL6faPg02/veVS9r+DH7+tG1nCJbLjcBz/hURuib3bB0gSgfWho0odYJQqZji0AqT5FsgWtIysTgnF/9LeVY0Y+/0V4G2/3F/84Md9/8W+MtX06HJS6FJC71v+kwEZzk7CxCHIrCpN1JRkcm3QJV1WNs0yRaIlmSkEhCzEY4lkSr+MOBXd23hELlWHBDyYTEl4dsp33014KqI4S7PNmcqz1YExVrkRgsf1YVwGLeoohyDRQZAcwRD7hZBNPtngaZ/00KlfCShpoMmwT0PlDFvBuQIi4AyOTtbXhQW6HAURbsGObpxRAs1oUzZ6AgzScYhFnk7aAM+cbzjhoB93i6mEs+bn5fDHiEPQSQSdMo9IfdgK4BGMDQkbniB8IgRTszwc7woIGRX56wRkMq8cEOEkhytAczJIYd07DGJSCxmIki5g2oJxsiPq3dQd6x+vRMjd9x62zV3wg5XQRSYTrSVcdqE7mai1xMiZMlepIBYKiGEfXD7OhpuB4OBcMSgZbVusCTv7ljuCMkQ+9f8ejEnEu0P9us/Dc6CPxY/VhzKVT6doC6/5pozz9vjWNH4dXrr0ytabd3T3czJErLe5m1bI/4J7zoxPXl9FyGAyHcHLacdkKYeQM4eK9++lfvS89lapOp56yiYzcIgbIIypIQR142Pj5eJw0ATGaiT02cE+fA39KsA0yeo/Gs+P3fnJXn1hZdFI0TW/+6SUOvteFrf4SG395nqGtASZdcvd5Ut4wfeIcw7hcSCU025ubmWpiYAeF41+NIceuZnTnjnJYdWm213uxZh8sEyobAec1W1sM7TBwVdXQnGbIt6/MnlrTaxWG3SM2Lx2iQrFr/QKpCKqBQctsIIBsyb5yK33rgbIceTIVt/l2fgaf11DWznGt1cx+Z3T031da0/GRSiVBWG5rrNjz9DFjcLoEeBfFwEhZ85AOx2rWHR7XK1o5CVLuFUM3QvqNs6TkB9Q4NXqFb4x1vVk9eJxes5J3PE1w0M5IvVtw3dvsykUBPoW/v4dYJ6CmXwSgQ750hEIYUMvNs/tfLuu93ijgZ2mCUddY1Tff0TXUgdEQffpw+lnJubbre7cHOzQLC4LcQCuZ8pUcjzWtfHLpcL/11hW53wkgGhv9u/9IIJQDc5LBQmmcY9ePiJ2FOfk9MsXBmoF6NxSjrLxITqKj6QNN7wCPXEuQFC8a6/Sh1FsA/050xN1fc3kAUPWSKkdank+r6+8YAQKWyTF0ohT23ydVGbToAxBbKoBOPHfNCc0tp4IaXQUMcLeUEo6NYvPVKQ3716fSfGi3q8tWNlUt2JQh7LZx/0qoXr3d2dfjGm8VPf7hPybSe+4BXkWogBaVeXXp1E/H2kr5HkEKLPIX1bL6yZGk92PBLa9LNCkHZeiHYzGWB0rF2hOA2QMMYH/PNaZtHlKkyF/K2264SX5AtN3YynTJk/PrnVqcfAH//JXzcgHkIhHWLkEtMlW+pOpRjDPef+MJPgJFJGXRsScjlER+P6wBoASVermeAEglxMdmBEEG4RKGh3ZWQYy3njNH28YQckm5/77VoCY6deGoPV8fFJ4ZReWNIt62r2+Fe8w8MmoVDbUMAK2sTovHVCcf6S16NuHh5ezWf4ObH71/AIGaZSgjJuvQmiZD090RP5IIUMEGXALkU1xx2KwEtOq3ZBmhoNsH1iUGIimJiMEj+erpXBe6iYEQfx+xkxm29ixSIAjkq/65s9Or65YTlkkFcKIFoGhCgkujKL6J5MiEAYzu7JQbliXknogmDuAIiMum16j5DpumCEnHdTbI/Z8mE/nCEJezCcrOQ0+G8UXXn4sv2S3Xj/9K47KOmr55776kUxP/hEpCLohhC0oPnplZOydC0n2rvqKoIYoQmhpbvumes4fCNFiM4VcqwyKgXiRSwuaezcsYdpqqO1sftBGY+WhJpi0USZybshAE6lIgFNuFDimaYRAGxBJFKoB4M7EG/ftcTvoMSJiHgnvJ2Q5WkbLxETMjBlMplum5KVnZjwyDpWV0NBUhmlECFJZwAYcrK7YYkmu2iUMDkAIj6/FB3cE/oxkAMPSygO4oX1bJW0dsLQ9Q11XkJIsxeF+G9nB5qV9c0Ny8tPhjb3ISpcra3AMCK6cSKHIHRjPkv0gLCgHBhSEoOhq48QCINQKY/xYfItHsCghbjRe7uYrk7gT03j08X8bhNyiXbAA4XNJ5eXh/3Akxll3Vmrp3V1jWFIRxlN0CALZWUL/q3rC+F6HZQNPblqKFlaX+8wcBCGDN+y+/uMf4qXqdfvhrhhx/tNJ1DIegnhUbehjpzbZShEcF3X8vLtukC4q4wQDcMPtnZ4GIbtLqEZMWlsKCsbaG2dKCnxYHzk3zZM1hf86mGT+ECYbBX/VbxFSZ67F+KGGfD2j09B/e3blRZEPOTpH7hkWdY/AFDmeeTJB3fXu9Hw2EpHxyRvkT4+QIbQIredbJ3Qaoc8kwsLfZ0lT3powyU5ZH+cpuC++TolkV76AMSNWL20lM9PJ1v8RURI2ZBHnS575BFATvRBkAJFJkTBar2JZRgpaVztYlBI920vrDzSOqHOuc7j8ZR4Ov0n+w2GOhMN+3BQiISDu/Fl8DixOPZNVRzZRkYgHKM5Kvs2X+dtZIEjj7R1r5aQAW/bCeGJtomJDpYsDU8NGRonDYacEinsIw+PN0jBu5e+eDPExy3lGogOwTVl/oTjhSAZge6ox3GLwd+kqw6viDzXRwwmAyEGKMjSvZNrKbWMFIAgTWm8JdRJj39wX5zhnmfNgii5BunLFxwjhM3YraXgIEPMjx8LDKGxbEaO4CIGr6k71Mi32U0cHojTJOUWiJY3r+HpgtiQignCJ7pwpfyWkSKLQq4o3OuwcqTGuGPrx/EUqDionM+CaKlHGQjEigg3yIIhnbXzmDn8MLbS1Ozs0pHMYKk/xJ4wplWkQtQUBEwC8VN5VOpZaJYrKnUC0evcA5cKYsmra8urdA5rKcSgpJ6X0meCeMlyAHKUFrOZ46QAMcwmgnl5UfLZVmsmxEb+NdeUQdzkwdEkGTKLXsdKfe7Smx+P+mkMxEdm34MQN1Y4hrmNjXIOQSVhCTqhCY1wDMdIUSgi4skAcFSzCMSDXFHwPwmxnC6d8dm4AHs7zyUkIYQkDJubtdliFXJ56KmE+X8S0mSmhTabrmKuB4XY7c7BW0ZnUcao0+ks6GHIldlEbp8brL6YjJ6qrSIz1YSc7XaPzs1Z2SSIh3dSIT6KFHAkL7kIa3MNjjKMneMW0/I2mwxzt0D5bJV8TLqRlTSnMOfZbFgOtmgwzNaSvGwyg2V5VpttMU7fKqiA+FAojxEiJDRDxnxCZlFaOshduWn1jdXAKULcG1x7aWY57bTZbKfo0naDYTRPuOi21TqTmmyoLU6LGB0QF7lKOJqxaoIs+mzMBjeYzOnmfO6xXLALyZyba7c7aQZbrWmia9sNZNHGnna57KnsnM020xSnEEWcQlLT4Gja5TYU0uOSb+RyPS7OMcoljanAOW9bdHO1TiwqtPscG4O0vN2gstN0U7J70ceikQZr440RR7S5CYRRIYBjaOrxoZLTp0c5TsFx1dUc164T2Z092a9z7tNCmq529tTaaKbn+UEs/Wx/vimVZXs3Z+McfhFFdG+iEcKELxmi2QwjNDGZuAP43HJ6l6TBUrwUJrEBkiBOEizHW4OjyUg1oXHmyggeQ2CJwoYIwJ29vZhySYNw996XyVVaN2laBBlSGqnOnrfytZJQyuLJwBkQP4LDhy2+SxHCA1KyTXp6uhSK5EqImrCJ6u67bwbLLYAt50Ep7opeXhVA6GXb/yNnmtHUztBmlWp+hJBZQuSzGlo6aFclK3OV/815E14PdD1aheGCzzsKayrT4D+RDYdhrxqcTTRnq1SniW6TXNljrwSctHoUXIwJfyHs494P4CA6FZZm/RdyjYeOoMxge2JTj8s1RjKbyKlFhmkfcbtPV8coROA40NURV47KqlSUHD9pjgSITDJpT754Q+OaOUXsabZFzDZ6erG6XyiNdYg3wy5HrBx1qXL4LxTmQmSqSQ89slnhqh0kG5nVi2aGmetVtNuJCGLjSk3lQed6TgAH0CTDf8GYVwQRYYmLVDc1DTpdmDPpFmcYprfHbp8lEKtraTQH+urx+x6I0BJVEfwHkvMqHJZCJRyAIGfPAdgI8RECDJLkYhkGYiEzV1E9o9GYD3z/++AglvjLR5UWvrNGVCo8kGvkQGk6Ic48EgQwFEVSFCOK7Q13+QjLuhQa1TsJ+wL+0ghRIh+JV0dNYEjRWYyZiipL2j4l24u4YGxnpWp0sdkiNdtqTZX7WB6fRVXzTlrYyc4RxuAseSXEh2Xv+cFmVU1lZni/SRnpjgEKVckW/HL0MW5FXPgEkzAsYsvVqCxBz9HxXXaz4GCQyI0QBwW55vCB2FKDLlaDPyQyaZWq1NTju0y047V4kanr9VpZuoFBMTqNRlWJ90dUVqsGIm0U5SoS4tqZskT0iFsUusNdMQtd8Ri0raHhmxfiuW6pbkuGpCPoptY8tJM5l68R/WCfSRLM6Lyxo5QrDmlTpkUOh+IwHivkdr6BgpMePgVIb8AHXVNa2SPv6gPBxhYMLAyUQSqa5LlL982HeQqIHYEZdRyGWVGYddiXSuEY0h/jtQqGFh7Tc8DwQi5JZ04MrfK93ZrF6oa0KQNQka2ES996fHdWrsjLVl0ZV4plSThqkVKVrIKIFB57lqd6OguQrobH1gwgbGj2jjfKtPr+8f6LL25rVmcI3mVlA/BddiXc/dauSVTYrfFRA8eSAZFIPS4e2ccAaWhbeMxvEBkWGhoZmey2pZXxd4UXd+SwGbA2sF4GluzsF1988bndHDzemVCZd7wMJrIQ5XGuNZWfCXD9uqszh4Csf4HFSJ+S9Y33r11Xz6KQgGFRyOOPP7DjAlhBHh+lFXAMYkiMZJK0iuP6TvvC6rC3oGN1ZSJHBAb/IzhgybauX+o4ccLb0c/CrhA0rSAg4763vr/y5vvuizDXSznRMQtcy3EGSQQqUXxQivHY85lwOazPAoER618AgCMcXmg1GyDYG+98h0L2ppA/6zIff/zeAzMLRwgdrovkc9KMPVOf5RgZ4kTqXxwjVydGn1AkAAAAAElFTkSuQmCC", + "data": "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", "public": true } ], "scada": false, "tags": [ - "mapping", - "gps", + "markers", + "polygon", + "circle", "navigation", + "position", + "sensor", "geolocation", "satellite", - "directions" + "roadmap", + "directions", + "placement", + "layer", + "openstreet", + "google", + "tiles", + "location", + "mapping", + "gps" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/route_map.json b/application/src/main/data/json/system/widget_types/route_map.json index 6d9590fe90..f1d2cd571c 100644 --- a/application/src/main/data/json/system/widget_types/route_map.json +++ b/application/src/main/data/json/system/widget_types/route_map.json @@ -2,8 +2,8 @@ "fqn": "route_map", "name": "Route Map", "deprecated": false, - "image": "tb-image;/api/images/system/route_map_openstreet_system_widget_image.png", - "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Highly customizable via custom markers, marker tooltips, and widget actions.", + "image": "tb-image;/api/images/system/route-map-widget.png", + "description": "Displays an entity's trip on various map providers. Supports custom markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "timeseries", "sizeX": 8.5, @@ -57,24 +57,40 @@ "public": true }, { - "link": "/api/images/system/route_map_openstreet_system_widget_image.png", - "title": "\"Route Map - OpenStreet\" system widget image", + "link": "/api/images/system/route-map-widget.png", + "title": "\"Route Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "route_map_openstreet_system_widget_image.png", - "publicResourceKey": "VTTAajdw2NRpCUbQLGVFCMBybEtJfwFK", + "fileName": "route-map-widget.png", + "publicResourceKey": "xHDxUSAefNkVjlpwj2OoNCHgKGGJLfbx", "mediaType": "image/png", - "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC+lBMVEXYz8jx7uj////x7efXzsbWzMXv7Of9/Pz7V1fEtar3kALKvrTEt6zUysHVy8PMwbfVzMTGua7u6uTs6OLOwrnv6+Xr6+rFuK3TycD61aPLv7XSx7/Rxr3JvLLPxLvHurD6+fjCtanQxbzIvLHv7evZ0Mns6eXq5d/29fXr5uDBs6fTysTIu7Dm4dorKyv29fLj4t/q6ejKv7bm5OLVzcfp5uL7+vri29Tc1M3X1tTg2dLh39zo49zn5+bU09L49/bk3tjx8O/S0c/X1dLb08za0cq/sKT71NHz8Ozm5N/e1s+xr6y/sabv7u3g4N/a19TLwrvW09CtrKrf3drj3Nbb2tfY19e5uLYAkdnz8vE0gi3d3Nze19G3tbLQyMJZWVjNxb/31M749vP18u/Fu7K0srFrampMTEtfX17Pz87FvbaCgoHk4dy8raHe29fGxcSoqKelo6Pv083Gt6zn2tXCwcC8u7rAtq2OjY2JiIh9fHyfnp7HuK6/vbz+7e13dnZycnD16eJmZWXx5N70tar0q6DmyZpTU1PMy8r7Xl5+d3OXlpbW0czSzsrNysfzzsbzxr5FREP039jRxbv2jX/7d3b6wXP+9vXv29f7ysfzvLSRkZBubm77Y2M9PDyUlJOampnaz8zNwq58hK79mpoYFRT95+bo3tv8tbX3ion1lof8f39Ynjro0c7oy8U2Njbz2dDHwLzYw7vhwpkDAwP83t3y0J71npL3gnn3pDHgxb7Ow7bt1LLesrD80Zf3s1f2qD5VmTv78+79vb398uL9pqbzqpP6k5KGf3f3kge+urf8bGwjIiGyq6fUvJIintz8rq7pmZjdzcf83rTPu7Hqta3kpKPzupj4x4X2uGVuoUz4oSo5JSX2mBfIvKrIso6+p4ymYmH3lA7P5e3p1LiTiXvcXV21uniBqFj3kATDvYmll4mnf37Ib25cQ0Oa0/BVrNbieHf7q3GVpW+jtW2QsV/hwK5fe1dsSEhPjD2hwM7Nh4aCm2l0REIeETG+AABDWElEQVR42pyYW1MbZRiAv03228zOZneTsJCd7CEkbBJIgiThYJqSchAQqdBaWrCTWqC2AUoraZHaAgqiDsy0nqozdmyntTdeOFOs3DhT77x0dJxx9Bf4A/Qn+H5fNoeqVcenpRzabt4n7+F7d5GjD6tY5DUU4HWseTFWNgREEHmDlVY0jOMCxkne09XV5bB59zSyUYznn1cix9sA5q9sL6Wa1l2OfP4Gs752u7NpO31uYffDZbjCrz+++OKPv7kcQGcU/qWvkanwdCQgCIIMF2dZ9I98HWM0HQGR3LgDnVRAAG/IyMMrWF7RsWqLsPwGi1dMjEMbGMu81NX1Xo9ja2YNXjv1VVUkEOOYP8PRH402P1WYmJhY+Gx16c4FZuv15bXFnYWF9eb5CYejBzyAX4nI4HOMTfsQlgUhoHo8Hoz+BeL4aIwZVkw9rJuK6EJZEQSwAcHDZ3PFwFgwEJIkhFZ4ll0JYMzzGFKGu7peOnmtsD1xz+EYr4iIAZaxGR1lmGjq4OQlBrgw3djhcj01QUTOTZc+Sz83P3+7kF441+pyXVuKcvkXayKLdjLYAAJA4b8g+1vwULadaRAVffh4G8d0gNsGlI4MwUPQkncF44/gihsbCCV5zJogloxjSBlU1leTW67t8asOhwtEwhs8bxiaz05C85XWUaa1tXViZ/3cS9zS/KrL5dpdKF6ZuH14evqalZ9ZnphovbKzNe1aLHHRX8sinUSknBDOg56kkNB0TcWVMsNegY0M9zN/BgpRhtIxkyBigBCIiDyL+DhCAmSE5UUsx0VIWaKrK2stOS4Vm+HFpzb25+b29kZfdjJAN3z0gASzuTTdOjMzUZzYnV92Aa2FzQOHr09PFy2raWnrauvJqUFXh8VxwpWySJQ0CUMZDv+1ehKnT+vm7FFzOCeFw+FkMJgRaTs2MDXqRTQoHQ0yYCbh84qE1RGMjOcRChCRkKEa8QQWV5KaJ2ZZjh76LhYezM1d3tubnoYrXBoYOLfe2HNsd3z70OBMX/FcqnA1tQMihM3dibuX0p2WZaXTlo0QRu/+Tjx+95E3pYMhaH9qbE9i9uisFBlWjx49yjDOpz25qM/nZB7nLfvz219cJiKYV3GCh/8MmcGQEWHEg8IjLFJ4D4jEgYSywgPRh2dca3c/gxdvOvTKACnv232uwocDA9OL84OlUmnt0OLdzqvrqeViaulEd3dn5/z29XL40ahVQ9Dgtbp2d5ejvj64iOsghFKngRVoeH2sLUbf+CbgBPM3jHbcvPnxTebhTfj6/fcbiQjLh6GjMUIggf0SNkZUJMKHyhsJ9udHDy5ffpRcIZ+B7dWdA+vw6j0Ogmu+dPjY3YGBgZmdUunagYXF5bvL966lUi+lUu+l8n3NfT09EPhxAd4LjhvmorHu3jZbhODzddCx1cj0epGNpvl8vtogPJgHkYPRxw06Rm/dZDo63n7/bTslCEkyFZEhE2EqIvk1rI8kdWPEL8K4Wtm/XMdb1urq6vbyHUeV+eurg3cLr29OlUqvb6eWSpfmjxxcTJXpPMP15oYsi+O9pi7iEezhCRwpLQqEPOoADjiZXFXEaGfqeSYFIqkUU+PhF++Mdnz6Dp2SlOOySMYY8oCIASLQJDxOBGB0QUooFYlX7796+eabo99CZ3Kfrd6+vkpy4aDs7J4cWL9+fftKsZRaTqWOHXvmIChMHg22nE7dyBCkU/1xCTItXYRs8y1BvtfKKYgS9PmiDgBmRX9FRFUq86ihf3iMc75wEEQ6JxmK8yYRefsmJAUsbJwGvZyOdFbiN0CEDyB+hfARxnOX5+bmHlKLh29966jRsws5eb3HUWHrcCpVWk6X1iZThGOX7h5IpQ4KgodVUYYSADCBxVjhRU/GZ/UGEEHy+nyTDoBEbYtoihiDb2N6xiCMMS+cAJH8pNP59kPm7/EFJQRgJLBhnsf7P/NEY//9Z+eeffbZvb29Z450NwMux+OMr65ufX8Vvoi+cOOFW892Ia6HjNlO6gEZmQSjhNeEelU0VclkRGyCiI3Kw5ktpdMCS1/a7YsecNhjy0AEXZFoewwZssgiLOvMhWeoSJR5Ir4AKrOhGPxH+/twLjwL3AdOWeS9ryq47Dqif5auT02sH3Y4PpmjDCPU+dxzkIQsEs1hFIHaEKWwN0AaTxHFFj+p3oBmi3RBRjzhKGeXltzve4ZeGFqgBREUJcYQThkYAaLQyAyCSPqfRDjJFtmHyboHMdkKfzKoy8i3t25+23MsZX8nG0GARdKN1KRHcANeUYVrYlVVlSSIxAOi6IbTCIUDMgbGQh7s1TweL2dhVGbY94qDcMgJw5OKaAwlZm97RhuIABPd/yCCbJ59tqsL3X/weU/PExU+v3kLLB/MPbz1gaPCS5IkEBGFlw23WzAFd0BVTbheEkyeJyJeUQwSEVSuLSmk4hYTMsJZyCbi63bY8zei0tJi7Qa2y0XoLYvkJ56k0T8UUVlEufU5CfkJCq43bt2/8epchVufVmvtNQgtGCR7jBp2uzGS3KaqwvdoRLNFQqIoEBGPRkX4kIYzMjRJtCri8fnyDuAIw+TCtNcxU0ZGlEwMRAiLT+gPXURVnqAAlZS9fx+VBR5t7PP8zxv76Kc+h81piE0PRpDgxZ4NN4t0d1hV47bICAvbpqGKChExZVtExaYAIpzVYMcp+3y027sroasiU2ZYRwCGHmmaIE1SJ9I+1MtUyLWYqMpfFXxj9+9nb0HnQ/vvP3r0aIOPx+F16P7JVTIyjIGRCAq0YJwBERkyopG4n4fSCkFGDEMXNbcGhU9LyxMK6Vhr8XgilvVckKVhh3y+Q3T+AiYmE9lkyowZJouwojNM+pmqSDsOG4ICgdSkWrR6kb8qQBLe33v0aP/nZPLnb0TVG08im3aHzRAVgVYOQmbcCWS6ZVV/Hq4bBxEYnqxsBESV3OZoAT2MsRiSRSkQ9OALsHkFVVJHvHCcivSQkauKpLsVjqE0GoYQMALH4cvBNJgM5uHOMWNK8AYhNlk7DBX0Z5HokSN7D6gCZOInaSVOMaTyxI9XMvjTp5WM9NoiktuDsT+MNLcBGZEhPgFMYG9WDSMQMAy4gMxCSiTe18jznia6QsK8a3HH3epQ82Dp9uaXH14bZVhaTEqbfbtrGKgXvq6IFJoYRqkEoTNVIqjGzQc//fTgIS0kmz1eV5PxZDgQrwjzYVRm7G2HTXOiLIK8GsaGm5Xcfk2NQ23hkAAiEkkJQEQgpRhSwnGxXovADQVBuEvqQkPNC+ufbRc6N5uZIfoSZj8t/YhgyEKLNyg1MHa3DzLRjIRsev9WhGSBfti8v7+yIklqXCW3hqhMSEZlciDiKv/6DjIh6AjJMsay2yO53YrqpSKhsMoLkBL89JgAIuBqYIJFiRrBs0oXouDma4fyd17P32lmciYCwjlyigS08t+aXr1OREYVsrUHFY+JPPhoYwWG0h7NBqwq/IYkSWTT02wRNh6sXAJVp8J5EAlDC4jQJB+5DQEOE9WEFLGhkMzyvDbU7nQ6xwxERIJ1Ir6gkUBlxNDL9+5ZxWJxqZvpL69+ERJdQKoUkddXHVtcAFVgqyINJqoSimuiwlfQVvgNfoWIKBgHRhAlEg+xyKanMuRuYAB+bsYxTrgJG2rgIhFRG5wVGgQEBP1aTaQxKCIbITPQsTlz7fAuLCa99vylIhjZBNtqYwvpyMaoW31RlWQcliKDWriTvKrzno94SZLJw6yWikgwLlVbymGTJVtgXFDj0MlsYKg91h4auUhFYs4awxp95BHUKiIcJyRQl4pykewpPdpx5kxHZzrPcEwbFUmY5LQOsMgm0Gh3+6E8M5asJCTIVEE16FJkUpFwmFcTF1n2oibpbiOBtREIwz7SECvpcjKMLlfG1hjWhVA82d6fg9aJwSOBXCKRECQQebpOJOfFUEJ+v1fxVESGZJLYpTNl0h/evTPNTNqVgxUf6RFUwWy3RSZgbHmRjVgTcYp1tQVLkU5FJInXqIgMi5ShY31EVjQsmbIsh9yhUCip1p07DcE44GmrwoLIUS+LvMPOOvwCqRgU83HcIYADHKjrwnvf7zRRkYUrF8iMZRikIUCByDkZVRmyuz29SEVs2msmNWkk8xURzPImFfESERNjLFfwB0wNI0J1EQi1SEK8nTrE+k9lu7ItiYQOImez9SJJvXssMcwR4AY9f6zEcZ2bt2cObC2focz7GADCzNGQdDK2VFQlUifir9Or0oJqPC/a3a6x3gwVCcH8RU7yFMAXa88RHfWvm4Bbgz1kKHZ8aFiendVnzexXkJKzsKs31IsoUe7OvXe5d1+AY6RYWl+f/qxhZne6OLV2j3qM57fvLOzk83CiZ2jVRB4vfJGpbVuyp9oOfyvCgkiSihis253gA+zFuAQpqdvTFEiJP0hl9qpj63SCZeHoNjIsi+G3mZ0lIufPl0WeO3LkOedBzhofXyh0c1xxppGbbxovrl/fPLGwdOXKxNW1yTPHw2I+ny8caeTSabtIWPHxYw6iyB+yRSSpOgPqur2uSdxizqdSE5j/Gbcf/gQRL1dncqqtN+xXEPBorvpIfpZltYACOhQzQTjv9XoHCc1wf/PczOr4+Hh6pzPGlT5MXZgfGNhenyoUri6tXtldXCudMU2WSxfWtu8xXL6yuvvJ7osqSEnGrq0TFoMrMXu8TBWWRlXLVOMGeIQ0quMnIqo3y9QTQxJGQG1sHZkFhwqz58+fzsKzhqOWNWBNLQ5O5Te3V1fnXx8H8qkPrySKzYUvmwbyC4XVwsTS1nqhYJ1sV82I07m1dWlwszPtZEwJATx5wGNWRYKkgZoozPFqzOHqgZgNmKgO+JGHiAi8jaQK3iGmnn4EYF2sNUlqdpY6nP7q/Ffnzx+NZHtPDgy0Hrt+4sSd9UI+39r6VNPO1DhleStx573xrYELsvCDEGtsbOxFSInAFhN2OovpdPpaGtKv0ZBWnDBTk8iG9RKRPBXhoNvrt61epMkBRWdRDbrgrJBcCKY4fKo/hgyvMeZk6smR9wc2635UyUjf7Oxsopu98MqNg+k+eJZyuq9vqvXc9d2pqTuHofSLxacK2xbROHB4YTqRKN3oPx4QCBlZhhB5spP7nc57aYABhgRFU03EAEFUQekFkQLc7B7KN9WJZElNhTURoxoBzxidgFljjKnRwPyJYTPYAp2tW79Ut62BG6e7HcXUALC1ecC10Nd3+NKlndWppiubm/ntxakvB08Uj4w3ja+fO5VPAKZQIYgR5nVoMTeMhc5UOj2zxjBRyfTk6OreFkT1IS8ubxZnZtbqRVim0Yv+BPzXf6ddg4fZbhCRrVutj91TFl8iIumTC2WR3fxL8xDc0tLS9jY8yD579ojT6j51KmFmMkINXlYFXmHZDIi8Mr9T3M7nX2aqtJkCqjIMItdmZpZuL6QZpW5sNVCrepj/gtZCrsFixei2xh02VGX6KhGhddTXWrrauvjSXeelhaVdq5DqecFz9uxR0FL5RNdQLJHwBCsZ4QEY26bidJ5sBq7lp8i+0Z4b1oJBP8zHKpg8kN/dPHZsq4mR1drYavx/IjnBEGQNId3IWZarPiMDC+CxuZj+cnmxeHVzs/XSMefLfR3HdR2xrOfo2bM5J4FzNqCAwPKyLFDa+8f8LSykpKFsspQ/AJNTU0xdQljw6wjVb7rQI3d2oLRYtTa2nE8WcfY+3ftklcYYayAkG6JlPZaRpvlzAwfX4JZi8ZXZr/J9ahs5zQOArmfOZsJkgWzIesKC11BZbwMhlxFEOHl5P4jIWWfn2s7OUp6IxOwRpPo97GMHyaQ9f0+pdWMLiU8QaQ/Dq/c/UcQ3Jl68eNEwFOv9Wo9Ex4ZGr87Po6kpq+/lxtlEJKKS2ykxQMhGG52E/hY4JAUSA4gQwkLmokc0eAy1FXZaE1MTcLxPwUso5fhZ/5C3TgQoHaAijF43A7It9r/GVD5YFVH+INxcY+KoogA8O3Nnh3F257E72xl3Zt8su+guUJDdZQssCGhBpbRAG1IULVKqrVp8tAUUwULqG6pohWptjFTF+EwVH/ERn9Fo4iMajZqoP0xMjIl/jD899+7ssr7iF0Gg22W+Ofece+69U1V0Sug/RQYh6ADLV8zRBQZgqZdtLW+tjkQkaAovlUEkiy9dktg+F5PHI4GHRH6pxitRd9IpCOZVTsQ6dITO9sWY7lsONZ3b0Glbb28T7QGqdO2x9ebpDTfdACISWr//TCDgJFOmVgs/jdmLIn4DfqraSnD3UWEjbX3TaBIPjY/NFSPS0iPjBpViI5EuEDn7EhAhra8bW7iT6Qwb9jq9+o2KSGGkUOg6H8wlBqx4EOLxnr03w1y8f/+2nYdKRcLtCaqABHPD5O7mE/unN5TbpEJIWAH2vnBR6BBR1/WH01SXrSjSjifW2vz8YY/1ZZxqBBBYa7TZ+UTY8MM97awcpi3u7Mm/sTMi24H0pSCCGIILOUKAT4qERNarsBRGD4UEQEXIgIjwXhAxwyByx5EDJwfcIIIKA0r5y2x9YPfuZ0YPwNwuSYall8FpC472mZmZhYWZtbXDBRGyLnMKyG23dUkKQQQVHUWt8PB5/JWV9YW5/ZwekAf8gtuOgT9WnXYiglRWVPkQ7BlpLBtW8okJcgJgIoIjgPDYcgePHD06kYv3l6ycVM2ginhszNjkWPcGICPhlR4qrEeYuTXb4uzM4uLM2ipDWRpRE+8EChg/9L5Ov6IgVsAtuo2Q5gWdlf0+I1s5T1tQPT349gkRwYU93I1AEtKc0kKC6kRIhWiwPMvqShheBukuYBGvJeJz4O7f1xg80FA+kKtuY6zVHlka31g0ieIu/Jqx8fHR6Q0bZghLS7bZxcOL+D/mvdWZlxYhKJZIi7e2tiUKvwUKvVcWgbMjCkJOyEeJsWGcDpSHqpwtRGSQiLBCJJIFj2Qjpl3TNP0sQ1UhC8SQwbIhlmUTMMMFEhA6LNLhRwSJN7FIe/CmHUdGd9fAlrxoeQQ0IAD7lErE2YU7j7uP7J0Y3wptGx5Ic/Nza0trL82BBVz/PbMMwRLxwymZB/otQWRrVayhAIiQiJGR5cCH7qLhF5Q+RFvUgwgGRZUsDKuEmU62BxQnks6iVBUulw2p+HEdljWh58CHPiIWkWoRQcBJovv8IHLD2O5b6yFN8+M0IoV1NprJ9iVjMNDzVNx103PPLc4vrl22urq0PLu0vLj8wLNMCYNExF4r65Iz687CW5sRzc+m3Y2yJeJrIQHheYcCFxuQDISK/W9PD0sRlC5e8ivwEk1TEAqfBT/BZyQhASKCQxKiKLznRYEI4MwHhIeXqr6sC9M8hqcy3Ul1JG1/4575+adPVFWNPpd5CVibn1tamn/2cKlEH65L+YiYIKCqqtfAJzS6i9Sdgkgjng0NCd9Lk0UY2gKyPZ/FIo94aGgV3mkmNBk5zzKoMBYJO8ABPsQQCIMIAhG/F2e7AdZmn6x5u8CioXnvEKSu7R/cw9jeg7u/XIfZw8zNLc97mH9Q6ZNEWc6LUIiY6AgIVTKEqIQxk7g0o1JeeaEwkezrITMJLPkVHo8K3kCiJiL2LJkSsYjzvLyIMyTCeUPEL4TMdCwrSCjAa6jTVWBs24gLZqpShYVrmYX5wy/NzSzOLa3OMp9dc82BUaZAOiqrGYbQCR/ZyNnw6BIWSbcLEVhrWSY6k8elR7dkBskgzaJSlheKUzusbXnNwWPgnut4jguEIagIckLF0cVDS5F8UMUCIUXPJF0ejzvtdPDhSqJQUeEi2HHBgZA8+6xted42u3B4de3w3Nzhl1Zta/MM/HzyyOjoRAxfdNgbwYt7s7aPwTRUYBE5L0IphunAwOA67yrBXjSvh4BuIjnUgQDRVL0JJRB+fqE4t19+8PKeHomIOHH/IiGkwAcOTyBCIuIN5eElKjjQCsTpjcHKlrzAvraGzurg1A8//OD50Hb94hxc/D1QTF9aBRFmdia3y1bCxA29DOM1KURhJJN01c04QFZEMgoLU5Xa4vIA7mTJILSDSAND1jpOv0/jeS3iDesAbQEiOIaskeB5PMk7fIjVNJAxINtDEJ3zSER8JpVtjMW4qqNDQ0PH6T1tm/c1t22p8QyfWvrk3WNXTZWVlf020oYnhrlZz9wss/CsDUNfkRouaZgmHm52McWNet1LMrmttRO6UlUGETv+rUhLejyuQQOl3ZZDu+TNMiCynYh0QNES/IZuUYzIlQedCCPDnI6LF6+LWCSiKhde6AWRwVgMDAhYpDyVCuZS+NkiLnXs7aWpY+c/emrqNPfCbWW3bee4Y28Nw/Muwc2wI1k9jH9FVTl8keK4HITjxOi2mmY3U9yoR7VJkiOtWxi3EAGoRgfCkar0eDJGGK43TUTafaokefDQIiIRh6qv8ztHW8DOIiJoPG6UIrwW1oQwJJuZjKWpdIxgyexLVW0or6anjv16mP7jh5Vf6O+/Xbn92Gsrpz964b6y+4ZPXHA6NdC9d/KiXbkdk7nq7j2pj06vHOBah1ae2VTdciVuUSAhvFQBKZsf/jC4GjsyaUTFNEQBaY9HN4wAFHdioquIFWrqgAocVsnUCV4BPjnZykJEKJTnHEiSiJPFS1jNC6mGkrESepsboNXkuKoYuB+b+vYB+tejT516+qMpbvcv9Asr9LGHyu577PS2i+jxyabc0Qdz247nqvuP0vRKOResOpfe9FFqkNoA1NQw9xZF/B1EpLfBqsEUEzEpjMeDDEPgNYXP4NqrIuTtxSK9IBLlLa46zymy0Wg7bZGxRGQewBJs0pNko8lCFDa19YMCPXX+448+edvUt6eBe2gs8trb776yci391qkp2wr3eFnZfQ+cpjdyp7lqrv8j29i4veXco8HgSnnflo+gPhzdnrQWiQwqiph6vmz1MgQ3ZevAp95GwuVJGobEn41UB+RJWkDI3NQEIm0QD97C74zm4ay5PVkQgZOTrnQjqRcVUDJi+5qb2/YVFcoIU9/idnPqVF7k/WOXjFZt43bFV7glELnnNN09sMLZKjpPN44dsjduJSIVN6yMAs31G5omp6entzHRoogu5ctWJ0PwUDZXxGfWOhyVMTlhqDyLWEcjni1ZhHywWs612QbvpaJO3XIoihCGkck7DOQ3PQQQqe9vADbTf1EoitxXVvbQqROV9JErqqs5SOyJQzSIPJAX2QkRYdJbqyr3j5eDSAqL9D9DQ3Xgssf3Dk0fv8DFtBRF5Fo3EfGsN41dDoysaprh52XEBmIgKOC2ztbMcW22dhApIdN1/XKxbHnyVOBY1LRt3bKR/rtCqchTX7z2/NWpo+X00LnV1TdNc9zYNDdwmuPuIyK93MQYlxvdX3730VxuHEQ+6uZSVdfQwZPB9B27G7Zv397PMEbRxJskIsy+ogiDHL6Aw6krWjiMRSK4HgisJVLnSRdF2ruy6esxQdpiU70HKzS31dmr/1vhofue+PS157mqDTZXvYuI3JoM527+qOpEeSW3Z6X7XSJSMZx68KOViVQydfNK1fE9qc0Xr4xA1ap6Zkcqc+Xu7RiGUYpZonYxQD/Tts9DRBjochy67nCyqiYZIIIkvFz1m5Dttt4BbmO9y6TaM0SBsLC2fD1j5Qhd3tBb8b8KT7mhGamsyNHcoBR1D9Opcprro9JxOpWqpJKpFDdX9tAUTW+2QWXj4mmqJYXfvrwrB9WkE74e7qKoSewBRy6J9bKF8iKbtm5t668fpuAgMBYQ5YAfyZpgaH6EDJxGsoqQWFdXz11Uw3TkHb5YXFx47DH8FYhY/I/CKzXQiLhdQGOHKWgOQCIgCujooACkomV4/ZOpHFkZUUC0q538SYddkal2kuBXnti/fWTL1SWHn2YYD63ttoF4PJ7L5SgtACKy7HcgpARMTcG5AWmUFOELKFpcvIZpWV1dW3isMEWDR2PwfxWeIovflrZ9LowbORyRAHwYKKCauiTlrwf64rAXes5r4W/dNjRqPam1jmKPgDFhb/PkFXfctIWhiji9uCuEkwBCK2XK3pgqyzKIJDSABYFGeAXCIvOzx1IVNg+sAx6LxSqrN8c661ppKDfA0kP/ovDSE1++dj0YeCrsmCx+HkVQMy4ZQmGgBF6lw2OzsiT5RSmg8Rj8ebgMeKppdBc53F1HsxcD8PDZ1C0TY6UibC2+TMiMuu0NfoitKGOIiKoBsE90fctmZmFtce0B2/zsDOkZY5g4PdFWQRd46J8KGTgsdNlLgFZFgsRz+JyqwyEi0xJhJSnAAw6VwuvnAD/8E7zFF03TKSihAlUEaXaqwKXPnLhlsqmGodTSJsUi5oNvwUG3RNCz7orDaHV1zh3ffO21j80UHqPfHAcmT9Z3rty5ufjY6W1/j4LLgz+ljQDs1lkIcJ3nwVtDxCWQkB1hS8RvT6q8xCLC2cj+PrzRp/mzBQWVDi2qwNVNeWoC69keZSwaI3mRyKAsZ3qvf291dTYeb5lbvmw43vqUbyccytoIuw/F42M7dgxddPPd1XSB++B3P/H2+TP19fWdvVtV1R8Oh++NwDFKgKU0hyLoLRnlPIRFRCwCZRHJASEv4vfDpguvI4wYDnTaZ+HNXmraaPubSKSleG5YN3LoyNDJuqZ9AVTMdpOxSEYooD5Kzb63+lRv79zq8j3V8Wrm8OHq+Mnxm3bWDN3RsHP6OPNwd/dQ65GHjx+9aOJ4ikQEg2/i4zQNHumorrcbYTgJ66DgGQId7mQirDiA89S8iCDDJxMZvgBCMn4KDUTcvM+ohTrGK2Jj8NipU6deGNmEI+Kl1jEpF5QtwuDYge7tt+xs8vioAmfXFmb1Piwyd5F9cHb2La679zFMRTxePRyP37Jz587+6fHmnXuCFz1zy56H40OH4IR2fLqbBpYXFx+gPwGRR7FIWqdKYPEyN6KypkM7G50XIUNLU/DQlfADmwjhp4lYBYsADgXZdz371RvfnXHGGT+++fo9qeCwTJUQsBcDEMXnknVNTHj9BTjbCWksspDiquvrR8b6Jx8j3HDDCGTEwPhIf//4eFPdbi73TAOU6pv37r+jd8P4HfQMZ0cIBeklXPuxSFgs9TBwsuLHP7QESIQQ4nVZwUmoRLCIii68EERMu90MqIbYl5v5+oMz1vng/rdyLaUiJS3JxSN7jzY11YdNqkA4XTq0KlK7YHhsObRh9LG+sb3H79wxsTFOGGobGW8tH+JSW/cev8OV27+tLccNbEF57qZP4drP0QMoVPxdor/WNE0W2iAJbrqCl1si4gU5koAkERzI9Ak+r8+Hz5Tt9pCT7dg888YZf+eNmc2Z9Wy3UUVO3jHJQFRgs72AUch2D757J7jykbGtvUdSo30jE3eNjuw4Uh0nlF/cdvP+ix7mFp51AQ+4AAoVuCDF4bI1RXMhS0SEVb0JDzuxcMfUAEQEylSEDyNVAQcFz7gsK3h9AuoyfD7YYj3LSAdf/PmMf/Lzi8G+4jF7MlsIwGDnHYcOgUivtB7+YrZniMiObTCGRlv3ZA7t3rNja+MtRGNkfM9dbW03DQwMzy6DQiXZ9tBRgY/jpGydD8v20EETYQtAxlvkDkSFHbrsABEvb0LNhdIr4LGFcEjwvmWmD1LkrMyuN8+wePmN+++//42XC9++uStp3XIlm1Wss9xNDw9N7x1ta9r3aunIs7BDSzPGpe7ee3PwyMaRbduGbmqNuXcP3LO8MAPdyVMLC60DAwNIi8BVBvpAxMcii4P7CmXr1oOw2ROBw2cFTLKwdeSQKd3hJSJ+XnB6HbLpEEHEdCJkZG0Yxt4S7Rv+xrrs19+h87zz+puWWN5EjCjZRkVRAgElEN0yNHby3NGdTe71HEEKUwBeP8kduqa5iruZ3bY/9+DQ0Fa3J74wh3cpO7PoXkA/zymAh1cFkXACE7rwwgsP9hTKVvOBkGq9Lwwd3C1JutehJfBUruO65JBhqZbMtrMqi1BL4aH2vl35AHxzJl3K5y9bJnBaJoFDlunD4O5i6NyTJyY2XuP5Sy0o4Bapk9zQjmtGuRGqpi+XSqXq3a3xapQHRAwUglnNp+kIiyBfIkEeSNMPHiyUrf4DtWyxHir4kE5y5BGcIq+7bZIqm2m8yydJIjk2Iq8J5m/+/Rz9V7j782EKZqhKOKXIPkLAz2bs2LhxpGnjNUzpPDNYFFGok8HMh/018czTC8MX1fdl0u6OaFgRCyKIUkBExz0x5XKJCYkl1B48mLLKVveBWisieq0mpcn9Tqa7omLYG9Cc8J0YCJv4/y44ApLkwr/m/YpUWwjHP3id1OPXh50g8CoeR/Ijj+AN6HLyJN2tDFWCryiiUeVfUJmrry5WPNEtIlYLIIKegFIKIiz+XoGICCLxMA6CiFW2mg6cI6jGhYDiCzuR7W8waX8ggP0qdRBp99gI1e+cQa6W/jfOJLVrJvPIvTjUxKTLBmzaQKhT0T/HVqVObcKv9EEtc5KKprp9CKkOGRHOg/BhEQ2f+blc7YnE5T2G0XMQCFpl66IDjQJkumo4MeLfRVj8735gZLRIXq/otllwb5BxRf87ZHS9sWv9eu8lI/LcvIhbiRjQn4Qlr1Dsf5PQxkfJg7l4M5UC4Jf9yciZxbZRRWF4bM+MMbN7xszgscdbncQbSRPXduKQGJqEkKRJSEOrKGkTQdKmaUlboTZJoZRVFJUdQQWURQiBEALEA4gdxCIECIlFbEIgwQMSAh7gCQkhcc4stqkJ8EtxW3fzl3PPf889995RMSQamRHgw+MukyrAr3UAGfIPZbOAMD8eN+ZXpmzbajmcCraHOFMhIeIC5fr5otOxjcAVJqw99ZCAh3jrA/K1dx0Qr+ln11SIGkkNpM2jSFLQELg+aD857osjsBKHCDEQjjQBYoZkVcQuNEQFmrfwHgnTA6+GM4pK+ruyBpmxLDhSsm2rcDilWBhGBGyGhgSIQ3no7BVHYAWFgcD9VZejgvk9/9S9nj414zVVA3nEVT1Jt9kjF5s9dQrIcorFDiP0hYMG0MTMFKng0EIQnZUARCMUVc0yKiiS9FcAxBI/n3Bs61AKKbKXXRYDjis9eDOQ5LK0ExGST0B4KhzPQ5hsed9DY3KvrzfQCQp9tWIEbeusNsz2WwIOAZ2qDCV1IRwM9ZMIIvBE3ADj8RGoosyRAKGQMKPB913XJdUUl8MOQjYbCTPz8ywOMLPaesDtTh1uhrPkHKdp8B8K+OFxb9BOB6yCFLAqUgASqn5kYaYfuuOcBj3zEDoX/oE7c7WpTwOSjT0miNkjnUkGgwwjKEZIc6rhgKITIQOynbWqZbkfTIpVRQDhM7rel7LkR6Wy2cTKSjhEcZGVJbdtW00AErYPuDCkDUJ22aYFoTYUPCcl6JoD0mv6Esznd+yBK2+nCu7CqepPOo8/s83tNkmfp4mqvv3WOVy++RYJJnpGJ05XAOpiLgjZAl8gTa7wuCiSImxWr3L0S2at1ZRV8JAvSP/ww4JTbR3u9pFIwQm+mBmFK0lQ9RQ+H1YAaEhgmKKTIl/h5A3f+HN64eUh+6v3fniBr7UT8CNO8F9dSlT18reaDdKRIf5Zfl2xp0nTtrKyLDd3kywoZHN0J5VYTPajGKTAHo704Ye2bSGIxjAxXwzlQiVEUhRsiyrq2cSMGRu5Zr4vor0iyJGj09vhkQWFXTumjx558P6HH3oQLskvwu+8jqjRumz/dsixrfZ1QAKEs501YtZhKSBJKkHrgwPGkCFJxnis2Y/KMCrufbJhjUi3OdXWoeYcgEQMno9YIFxYiEiEne7+Zr+rqlQOadzfoykhSKOgHXlHwZpKvvHWDS3N44AI64FMBgWNckAIvVmWxWAQQYrxYmleNThJEcU4SO/yZ+bCSkg3wWP7q9VWcz8Tp0C8boEUJVAilGs84ymp2iSAoGl9+c8gR/YfPQcuJ2IF8727DiRg2RaIXw8kUBKCpkZs2yqCGwebZatJz4hi+0BGlCIa+JI/s5NwtHTY+5xlW03NCsNhZcWynFVJMZIU5jiyAaQPJlcCQL6pRmRxbfHvnrV37zPnbG+MSAICgrYFSq4LghszKYhAIk7ANnRSngxm8ko6zVC8LqjjYF8AooyIIj8v79zppNot+0fd3jNM27r0VgBWGUVhImyfExKcJOUaQnM3vnIqR4r1ObJ39aHVvfUcB1ZXDzxzoDFHXs5Vr/L1rwfiCAqpbBha2v2pkXxeS0sUSmiHFRGAiPOiOL4ysxOVLxEl2Q2ybauwHZq6VsAzzJALVcKIiJ4qh18JFunmmYgEFTVO7JZr7TnnxMFnDp6ocQDY7OLqkRO2aw1qhCOOqIJU4N1GkQgiD1X8/mQWQaAWi/jS+TyXDlGoeBgXqRkRlFlZyfct79u0obXgtuTY1vadrGb/c0HGXnBQQl2OpAyYvkKqeeJmnnZFnXnEPQEgw+46rS4urrrteeTs1CN1IXGBeqxsp5w1O6/pcUOQoC6VJARpHxkYSQuwIps38qiRnXkxLVKocRYgeMDIl2h3VU7f9B3btlIqE6KwGRBJVFwN8vcFGS7EiKSAIJGSq/VNp4aPwtCKuut0aHV1vzOzv10HIprVVpvlv4JGEpRmCO3tQojskv2p7lylYoIQVBxnypX2vAViAAhPofA+cf8m86574zVrx7aOyxILktj2IbqRowsPFukilG8UDq12aXIQsv2/ai3M9UGOcwbOIybIWRYIaFwnS8VcKkBXRXQFAuOEpZWMDaLk85mlZ/PJvib3Onfdvebwsm1rU4k1053yuBrVzxiawijgeyRpYCdFlUqFL/9H9ftlwVV6Wcc728mXv32kWAeyc6YbCGpqMkFIf8A8q1QXkZiW75MbCWqihwizSVewbMvNsGGeBJ0OUfT50lKW4zlJEBOqBgs1jqQkNZJ788f/XI/8eLLVlXrZ0l3ORYpOzPbypr8jlHvGNnlaCaopMN6esUBW5vL5mzNz0b8z1Km3afKiW+dutpt0XvcHVrWVYENYf8VP5wiSPFQ3PB+MCWK7qgOIgp0MKhS95z9XiF960TOSfH/RcXFnbVUuy3QgUEPwmiKoQCAEF3IyRAWbWPnltiOL+/8BoSCX+iDzESDvNOl63U9YthVhw3qIYZXTkiOIJ3sVAMlmOVFRARa72ozKi91vvvcfa/bvISANskDgIV5wo7Gzw0GwQeCy+PRZwNAy/Ox5U1fsXhsdXpyuj8F5m8oz+Ywcx/KFDSOEo2ebHdtSWVPC5GkcCCKmOT4cE0XdBIlBRACE6n3F7KK8tm4X5VOvq1H0NN5SOre8xdso4ujVhzdO3D/RsuvA4uGFK7Yfmxg95YRiH0EtzZtKyngLiISKZSegxFk2PTKicv3utyzbSrBqSFdZBdYDeLlpqJhrDnQFFQ1ByHSIF2L4HBEAkSL4BW4+5P3qX/ta98Bmdb08TU3n3npReXrPntaC9x9F7J29v2Xvw3vWjlxx7f1XHDk1va1jsXeq0pdsb0cC0WhnifH5nEzg1M2iYNELfR9d0LmbHduKm++bbRoqnoiZCgY5csQEEfhQzBDFhEGSCZXiVQnnpVz0jX/pNL7hrRtYga3Lyx0d5061wp7N7O7RRoZW16ZtPcT5s8dbbhztnbh6//HC6Ozw9J6Og7OQ8/l5uP0BIKIILaF8US6FcEccxNX0rGNbaL+1291w+k81+GCctEHCPBfD2hOaAUE1wagqzCqJge7o6+v0fpHjEteGunDsGzveMdXy0Oyx6b1rjQijuH5vIs6anW45cXRDefeh4w83jc0uHoX29RiAzM3rfNwCwQVwlxISdUUxLBAtLoQldodjW1Mqy5CEIx6MikhGOBtE8fG8JImiIMFEopqKSCMDAynvV+t047/yXtKxdfrY3s1IcenxYw9tu+LA7raFEz0PLhxYa0TwmCLadm1peXi4XB6bODg2NjY1OCPA91QHkst4nr8MxnYaP1uqoqCG6DD05hMqjC/W4HYUHNtaWsnUOJiYitfzMhYIvPJmtgsqRbarapioyHiGteJpdX8Ked2g71/xwsMqBtdOleHpgosT0TvO2nLq4IEbH96169j21q27F6KDmzp7OhwEFN31CZUhOsprB7aWy8tjqDwe86coJuGA4JDA6yjJlMLPdMHfLEmQ8TqPyuxocWzr/Q/nCBTJWSlCWcdl2XgVhBMZVZKkUDHgqWrQe/Kexh2rQnTr6ODg4J5FvAIzOxrd3bbl2Nr2Q4UTu/b3tvUeu5GGT2515po24kMp+/dtBJ1LAASEo1yOQ30SZ1AipcTgF/D/8wO4DavoQUlS0z6FoLEf1iXwtnZsdWxrZWR+DkabtW6PKzGDIPAyWCRig7AQEbCIUsrzd13iffO1b+qj8dtzJ+d2nbqWBpLOxQfP3zq7JTq73HHx4qFDO3qPXnt1JxzWGLQ2cgmFjV25sSYClrFiub1c1hhbQQpAhHye4YlJGpXMSlAlo2QPirQw8HquY1tzYHBLMG/4U0QsToqxIEHh9XgGr3+GfCMjMQ3mRE+jNi1vLES/rnJ889hj79z057lTR4YHUVt3N81ORY+Mgk7sGN3Sa2pDF8lnWYMEZbM1jg7CCPvUcrhcJhhRlmGdOAP1ezgmwdiiq2rOZpFD4Ck8/6wFs6oqhcNLS8+6T1q2NbODROFwwqILyvaED/YPJZ9BaiyeXKVLZODvCHDOrmNqakvn5t43v3f2dC/AAH8Xda+uAsZYZ8+a6/jixdOjtkqlONRuPAkSSZTh63A42gzCBypHyss7mS7rJEkuWaQtJSmiiKcykiHpSgRJ87zimYRutGBg7318acmxrfMmSBSMKfyBUhQfKAsHTlQFmi5ytcnpOW8DvO6n8Z1A2+M/f/bLLz+cccG9NsjtF5wB+uMgvfkYgCysPuwyNTqTDGUT4SA6ezqtkVXpMaUMEH1qhI3FCCroE3IGA9KKfHuJronSYIVIha6EGVkwb0nhYPdUOFsagERt25oyQXDCw9eQEs+VNEHCU89UbmbGTEzMr21wL2FD6yWnpp77+btfX3CO5dz03BcmxxfPYevyiYMHaXoTPPMPz5h0kQyMa4okIcAMATLSZE0cHlWHPqK5HQKNOV8yVyERBSVXJp14pA2K8kEQIGM5SbNAJI9fQApIEJRjW73vGySKj8ELTDd9HpRfNl8RYR88084FB1ke//2td3794bRDXh9c9Rtw/HbVu7+8dcFJmCOgTJ9px2wV4pQvAhgk7kYGrWtaoRqIGIsxHOEI9pCLOdAMw7RrJOujdBkoSpSaTgsUlcYr+hwUSHEMbIiP9A+pguJLJ5aW5gBmi21b3jlW4EiQyiakuF4y7TGArw5CAc7BP/qkE4TTQB49+dWZr3sHaXqDF7WNTjKKfcXbJ5CmIj6BAIV8GB9HPl9tGsYcmUSQosBkSEhPKpSR4D1Vi6QTFBUDe8L7dleadx0EHo90JQjJ+tTjS8uObTFWFaYyXM4ZRxdu3nwxIlx1wVvvPIGp1Kib7n7il/tua4pGC94vvTQsMdpaLJCgTtiCksFU2GeY5Q8MsRqIohI1UUK4H0F8OkMBOEy/rCaA04i8pFKUGrLGlhQmQAMws4hChCZIU/zSrY5tiUOV7mY5YCGUezqR4ORz93707t03/SPBDz98d/1t3R5URzTaAgf7bxle2ExfuBFBOunl/tpNPhJFsRaIDkOsBiLEiDrpSnwyV+kPiYwG4BJ8eIWMA4hoxGCOj/B8CApwKWGCaNwAyUt0RdMxwLp8yG3bFs5SiHA+InjXHUaI8O4Nr/5U8F445rG1JxodHLvilmXatYNuarMisq9a8VBWmaP4AMQKCcE5HEasHiSuKJEBiQIFYTgy+CS2BMn5fJyoQyfFgLHFwbLb8t8BVoWISAFaVFlJIIvy+27btlrHIBSDl/z7MPr481dv7ulpgvO1ZqW0b8cEankqOhiYwPXsAk13IkgH7TeqpbQFEtHuMiwwX7+BYGGdyPl5qQ5kAJsCMQQRspD5EbBcltIRhE9zFA/Zzku4tRIMKtQASJX6aZA8JIMAxLStfx1GH7zw8ecvXWca2HLPtvPgmblNRxeGJxYu8lhyRQebJlJQrQ/T9GYE8dC004SLj5ggvK+74jymsh/T/3ICncQfqgMxDTnNJQBAFUmF5YAirvjw+EhaR9uCCQFIDEUQQslKt5+2D5HK8r7zz+9wm7b1wDpuBASvPuWpFnmyp6lntGXq5oVDt5Qv9HsctUUvvRBBYGjJF5klOk3H7WxnikOW+4IZObcUBB78yqoT6hvaImRSNh1LxymKFaEwyobCPibsw/UoG1EiaSmiUDO5AI1yjmBOw9ofTlSjwLYa9QAOo6cxCH66OqPcvDA8PHy0s2XqfHivXtuirbIJchEd2OpFlWnRcEByZq6rAsMQlgQBV0XWxm59Qzud9rH4mGUDQDhIDlDir8bOLbSRKozjM51kJMxMJslkNtNMLmNi08Y0GLcb26QxmyatdBprmrRpXG1LsfZBqC2CLoIWBLXIUuujlzcRxDtqQUF98UVEcAVBBF9WUFH0QcEbig9+58w5M2myNf6xdS9pN7/5zvc/3/nmnGlQxpvegvDHnjnKoKBf5JqwCx4QHD3dkwjvwp5TsligCM31en19l+XNo/puGUBOcvAF4wb2CEAQCY87I9XULLn+WnFUy6CQ+D1xum1DluWpFd8UB4LxZismAXDCc70EIIIueMILaR5XXL6RrKJ2t8Fq0EPq7f7Cfy9bw+hVcKO3c844chBq+MQ21h7MFGM9IGrBOA8MLB5bPIdAauqaTO1XFK0pUaCT37iqqmm/X6igr406IKRsCaEtk+BxFZ4KQ2CEanOiD+EaJyQvvfTsizvmfyHw6FO1sw5vFPYeUZBAdm0ks7m5tIRAPkHd7nqKn7Bsy7nUAilKJI9OdjWPZotzjOZFIVFCvSAuHUD0WNbuRSKM1b3V1X4ER/OqmqoNYdUDV0WAKoWdPtrvdDp7E2NUVSWdVkpTG/D88c3NDfjfBYPl1wFEqatqwbKtkl1DZei+GI/FlhDJX82hqtJD3G3yUeaXjBwStbk5ONE5aSNMN2unBIHoholGs7EDBqw28MvASglCEyMEFB6GS2d7fb3TubhXvfM6AnHhzC6UaNBOj3npXQ5pKWXMKwiEgxmxatmWwtg66yYg49bz70XiuVKiokCYCMejzO0XY8xkGvmRYqUCGUcEoBdhSzFXoV2pUKkqbz2yl9vdRwh4HNU69VZrvZYC8TOLNBIzldm5zbNLS7qX6ZZraceYN7dJsk8T23KMVRdQFkegynLjkUbdK+xBigFbIvjoo98yt3/DY5m1JuoI/0cQ+FyhWl2GN59XYBeIS4uFhSRkXn4IBFm+AyCgI3BZAMjn4RM8oINA5KcO39pgrq6lNWMrW2e7basZqDhuJIr+aFyYXUBjKyHayROPet2+pAykcuXRb4eZ2++BScHszwTnl4CQX26bKii1XM1WYN0fhb0c4ZgmyeOlwOoQCC2D6ixVKlcut3OpGWvb7crh0SG+y3cayDnjBt764g6xrYbK234kiwkFT6ciGHDSmcsxkgYv873+zEfDADJ2SipQCsyQSgHGZNQjhkRP0PmJDd7xWSvdUfXU4lj2SRbpCS5l7parE2M3FqqylxmgzU3jLvaQRaoH+DwCyfPOEZioWOKw0LM4mTBDFcIODSBzz3wwDCA/D/XqpvMXrktN7JYLM/g42LKitttt0XOtP8b0SfCptw0hmRyPCj881HM3NlKQuubuWLnNx/SBIEs3gP/WLBBqW3yQmEEEApGI061yRWcGHMdMiOZ4GIEMIc18c1NXNu8iTQd28bVuwwwhhPoZqKWoKh6VtQDXMtmdI+u+0jS9n8Gz7tggkI2lMwZMhhiED5RJtSWFg0lUrYrgUi7BT8sa57vJArYC5gsYVxTk59vhB818lsKHIZfRcxenG41lTq0iuvYOgMjM1aUni8S2mikOqpAnEUieLXMWx0SBZWGOHiDfkmnMw2SIQBzbcolIUHVryGo9JQLi7MzUyQE44CAg9AfN3BYApUtFBp/rEDmujO7BpXY5LioxPXJrkZgMpsWoxLbaMI0oFghcAQJSgMhkBfcAENdSBerfuoKuQc2utqZQF4BBwiCTBESX7C/DxRekBwFpN2vl27HKa5ofCEBhbyYs8hYI105xdF9ZJCKdw7NrdBxJdmVVELYtBWe7NbTyNYuDK8NV5sMSM0CxNeMGs47gdzt2tVX0d8Us6ZkiIEyCIcJ5/znluIIu6YQFsg073KFEgw8vMgWVayKQrbbCCRaIHonAlgAN/WwcLAEW0lk1Vbb9N8BiFzWXWSy+vGiybCQyMNsZ8F+cI6MtmNBItRVyM75EGDVCRPhwE5AFJ9vh+37xJeG4fIlRJxmXNbQ6yWBEFHVNFH0WSMMYAhUCnOAlAVljuzQVZSJn9SyAWP6baikWyLRC7yYvQpKMDM72DJSNTYzeCvBNUm2JYR0ANM0vxPTIeIyAVLpOXTBvOBzfMehhGX/jZL87qUFfCp380BBIgGtTkLBEQBTW0YIH/+mUmrNuOHKBlsljkPZinsXKL5Z59LTVAcpsGIblv1yLVFt38YoYkiVbCYGAlBKMrfeOCcfxxx9ftqrfo71z0EGBVIiHUE2QsEB27iIgskZAHIySFrK+5Yjjv9umUrcC0WYJSQFlu2uQbW0a1H87PETEmhKlbmlJWvv7GKrHP7I5Ll35EUCI3FF0r0oEj4kHUW8S0neegnhJjvCUo2LPjt6A5b/QRmkSkJ3FAnnhMmSLIngH2haAWP7bxDkCqvFrNoSckPQ4AZlkbHVxfP3jHYzNISfh/QdRkRxF+x8RCEtAghI9CkRBYoJtOSl1YgiUh39lnV0ngSAgBfTmZP1/gMxb/uusdgN8SI7J8bCkibfccn2o2MdBjffg40s//XgHBoGC3xuMJpNn0aGiuB4TQ14rIhwBkYhVeFiqcfvNRZVAwSkbLeOdpkOrjLJFH5jtS9NoIsFXaZuWjTx/K+j6a2EEu/SEu5fjj2OH49IjdyAQXRgXBNm3ktWgtNRELL8sIJAUAXFFe0H89uavuGLbltoiJVN7mk4kKFsYeaD/Pkn9l2+R1W4ezsV11ZtzPRwfdXNc+hOBCMyaihXQk8mgJspr6akQoIRXoANKQGb9fSA0gaV4mpSNt3EKgHRY0GqetZRD2bIQHui/G9BIqVkgqkqbdBrjSDrJ8e2ww2EF5EPGolBGizH07Dx5RAFJERBEs4HXTCluLeTqAUkyNkiJ2FZZRSDbKhpaNJdwtqwMtK3Mpt1I6dirXX6kK4vGr85xGTg+/hNxvMagO7CuhJAESaMAYYPoHF/DIHluJeTuARnR7aFVIWXjKs+tg4kG0JBiYWzZ2VISMgNAzmEQvMKsOWVjV/fN678qxxUIx0+/Yo7vnbOpclJSiGIIxAsdJwwywWX7IrIWd9unylXbtmpwRXEwlFX4RLMlIGiDbAtPJJ+cWO2WA5WI84L46FU4vgaOr/+xOIYZR0HBwhiNwMM+bxFWOB6D3Jzj0yGaI7YEakVSBnpC1LYabMcKhA2CsiU8uNoC/yUgtElXUPkuk4jYznd83M1x5VfC0Q3i8lkgkbALJJZIjlzI8aq/D2ROtmOpKtS2uIvs3jTOdjq0ljmIUGKg/2byYFsdljTp6GpX7s+tQ4fjCoTjnR8wx2/D3SD0oalZUQIOL7Lf9hBZaiT7QEadVcakCu4Gmua4Tqqxh7ynQIsyngMkb5AZpAXjfL7DkiZdA4HwPK9rTJ++pBzHl767fPAV4egFKSGQBVHQ3F5BDHSBkKMOesUhmWWoRqhtpbhPAjvrGGTRtG0LR2+QZg1j/QHWatIF6L0FV18knXXUOx//dEw4fke//ZKOdAyCXWsOTYgh8UREJM0C8TkgXaexiW0VeG5/1CT1b43aVgOcQRgIcmgYR9vLPU262fDpHN99fXDwA+EAffkUk8ALynvRNY8rSC4xKLl9cZQjuRlSDpIqQ9c5p0ix4x5VVZPY1r45Wldp/Y7VhndXEVyDZ0TjQmefPXlvoRg9nePKwcGvFsc78Ps3Z2aYePzhqD6ZvhZFJI0nkaAbJIgrAIJ9taByEWva0HTeAbGtKK4GmsS2nlxX6/glKRqRhgn5JHhPJXgrCdo496RhGIVt/KXOarcinMYB6XHwG+b4ATje+WgGQBRQOqvd6oaGfRFPIoChR0VxDj0J/RqrRiEdZ19XRJJuByRVJba102LrpH6nIKrKquFMP4J+FgoJYfMc5FhxdN4ATeyTJh1d7ZYS/8HxvcPxymMIJD0lxWHPKAYZL065tZA/hFsQGsflc3jJpHJr5JKGWFsR555XgCwSTS7dYq13UzBpjrCANLLhol7t82Y24slxQfdpGTcUg8XSdecNS0c5AqKS1a7i6uGg648THMMfPcYvoqGFCgivfD18VyBAGpe9bq9XgDWZ9QZXOS4b6wNxKqFbU/YiEaqtXRaklgP0qAIqBosbG+hWJTDASRr65O65tYoybzgag3YradL1V1uU4+DW+x56EPLjR8zxFeI4mJlZPHNmkSGJiD7HQ0LGTRURRzmuQOaH2bCbgPTbr+texWnSwcA6xCBjWywRqgU3qM6ltuZ50OKFMQpwY2Fv786b7+kABwHhc9i2TH5Scp/k+PQF0H0HB5f/whwHyK+em1lEJ8SZKQdaDgUJBTTsz4pFAEE5gnYyhqVekKyXgtySdpp0HVLIN8aMPARKMZcNYwtOyx02THNhI1McM2ydHxubqFb39i9evPhAp7W+bBfMVbAtUqSMxHo5QMeY4zWL4/ix62YQx43MeWdmk0JJPRaO4y4dfLgICLhRJax3gShpyPkF+9ErsVGnSVcjDfn8mLE4DZoxQGfatWatbW4ZNBsuzAMq3JAr5FS2T7vb1LbQPf2V7nH1AtbB8YcOx/14YJ2ZMJihm7J2vXwvIgj5k+GED+LzvgPCyzgikYoKe7SZuD8Zk/1+nSEqBpwmXZOQbMGYyd08ZvTp5kL5gYvwklNltnhrkbjLI006eX6rBXLvV5QD2O4feX6EmS2dH/oXRUAY15vCDbwAAAAASUVORK5CYII=", + "data": "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", "public": true } ], "scada": false, "tags": [ + "trip", + "route", + "movement", + "tracking", + "path", + "marker", + "location", + "point", + "satellite", + "directions", + "placement", + "polygon", + "circle", + "layer", + "openstreet", + "google", + "tiles", + "roadmap", "mapping", "gps", "navigation", - "geolocation", - "satellite", - "directions" + "geolocation" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/trip_map.json b/application/src/main/data/json/system/widget_types/trip_map.json index 09eba54978..a596e74c1f 100644 --- a/application/src/main/data/json/system/widget_types/trip_map.json +++ b/application/src/main/data/json/system/widget_types/trip_map.json @@ -2,8 +2,8 @@ "fqn": "trip_map", "name": "Trip Map", "deprecated": false, - "image": "tb-image;/api/images/system/trip_animation_system_widget_image.png", - "description": "Displays the trip of the entity on the OpenStreetMap or other map providers. Allows to scroll and animate the movement of the entity. Highly customizable via custom markers, marker tooltips, and widget actions.", + "image": "tb-image;/api/images/system/trip-map-widget.png", + "description": "Displays an entity's trip on various map providers, allowing scrolling and animated movement. Supports custom markers, marker tooltips, widget actions, polygons, and circles for enhanced spatial representation.", "descriptor": { "type": "timeseries", "sizeX": 8.5, @@ -24,24 +24,41 @@ }, "resources": [ { - "link": "/api/images/system/trip_animation_system_widget_image.png", - "title": "\"Trip Animation\" system widget image", + "link": "/api/images/system/trip-map-widget.png", + "title": "\"Trip Map\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "trip_animation_system_widget_image.png", - "publicResourceKey": "3UKAE6mZvW6bnIhr7NAtYTG8FbIDFY1e", + "fileName": "trip-map-widget.png", + "publicResourceKey": "nF3ox4p08cuECHLQYNdnhFUpkjK9Uw7P", "mediaType": "image/png", - "data": "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", + "data": "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", "public": true } ], "scada": false, "tags": [ + "trip", + "route", + "movement", + "tracking", + "path", + "point", + "timeline", + "marker", + "location", + "satellite", + "directions", + "placement", + "polygon", + "circle", + "layer", + "openstreet", + "google", + "tiles", + "roadmap", "mapping", "gps", "navigation", - "geolocation", - "satellite", - "directions" + "geolocation" ] } \ No newline at end of file From daeb398d475d40f32bb8cc8509ce13ec8ad79085 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov <viacheslavklimov11@gmail.com> Date: Tue, 18 Mar 2025 12:11:29 +0200 Subject: [PATCH 13/14] Fix invalid queue admin reference --- .../cf/ctx/state/KafkaCalculatedFieldStateService.java | 6 ++---- .../queue/DefaultTbCalculatedFieldConsumerService.java | 6 +----- .../thingsboard/server/edqs/processor/EdqsProcessor.java | 4 +--- .../server/edqs/state/KafkaEdqsStateService.java | 4 +--- .../thingsboard/server/queue/edqs/EdqsQueueFactory.java | 3 +++ .../server/queue/edqs/InMemoryEdqsQueueFactory.java | 7 +++++++ .../server/queue/edqs/KafkaEdqsQueueFactory.java | 6 ++++++ .../server/queue/kafka/TbKafkaConsumerTemplate.java | 2 +- .../queue/provider/InMemoryMonolithQueueFactory.java | 5 +++++ .../server/queue/provider/KafkaMonolithQueueFactory.java | 5 +++++ .../queue/provider/KafkaTbRuleEngineQueueFactory.java | 5 +++++ .../server/queue/provider/TbRuleEngineQueueFactory.java | 3 +++ 12 files changed, 40 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index 76cd2cfcf3..90d4056afc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -30,13 +30,12 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; -import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgHeaders; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.state.KafkaQueueStateService; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; +import org.thingsboard.server.queue.common.state.KafkaQueueStateService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @@ -59,7 +58,6 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta private final TbRuleEngineQueueFactory queueFactory; private final PartitionService partitionService; - private final TbQueueAdmin queueAdmin; @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; @@ -94,7 +92,7 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta } }) .consumerCreator((config, partitionId) -> queueFactory.createCalculatedFieldStateConsumer()) - .queueAdmin(queueAdmin) + .queueAdmin(queueFactory.getCalculatedFieldQueueAdmin()) .consumerExecutor(eventConsumer.getConsumerExecutor()) .scheduler(eventConsumer.getScheduler()) .taskExecutor(eventConsumer.getTaskExecutor()) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index fe42684b2b..ac62576714 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -42,7 +42,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinke import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; -import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; @@ -81,7 +80,6 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer private long packProcessingTimeout; private final TbRuleEngineQueueFactory queueFactory; - private final TbQueueAdmin queueAdmin; private final CalculatedFieldStateService stateService; public DefaultTbCalculatedFieldConsumerService(TbRuleEngineQueueFactory tbQueueFactory, @@ -94,12 +92,10 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer ApplicationEventPublisher eventPublisher, JwtSettingsService jwtSettingsService, CalculatedFieldCache calculatedFieldCache, - TbQueueAdmin queueAdmin, CalculatedFieldStateService stateService) { super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.queueFactory = tbQueueFactory; - this.queueAdmin = queueAdmin; this.stateService = stateService; } @@ -114,7 +110,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractConsumerSer .pollInterval(pollInterval) .msgPackProcessor(this::processMsgs) .consumerCreator((config, partitionId) -> queueFactory.createToCalculatedFieldMsgConsumer()) - .queueAdmin(queueAdmin) + .queueAdmin(queueFactory.getCalculatedFieldQueueAdmin()) .consumerExecutor(consumersExecutor) .scheduler(scheduler) .taskExecutor(mgmtExecutor) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index e18c8171af..7ddc9147df 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -53,7 +53,6 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; -import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -92,7 +91,6 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>, private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; private final EdqsStateService stateService; - private final TbQueueAdmin queueAdmin; private PartitionedQueueConsumerManager<TbProtoQueueMsg<ToEdqsMsg>> eventConsumer; private TbQueueResponseTemplate<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> responseTemplate; @@ -143,7 +141,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>, consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) - .queueAdmin(queueAdmin) + .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(consumersExecutor) .taskExecutor(taskExecutor) .scheduler(scheduler) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 6d938d2976..85b8e92387 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -30,7 +30,6 @@ import org.thingsboard.server.edqs.processor.EdqsProducer; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; -import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; @@ -56,7 +55,6 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; - private final TbQueueAdmin queueAdmin; private final TopicService topicService; @Autowired @Lazy private EdqsProcessor edqsProcessor; @@ -92,7 +90,7 @@ public class KafkaEdqsStateService implements EdqsStateService { consumer.commit(); }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) - .queueAdmin(queueAdmin) + .queueAdmin(queueFactory.getEdqsQueueAdmin()) .consumerExecutor(eventConsumer.getConsumerExecutor()) .taskExecutor(eventConsumer.getTaskExecutor()) .scheduler(eventConsumer.getScheduler()) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java index fed786e120..b5541c740b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsQueueFactory.java @@ -17,6 +17,7 @@ package org.thingsboard.server.queue.edqs; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; @@ -32,4 +33,6 @@ public interface EdqsQueueFactory { TbQueueResponseTemplate<TbProtoQueueMsg<ToEdqsMsg>, TbProtoQueueMsg<FromEdqsMsg>> createEdqsResponseTemplate(); + TbQueueAdmin getEdqsQueueAdmin(); + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java index 0b6cc1909d..0801399c14 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; @@ -39,6 +40,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { private final InMemoryStorage storage; private final EdqsConfig edqsConfig; private final StatsFactory statsFactory; + private final TbQueueAdmin queueAdmin; @Override public TbQueueConsumer<TbProtoQueueMsg<ToEdqsMsg>> createEdqsMsgConsumer(EdqsQueue queue) { @@ -76,4 +78,9 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { .build(); } + @Override + public TbQueueAdmin getEdqsQueueAdmin() { + return queueAdmin; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index 42ca604841..e985696040 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueResponseTemplate; @@ -126,4 +127,9 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { .build(); } + @Override + public TbQueueAdmin getEdqsQueueAdmin() { + return edqsEventsAdmin; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index d219428941..4bd3bf0fe6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -160,7 +160,7 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue int partition = record.partition(); Long endOffset = endOffsets.get(partition); if (endOffset == null) { - log.warn("End offset not found for {} [{}]", record.topic(), partition); + log.debug("End offset not found for {} [{}]", record.topic(), partition); return; } log.trace("[{}-{}] Got record offset {}, expected end offset: {}", record.topic(), partition, record.offset(), endOffset - 1); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index e97af10ecc..fac2cf5d5e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -138,6 +138,11 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE return new InMemoryTbQueueConsumer<>(storage, topicService.buildTopicName(calculatedFieldSettings.getEventTopic())); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return queueAdmin; + } + @Override public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCalculatedFieldMsg>> createToCalculatedFieldMsgProducer() { return new InMemoryTbQueueProducer<>(storage, topicService.buildTopicName(calculatedFieldSettings.getEventTopic())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index f3d1e2d158..f07bd9dcbb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -526,6 +526,11 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi return consumerBuilder.build(); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return cfAdmin; + } + @Override public TbQueueProducer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgProducer() { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder<TbProtoQueueMsg<ToCalculatedFieldMsg>> requestBuilder = TbKafkaProducerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index 43fbb5efeb..d0e6c2f123 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -321,6 +321,11 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { return consumerBuilder.build(); } + @Override + public TbQueueAdmin getCalculatedFieldQueueAdmin() { + return cfAdmin; + } + @Override public TbQueueProducer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgProducer() { TbKafkaProducerTemplate.TbKafkaProducerTemplateBuilder<TbProtoQueueMsg<ToCalculatedFieldMsg>> requestBuilder = TbKafkaProducerTemplate.builder(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java index 767fea9f0c..03f662d8bb 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/TbRuleEngineQueueFactory.java @@ -29,6 +29,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToOtaPackageStateSer import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; +import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.TbQueueRequestTemplate; @@ -122,6 +123,8 @@ public interface TbRuleEngineQueueFactory extends TbUsageStatsClientQueueFactory TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgConsumer(); + TbQueueAdmin getCalculatedFieldQueueAdmin(); + TbQueueProducer<TbProtoQueueMsg<ToCalculatedFieldMsg>> createToCalculatedFieldMsgProducer(); TbQueueConsumer<TbProtoQueueMsg<ToCalculatedFieldNotificationMsg>> createToCalculatedFieldNotificationsMsgConsumer(); From 7810b3caa16a6ae5a220f800dd0a8b12201b321a Mon Sep 17 00:00:00 2001 From: Andrii Landiak <andriylandiak@gmail.com> Date: Tue, 18 Mar 2025 12:30:28 +0200 Subject: [PATCH 14/14] Fix testSendDeviceProfileToCloud & testSendAssetProfileToCloud --- .../thingsboard/server/edge/AssetProfileEdgeTest.java | 10 +++++----- .../thingsboard/server/edge/DeviceProfileEdgeTest.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java index 71fe987813..4df305b2c5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AssetProfileEdgeTest.java @@ -121,14 +121,14 @@ public class AssetProfileEdgeTest extends AbstractEdgeTest { Assert.assertNull(assetProfile.getDefaultRuleChainId()); Assert.assertEquals(edgeRuleChainId, assetProfile.getDefaultEdgeRuleChainId()); - // delete profile - edgeImitator.expectMessageAmount(1); + // delete profile and delete relation messages + edgeImitator.expectMessageAmount(2); doDelete("/api/assetProfile/" + assetProfile.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof AssetProfileUpdateMsg); - AssetProfileUpdateMsg assetProfileUpdateMsg = (AssetProfileUpdateMsg) latestMessage; + Optional<AssetProfileUpdateMsg> assetDeleteMsgOpt = edgeImitator.findMessageByType(AssetProfileUpdateMsg.class); + Assert.assertTrue(assetDeleteMsgOpt.isPresent()); + AssetProfileUpdateMsg assetProfileUpdateMsg = assetDeleteMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, assetProfileUpdateMsg.getMsgType()); Assert.assertEquals(assetProfile.getUuidId().getMostSignificantBits(), assetProfileUpdateMsg.getIdMSB()); Assert.assertEquals(assetProfile.getUuidId().getLeastSignificantBits(), assetProfileUpdateMsg.getIdLSB()); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index f1c4f31f65..819dec5c0d 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -327,14 +327,14 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { Assert.assertNotNull(deviceProfile); Assert.assertEquals("Device Profile On Edge", deviceProfile.getName()); - // delete profile - edgeImitator.expectMessageAmount(1); + // delete profile and delete relation messages + edgeImitator.expectMessageAmount(2); doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); - DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Optional<DeviceProfileUpdateMsg> deviceDeleteMsgOpt = edgeImitator.findMessageByType(DeviceProfileUpdateMsg.class); + Assert.assertTrue(deviceDeleteMsgOpt.isPresent()); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceDeleteMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB());