Loosen LtsMigrationService.select() to pick every bean in the (from, to]
range regardless of family, so a 4.3.x -> 4.4 offline upgrade runs the real
in-range 4.3.1.x beans plus a new 4.4.0.0 baseline bean, each exactly once.
This replaces the ad-hoc explicit V4_3_1_3Migration.apply() call in
DefaultDataUpdateService and moves the 4.4 baseline flat DDL out of
basic/schema_update.sql into V4_4_0_0Migration / lts/4.4.0.0/schema_update.sql.
- Drop the dangling "spec §7" reference from the test comments; inline the
actual rule (a lexical MIN/MAX result over string telemetry stays a string).
- Use a zero-padded code ("0009") for the string-result case - a clearer example
of a genuine string that must not be coerced to a number (would lose padding).
- Define the maxCode metric in the test CF configuration alongside consumption
and avgConsumption, so all parameterized metric names are configured.
Address review feedback on the integration test:
- Read latest telemetry with useStrictDataTypes=true always, so the value node
keeps its stored type (numeric -> JSON number, str_v -> JSON string).
- Assert the aggregation result is a numeric node across all existing scenarios
via a shared assertNumericValue helper, instead of a separate standalone test.
isNumber() is the actual regression guard; asLong()/asText() would coerce a
str_v string and miss it, so the type is asserted explicitly and the value is
compared numerically.
- Drop the redundant standalone test that duplicated an existing scenario.
Cover the fix that makes EntityAggregationCalculatedFieldState.toResult()
emit numeric aggregation results as numeric JSON nodes (dbl_v/long_v) instead
of JSON strings (str_v), which had broken server-side AVG/SUM aggregation.
- New unit test EntityAggregationCalculatedFieldStateTest: asserts toResult()
serializes a numeric result as a numeric node and a genuine string result
(lexical MIN/MAX over string telemetry) as a string node. Assertions check
the node type (isNumber/isTextual) rather than asText(), since asText()
coerces both node kinds identically - which is why the bug went unnoticed.
- EntityAggregationCalculatedFieldTest: add a strict-types read helper
(useStrictDataTypes=true) and a test asserting the stored SUM/AVG telemetry
are numeric JSON nodes end-to-end. Existing asText()-based assertions are
left untouched (they read via the non-strict endpoint, which stringifies
every value and therefore cannot observe the storage type).
EntityAggregationCalculatedFieldState.toResult() serialized every metric
result via ObjectNode.put(name, JacksonUtil.toString(value)), which produces
a JSON *string* node even for numeric aggregation results (SUM/AVG/COUNT/...).
When persisted (in particular with transport.json.type_cast_enabled=false, or
for non-parsable values) the result lands in ts_kv.str_v, so server-side AVG/SUM
aggregation returns no data in widgets/queries while COUNT/MIN/MAX still return.
Serialize with JacksonUtil.valueToTree(...) so numeric results are emitted as
numeric JSON nodes (-> dbl_v/long_v), mirroring
RelatedEntitiesAggregationCalculatedFieldState. Genuine string MIN/MAX results
over string telemetry are preserved as string nodes. The instanceof Number
guard is retained (it only governs rounding).
A device-state attribute save can complete after its tenant was deleted.
The post-save WS-update callback then resolves an evicted tenant profile
via partitionService.resolve -> getRoutingInfo and throws
TenantNotFoundException uncaught on the ws-callback thread.
Guard resolve() in forwardToSubscriptionManagerService: a deleted tenant
has no partition to route to and no subscribers to notify, so skip the
forward instead of propagating. Mirrors the existing tenant-gone handling
in DefaultDeviceStateService.checkStates().
The deprecation cleanup set @EqualsAndHashCode(callSuper = true) on AlarmComment,
so equals/hashCode now include the BaseData id and createdTime. The delete tests
built the expected comment via a fresh builder (no id), which no longer matches the
server-generated deletion comment (which reuses the original comment id).
Mutate the original alarm comment (already carrying the correct id/createdTime) into
the expected SYSTEM deletion comment instead of building a fresh one.
* feat: send pseudonymized install report (tenant/user hash, tb version) to IoT Hub
* refactor: source install-report tb version/edition from ProjectInfo
Use the existing ProjectInfo component (getProjectVersion/getProductType)
instead of an ad-hoc optional BuildProperties field and a hard-coded "CE"
edition, so PE reports its own product type and the version is the canonical
cleaned value. Constructor-injected via @RequiredArgsConstructor.
If iotHubInstalledItemService.save throws after the local entity was already created, the entity was orphaned — visible in the tenant but invisible to IoT Hub update/delete. Extracted the per-type entity-deletion dispatch from deleteInstalledItem into a deleteEntityForDescriptor helper and call it from doInstallVersion and registerDeviceInstall on tracking-save failure. registerDeviceInstall now also wraps reportVersionInstalled best-effort to match doInstallVersion.
The post-install Thread.sleep was driven by installTimeoutMs from the uploaded solution.json with no upper bound — a malicious or misconfigured template could pin an HTTP worker thread indefinitely. Cap it at a configurable max (default 60s) exposed as iot-hub.max-install-timeout-ms with an IOT_HUB_MAX_INSTALL_TIMEOUT_MS env override.
Stream entries through a fixed buffer and abort with IOException when the archive crosses any of three configurable thresholds: total uncompressed bytes, per-entry uncompressed bytes, or entry count. Defaults — 200 MiB total, 50 MiB per entry, 10 000 entries — and env-var overrides IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES / IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES / IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT are exposed under iot-hub in thingsboard.yml.
Root entries other than WIDGET or SOLUTION_TEMPLATE should always be installed even when a prior version is already installed — only widgets and solution templates are deduplicated by item id at install-plan resolution time.
Route all parse-failure throws through a single parseFailure(action, itemType[, section], cause) helper so the user-facing wording lives in one place and update paths share the template with install paths.