Merge branch 'master' into feature/geofencing-cf

This commit is contained in:
dshvaika 2025-09-02 17:40:50 +03:00
commit 2a1de95cea
23 changed files with 49 additions and 384 deletions

View File

@ -14,41 +14,6 @@
-- limitations under the License. -- limitations under the License.
-- --
-- UPDATE OTA PACKAGE EXTERNAL ID START
ALTER TABLE ota_package
ADD COLUMN IF NOT EXISTS external_id uuid;
DO
$$
BEGIN
IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN
ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id);
END IF;
END;
$$;
-- UPDATE OTA PACKAGE EXTERNAL ID END
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START
DROP INDEX IF EXISTS idx_device_external_id;
DROP INDEX IF EXISTS idx_device_profile_external_id;
DROP INDEX IF EXISTS idx_asset_external_id;
DROP INDEX IF EXISTS idx_entity_view_external_id;
DROP INDEX IF EXISTS idx_rule_chain_external_id;
DROP INDEX IF EXISTS idx_dashboard_external_id;
DROP INDEX IF EXISTS idx_customer_external_id;
DROP INDEX IF EXISTS idx_widgets_bundle_external_id;
-- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END
-- ADD NEW COLUMN TITLE TO MOBILE APP START
ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255);
-- ADD NEW COLUMN TITLE TO MOBILE APP END
-- UPDATE TENANT PROFILE CONFIGURATION START -- UPDATE TENANT PROFILE CONFIGURATION START
UPDATE tenant_profile UPDATE tenant_profile

View File

@ -114,9 +114,6 @@ public class AppActor extends ContextAwareActor {
ctx.broadcastToChildrenByType(msg, EntityType.TENANT); ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
break; break;
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
//TODO: use priority from the message body. For example, messages about CF lifecycle are important and Device lifecycle are not. //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. // same for the Linked telemetry.

View File

@ -24,9 +24,6 @@ import org.thingsboard.server.common.msg.TbActorStopReason;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
/** /**
@ -70,15 +67,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor {
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg); processor.onCacheInitMsg((CalculatedFieldCacheInitMsg) msg);
break; break;
case CF_INIT_PROFILE_ENTITY_MSG:
processor.onProfileEntityMsg((CalculatedFieldInitProfileEntityMsg) msg);
break;
case CF_INIT_MSG:
processor.onFieldInitMsg((CalculatedFieldInitMsg) msg);
break;
case CF_LINK_INIT_MSG:
processor.onLinkInitMsg((CalculatedFieldLinkInitMsg) msg);
break;
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg); processor.onStateRestoreMsg((CalculatedFieldStateRestoreMsg) msg);
break; break;

View File

@ -36,9 +36,6 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldCacheInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldEntityLifecycleMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitProfileEntityMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -129,38 +126,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
msg.getCallback().onSuccess(); msg.getCallback().onSuccess();
} }
public void onProfileEntityMsg(CalculatedFieldInitProfileEntityMsg msg) {
log.debug("[{}] Processing profile entity message.", msg.getTenantId().getId());
entityProfileCache.add(msg.getProfileEntityId(), msg.getEntityId());
msg.getCallback().onSuccess();
}
public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException {
log.debug("[{}] Processing CF init message.", msg.getCf().getId());
var cf = msg.getCf();
var cfCtx = getCfCtx(cf);
try {
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
msg.getCallback().onSuccess();
}
public void onLinkInitMsg(CalculatedFieldLinkInitMsg msg) {
log.debug("[{}] Processing CF link init message for entity [{}].", msg.getLink().getCalculatedFieldId(), msg.getLink().getEntityId());
var link = msg.getLink();
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
msg.getCallback().onSuccess();
}
public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) { public void onStateRestoreMsg(CalculatedFieldStateRestoreMsg msg) {
var cfId = msg.getId().cfId(); var cfId = msg.getId().cfId();
var calculatedField = calculatedFields.get(cfId); var calculatedField = calculatedFields.get(cfId);
@ -311,7 +276,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
addLinks(cf); addLinks(cf);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
initCf(cfCtx, callback, false); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb));
} }
} }
} }
@ -368,7 +333,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); var stateChanges = newCfCtx.hasStateChanges(oldCfCtx);
if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) {
initCf(newCfCtx, callback, stateChanges); applyToTargetCfEntityActors(newCfCtx, callback, (id, cb) -> initCfForEntity(id, newCfCtx, stateChanges, cb));
} else { } else {
callback.onSuccess(); callback.onSuccess();
} }
@ -477,10 +442,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
return result; return result;
} }
private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) {
applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, forceStateReinit, cb));
}
private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) {
CalculatedField cf = cfCtx.getCalculatedField(); CalculatedField cf = cfCtx.getCalculatedField();
if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) { if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) {
@ -581,13 +542,36 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
cfs.forEach(cf -> { cfs.forEach(cf -> {
log.trace("Processing calculated field record: {}", cf); log.trace("Processing calculated field record: {}", cf);
try { try {
onFieldInitMsg(new CalculatedFieldInitMsg(cf.getTenantId(), cf)); initCalculatedField(cf);
} catch (CalculatedFieldException e) { } catch (CalculatedFieldException e) {
log.error("Failed to process calculated field record: {}", cf, e); log.error("Failed to process calculated field record: {}", cf, e);
} }
}); });
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize());
cfls.forEach(link -> onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link))); cfls.forEach(link -> {
log.trace("Processing calculated field link record: {}", link);
initCalculatedFieldLink(link);
});
}
private void initCalculatedField(CalculatedField cf) throws CalculatedFieldException {
var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService());
try {
cfCtx.init();
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build();
}
calculatedFields.put(cf.getId(), cfCtx);
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx);
scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx);
}
private void initCalculatedFieldLink(CalculatedFieldLink link) {
// We use copy on write lists to safely pass the reference to another actor for the iteration.
// Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead)
entityIdCalculatedFieldLinks.computeIfAbsent(link.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(link);
} }
private void initEntityProfileCache() { private void initEntityProfileCache() {

View File

@ -180,9 +180,6 @@ public class TenantActor extends RuleChainManagerActor {
onRuleChainMsg((RuleChainAwareMsg) msg); onRuleChainMsg((RuleChainAwareMsg) msg);
break; break;
case CF_CACHE_INIT_MSG: case CF_CACHE_INIT_MSG:
case CF_INIT_PROFILE_ENTITY_MSG:
case CF_INIT_MSG:
case CF_LINK_INIT_MSG:
case CF_STATE_RESTORE_MSG: case CF_STATE_RESTORE_MSG:
case CF_PARTITIONS_CHANGE_MSG: case CF_PARTITIONS_CHANGE_MSG:
onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true); onToCalculatedFieldSystemActorMsg((ToCalculatedFieldSystemMsg) msg, true);

View File

@ -19,11 +19,9 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelInvokeService;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
@ -31,8 +29,6 @@ import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg;
import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg;
import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.dao.usagerecord.ApiLimitService;
@ -58,8 +54,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
private final TbelInvokeService tbelInvokeService; private final TbelInvokeService tbelInvokeService;
private final ApiLimitService apiLimitService; private final ApiLimitService apiLimitService;
private final RelationService relationService; private final RelationService relationService;
@Lazy
private final ActorSystemContext actorSystemContext;
private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<CalculatedFieldId, CalculatedField> calculatedFields = new ConcurrentHashMap<>();
private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>(); private final ConcurrentMap<EntityId, List<CalculatedField>> entityIdCalculatedFields = new ConcurrentHashMap<>();
@ -77,7 +71,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
cfs.forEach(cf -> { cfs.forEach(cf -> {
if (cf != null) { if (cf != null) {
calculatedFields.putIfAbsent(cf.getId(), cf); calculatedFields.putIfAbsent(cf.getId(), cf);
actorSystemContext.tell(new CalculatedFieldInitMsg(cf.getTenantId(), cf));
} }
}); });
calculatedFields.values().forEach(cf -> { calculatedFields.values().forEach(cf -> {
@ -86,7 +79,6 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache {
PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize); PageDataIterable<CalculatedFieldLink> cfls = new PageDataIterable<>(calculatedFieldService::findAllCalculatedFieldLinks, initFetchPackSize);
cfls.forEach(link -> { cfls.forEach(link -> {
calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link); calculatedFieldLinks.computeIfAbsent(link.getCalculatedFieldId(), id -> new CopyOnWriteArrayList<>()).add(link);
actorSystemContext.tell(new CalculatedFieldLinkInitMsg(link.getTenantId(), link));
}); });
calculatedFieldLinks.values().stream() calculatedFieldLinks.values().stream()
.flatMap(List::stream) .flatMap(List::stream)

View File

@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti
// This list should include all versions which are compatible for the upgrade. // This list should include all versions which are compatible for the upgrade.
// The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release.
private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); private static final List<String> SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0");
private final ProjectInfo projectInfo; private final ProjectInfo projectInfo;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;

View File

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.service.install.update; package org.thingsboard.server.service.install.update;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -24,12 +22,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.component.RuleNodeClassInfo;
@ -129,60 +124,6 @@ public class DefaultDataUpdateService implements DataUpdateService {
return ruleNodeIds; return ruleNodeIds;
} }
boolean convertDeviceProfileForVersion330(JsonNode profileData) {
boolean isUpdated = false;
if (profileData.has("alarms") && !profileData.get("alarms").isNull()) {
JsonNode alarms = profileData.get("alarms");
for (JsonNode alarm : alarms) {
if (alarm.has("createRules")) {
JsonNode createRules = alarm.get("createRules");
for (AlarmSeverity severity : AlarmSeverity.values()) {
if (createRules.has(severity.name())) {
JsonNode spec = createRules.get(severity.name()).get("condition").get("spec");
if (convertDeviceProfileAlarmRulesForVersion330(spec)) {
isUpdated = true;
}
}
}
}
if (alarm.has("clearRule") && !alarm.get("clearRule").isNull()) {
JsonNode spec = alarm.get("clearRule").get("condition").get("spec");
if (convertDeviceProfileAlarmRulesForVersion330(spec)) {
isUpdated = true;
}
}
}
}
return isUpdated;
}
boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) {
if (spec != null) {
if (spec.has("type") && spec.get("type").asText().equals("DURATION")) {
if (spec.has("value")) {
long value = spec.get("value").asLong();
var predicate = new FilterPredicateValue<>(
value, null, new DynamicValue<>(null, null, false)
);
((ObjectNode) spec).remove("value");
((ObjectNode) spec).putPOJO("predicate", predicate);
return true;
}
} else if (spec.has("type") && spec.get("type").asText().equals("REPEATING")) {
if (spec.has("count")) {
int count = spec.get("count").asInt();
var predicate = new FilterPredicateValue<>(
count, null, new DynamicValue<>(null, null, false)
);
((ObjectNode) spec).remove("count");
((ObjectNode) spec).putPOJO("predicate", predicate);
return true;
}
}
}
return false;
}
public static boolean getEnv(String name, boolean defaultValue) { public static boolean getEnv(String name, boolean defaultValue) {
String env = System.getenv(name); String env = System.getenv(name);
if (env == null) { if (env == null) {

View File

@ -953,7 +953,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
}); });
EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter); EntityCountQuery countQuery = new EntityCountQuery(entityTypeFilter, keyFilters);
countByQueryAndCheck(countQuery, 97); countByQueryAndCheck(countQuery, 97);
} }

View File

@ -1,95 +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.service.install.update;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.thingsboard.common.util.JacksonUtil;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willCallRealMethod;
@ActiveProfiles("install")
@SpringBootTest(classes = DefaultDataUpdateService.class)
class DefaultDataUpdateServiceTest {
@MockBean
DefaultDataUpdateService service;
@BeforeEach
void setUp() {
willCallRealMethod().given(service).convertDeviceProfileAlarmRulesForVersion330(any());
willCallRealMethod().given(service).convertDeviceProfileForVersion330(any());
}
JsonNode readFromResource(String resourceName) throws IOException {
return JacksonUtil.OBJECT_MAPPER.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName));
}
@Test
void convertDeviceProfileAlarmRulesForVersion330FirstRun() throws IOException {
JsonNode spec = readFromResource("update/330/device_profile_001_in.json");
JsonNode expected = readFromResource("update/330/device_profile_001_out.json");
assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isTrue();
assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature <Click to see difference>
}
@Test
void convertDeviceProfileAlarmRulesForVersion330SecondRun() throws IOException {
JsonNode spec = readFromResource("update/330/device_profile_001_out.json");
JsonNode expected = readFromResource("update/330/device_profile_001_out.json");
assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isFalse();
assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature <Click to see difference>
}
@Test
void convertDeviceProfileAlarmRulesForVersion330EmptyJson() throws JsonProcessingException {
JsonNode spec = JacksonUtil.toJsonNode("{ }");
JsonNode expected = JacksonUtil.toJsonNode("{ }");
assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse();
assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString());
}
@Test
void convertDeviceProfileAlarmRulesForVersion330AlarmNodeNull() throws JsonProcessingException {
JsonNode spec = JacksonUtil.toJsonNode("{ \"alarms\" : null }");
JsonNode expected = JacksonUtil.toJsonNode("{ \"alarms\" : null }");
assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse();
assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString());
}
@Test
void convertDeviceProfileAlarmRulesForVersion330NoAlarmNode() throws JsonProcessingException {
JsonNode spec = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }");
JsonNode expected = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }");
assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse();
assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString());
}
}

View File

@ -137,9 +137,6 @@ public enum MsgType {
CF_CACHE_INIT_MSG, // Sent to init caches for CF actor; CF_CACHE_INIT_MSG, // Sent to init caches for CF actor;
CF_INIT_PROFILE_ENTITY_MSG, // Sent to init profile entities cache;
CF_INIT_MSG, // Sent to init particular calculated field;
CF_LINK_INIT_MSG, // Sent to init particular calculated field;
CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state; CF_STATE_RESTORE_MSG, // Sent to restore particular calculated field entity state;
CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures; CF_PARTITIONS_CHANGE_MSG, // Sent when cluster event occures;

View File

@ -1,34 +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.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;
@Data
public class CalculatedFieldInitMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final CalculatedField cf;
@Override
public MsgType getMsgType() {
return MsgType.CF_INIT_MSG;
}
}

View File

@ -1,36 +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.common.msg.cf;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@Data
public class CalculatedFieldInitProfileEntityMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final EntityId profileEntityId;
private final EntityId entityId;
@Override
public MsgType getMsgType() {
return MsgType.CF_INIT_PROFILE_ENTITY_MSG;
}
}

View File

@ -1,34 +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.common.msg.cf;
import lombok.Data;
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg;
@Data
public class CalculatedFieldLinkInitMsg implements ToCalculatedFieldSystemMsg {
private final TenantId tenantId;
private final CalculatedFieldLink link;
@Override
public MsgType getMsgType() {
return MsgType.CF_LINK_INIT_MSG;
}
}

View File

@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
public class AbstractRedisClusterContainer { public class AbstractRedisClusterContainer {
static final String NODES = "127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376"; static final String NODES = "127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375,127.0.0.1:6376";
static final String IMAGE = "bitnami/valkey-cluster:8.0"; static final String IMAGE = "bitnamilegacy/valkey-cluster:8.0";
static final Map<String,String> ENVS = Map.of( static final Map<String,String> ENVS = Map.of(
"VALKEY_CLUSTER_ANNOUNCE_IP", "127.0.0.1", "VALKEY_CLUSTER_ANNOUNCE_IP", "127.0.0.1",
"VALKEY_CLUSTER_DYNAMIC_IPS", "no", "VALKEY_CLUSTER_DYNAMIC_IPS", "no",

View File

@ -27,7 +27,7 @@ import java.util.List;
public class AbstractRedisContainer { public class AbstractRedisContainer {
@ClassRule(order = 0) @ClassRule(order = 0)
public static GenericContainer redis = new GenericContainer("bitnami/valkey:8.0") public static GenericContainer redis = new GenericContainer("bitnamilegacy/valkey:8.0")
.withEnv("ALLOW_EMPTY_PASSWORD","yes") .withEnv("ALLOW_EMPTY_PASSWORD","yes")
.withLogConsumer(s -> log.warn(((OutputFrame) s).getUtf8String().trim())) .withLogConsumer(s -> log.warn(((OutputFrame) s).getUtf8String().trim()))
.withExposedPorts(6379); .withExposedPorts(6379);

View File

@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class RedisJUnit5Test { public class RedisJUnit5Test {
@Container @Container
private static final GenericContainer REDIS = new GenericContainer("bitnami/valkey:8.0") private static final GenericContainer REDIS = new GenericContainer("bitnamilegacy/valkey:8.0")
.withEnv("ALLOW_EMPTY_PASSWORD","yes") .withEnv("ALLOW_EMPTY_PASSWORD","yes")
.withLogConsumer(s -> log.error(((OutputFrame) s).getUtf8String().trim())) .withLogConsumer(s -> log.error(((OutputFrame) s).getUtf8String().trim()))
.withExposedPorts(6379); .withExposedPorts(6379);

View File

@ -17,7 +17,7 @@
services: services:
kafka: kafka:
restart: always restart: always
image: "bitnami/kafka:4.0" image: "bitnamilegacy/kafka:4.0"
ports: ports:
- "9092:9092" - "9092:9092"
env_file: env_file:

View File

@ -18,7 +18,7 @@ services:
# Valkey cluster # Valkey cluster
# The latest version of Valkey compatible with ThingsBoard is 8.0 # The latest version of Valkey compatible with ThingsBoard is 8.0
valkey-node-0: valkey-node-0:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-0:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-0:/bitnami/valkey/data
environment: environment:
@ -26,7 +26,7 @@ services:
- 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5'
valkey-node-1: valkey-node-1:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-1:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-1:/bitnami/valkey/data
depends_on: depends_on:
@ -36,7 +36,7 @@ services:
- 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5'
valkey-node-2: valkey-node-2:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-2:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-2:/bitnami/valkey/data
depends_on: depends_on:
@ -46,7 +46,7 @@ services:
- 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5'
valkey-node-3: valkey-node-3:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-3:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-3:/bitnami/valkey/data
depends_on: depends_on:
@ -56,7 +56,7 @@ services:
- 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5'
valkey-node-4: valkey-node-4:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-4:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-4:/bitnami/valkey/data
depends_on: depends_on:
@ -66,7 +66,7 @@ services:
- 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5' - 'VALKEY_NODES=valkey-node-0 valkey-node-1 valkey-node-2 valkey-node-3 valkey-node-4 valkey-node-5'
valkey-node-5: valkey-node-5:
image: bitnami/valkey-cluster:8.0 image: bitnamilegacy/valkey-cluster:8.0
volumes: volumes:
- ./tb-node/valkey-cluster-data-5:/bitnami/valkey/data - ./tb-node/valkey-cluster-data-5:/bitnami/valkey/data
depends_on: depends_on:

View File

@ -18,7 +18,7 @@ services:
# Valkey sentinel # Valkey sentinel
# The latest version of Valkey compatible with ThingsBoard is 8.0 # The latest version of Valkey compatible with ThingsBoard is 8.0
valkey-primary: valkey-primary:
image: 'bitnami/valkey:8.0' image: 'bitnamilegacy/valkey:8.0'
volumes: volumes:
- ./tb-node/valkey-sentinel-data-primary:/bitnami/valkey/data - ./tb-node/valkey-sentinel-data-primary:/bitnami/valkey/data
environment: environment:
@ -26,7 +26,7 @@ services:
- 'VALKEY_PASSWORD=thingsboard' - 'VALKEY_PASSWORD=thingsboard'
valkey-replica: valkey-replica:
image: 'bitnami/valkey:8.0' image: 'bitnamilegacy/valkey:8.0'
volumes: volumes:
- ./tb-node/valkey-sentinel-data-replica:/bitnami/valkey/data - ./tb-node/valkey-sentinel-data-replica:/bitnami/valkey/data
environment: environment:
@ -38,7 +38,7 @@ services:
- valkey-primary - valkey-primary
valkey-sentinel: valkey-sentinel:
image: 'bitnami/valkey-sentinel:8.0' image: 'bitnamilegacy/valkey-sentinel:8.0'
volumes: volumes:
- ./tb-node/valkey-sentinel-data-sentinel:/bitnami/valkey/data - ./tb-node/valkey-sentinel-data-sentinel:/bitnami/valkey/data
environment: environment:

View File

@ -19,7 +19,7 @@ services:
# The latest version of Valkey compatible with ThingsBoard is 8.0 # The latest version of Valkey compatible with ThingsBoard is 8.0
valkey: valkey:
restart: always restart: always
image: bitnami/valkey:8.0 image: bitnamilegacy/valkey:8.0
environment: environment:
# ALLOW_EMPTY_PASSWORD is recommended only for development. # ALLOW_EMPTY_PASSWORD is recommended only for development.
ALLOW_EMPTY_PASSWORD: "yes" ALLOW_EMPTY_PASSWORD: "yes"

View File

@ -19,7 +19,7 @@ services:
# The latest version of Valkey compatible with ThingsBoard is 8.0 # The latest version of Valkey compatible with ThingsBoard is 8.0
valkey: valkey:
restart: always restart: always
image: bitnami/valkey:8.0 image: bitnamilegacy/valkey:8.0
environment: environment:
# ALLOW_EMPTY_PASSWORD is recommended only for development. # ALLOW_EMPTY_PASSWORD is recommended only for development.
- 'ALLOW_EMPTY_PASSWORD=yes' - 'ALLOW_EMPTY_PASSWORD=yes'

View File

@ -79,6 +79,9 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
if (!this.aiConfigForm.get('systemPrompt').value) { if (!this.aiConfigForm.get('systemPrompt').value) {
delete config.systemPrompt; delete config.systemPrompt;
} }
if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.JSON_SCHEMA) {
delete config.responseFormat.schema;
}
return deepTrim(config); return deepTrim(config);
} }
@ -88,10 +91,10 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.TEXT) { if (this.aiConfigForm.get('responseFormat.type').value !== ResponseFormat.TEXT) {
this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: true}); this.aiConfigForm.get('responseFormat.type').patchValue(ResponseFormat.TEXT, {emitEvent: true});
} }
this.aiConfigForm.get('responseFormat.type').disable(); this.aiConfigForm.get('responseFormat.type').disable({emitEvent: false});
} }
} else { } else {
this.aiConfigForm.get('responseFormat.type').enable(); this.aiConfigForm.get('responseFormat.type').enable({emitEvent: false});
} }
} }