Skip to content

feat: Define measurement units using topic metadata#3776

Merged
didier-wenzek merged 5 commits intothin-edge:mainfrom
didier-wenzek:feat/measurement-units
Sep 12, 2025
Merged

feat: Define measurement units using topic metadata#3776
didier-wenzek merged 5 commits intothin-edge:mainfrom
didier-wenzek:feat/measurement-units

Conversation

@didier-wenzek
Copy link
Copy Markdown
Contributor

@didier-wenzek didier-wenzek commented Sep 4, 2025

Proposed changes

  • Combine thin-edge JSON measurements with measurement metadata to produce C8Y JSON measurements with units
  • Use the te/+/+/+/+/m/+/meta topics to set measurement units
  • Doc & Test

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Improvement (general improvements like code refactoring that doesn't explicitly fix a bug or add any new functionality)
  • Documentation Update (if none of the other choices apply)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Paste Link to the issue

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA (in all commits with git commit -s. You can activate automatic signing by running just prepare-dev once)
  • I ran just format as mentioned in CODING_GUIDELINES
  • I used just check as mentioned in CODING_GUIDELINES
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Further comments

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Sep 4, 2025

Robot Results

✅ Passed ❌ Failed ⏭️ Skipped Total Pass % ⏱️ Duration
692 0 3 692 100 2h5m32.053955s

@codecov
Copy link
Copy Markdown

codecov bot commented Sep 5, 2025

Codecov Report

❌ Patch coverage is 76.79558% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/extensions/c8y_mapper_ext/src/converter.rs 34.48% 18 Missing and 1 partial ⚠️
crates/extensions/c8y_mapper_ext/src/json.rs 85.83% 16 Missing and 1 partial ⚠️
crates/extensions/c8y_mapper_ext/src/serializer.rs 83.87% 0 Missing and 5 partials ⚠️
crates/extensions/c8y_mapper_ext/src/actor.rs 0.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

supported_operations: SupportedOperations,
pub operation_handler: OperationHandler,

units: Units,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we should keep a meta wrapper at this level which internally contains the unit definitions, instead of keeping units directly here, since units is just one aspect of the metadata.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly. However, this can easily be added later when actually required.

pub fn set_unit(&mut self, measurement: String, meta: serde_json::Value) {
if let serde_json::Value::String(unit) = meta {
// "Temperature": "°C"
self.units.insert(measurement, unit);
Copy link
Copy Markdown
Contributor

@albinsuresh albinsuresh Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure about this case where we assume the value to be units, if we expect meta to be expanded beyond units in future. But, if we're pretty confident that unit is the most reasonable "default" meta for a measurement, then this is fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Better to remove support for this shortcut. Otherwise we will have to support it sooner or later, even if not documented ;-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: c7c208d

#[derive(Default)]
pub struct Units {
units: HashMap<String, String>,
groups: HashMap<String, Units>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
groups: HashMap<String, Units>,
group_units: HashMap<String, Units>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 5b0032d

@didier-wenzek didier-wenzek marked this pull request as ready for review September 5, 2025 14:41
@didier-wenzek didier-wenzek added improvement User value theme:telemetry Theme: Telemetry data labels Sep 5, 2025
@Bravo555 Bravo555 self-assigned this Sep 8, 2025
Copy link
Copy Markdown
Member

@Bravo555 Bravo555 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to react to invalid data in the meta topic somehow, which is my only blocker. Otherwise if in the future we need to do something similar we need to be careful not to add more parameters to from_thin_edge_json but to do some refactoring of generalizing this pattern of "take some json key/values from one place and apply it to many messages"

Comment on lines +96 to +110
if let Some(unit) = meta.get("unit") {
// "Temperature": {"unit": "°C"},
if let serde_json::Value::String(unit_name) = unit {
self.units.insert(measurement, unit_name.to_owned());
}
} else {
// "Climate": { "Temperature": {"unit": "°C"}, "Humidity": {"unit": "%RH"} }
let group = measurement;
self.set_group_units(group, meta);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: So, for a payload like {"unit: "u", "Temperature": {"unit": "°C"}, "Humidity": {"unit": "%RH"}} we only grab the "unit" and ignore the others?

I think we should only allow only one and print an error and don't add any units if both are present, ideally also adding this case to the tests. Or really, for any garbage data that can't be parsed published on these topics we should emit an ERROR event?

Also perhaps TRACE for when we actually set a new unit, but it's not as important.

Copy link
Copy Markdown
Contributor Author

@didier-wenzek didier-wenzek Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a tension here between flexibility and adherence to thin-edge JSON conventions.

  • We want the freedom to add not only units but also other info about measurements such as precision as with "temperature": { "unit": "°C", "precision": "±0.1°C" }
  • We want to be able to use different units for "Temperature", "Climate": { "Temperature" } and "Engine": { "Temperature" }, e.g. with "Climate": { "Temperature": {"unit": "°C"} }

However, this makes things a bit complicated. As raised by @Bravo555, "Foo": {"unit: "u", "Temperature": {"unit": "°C"}} can then be understood as metadata assigning units to "Foo":20 values as well as "Foo" : { "Temperature": 20 } (the current code ignoring the latter, though).

An alternative is to flatten the structure of (measurement, info) pairs where group measurements are identified by a path.

 "Temperature": { "unit": "°C", "precision": "±0.1°C" },
 "Climate.Temperature": { "unit": "°C", "precision": "±0.1°C" },
 "Engine.Temperature": { "unit": "°F", "precision": "±1°F" },

=> I will give a try to this idea.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

=> I will give a try to this idea.

7ddb8ed

@Bravo555 Bravo555 removed their assignment Sep 8, 2025
Copy link
Copy Markdown
Member

@rina23q rina23q left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some comments. I'll test it manually tomorrow.

@Bravo555 Bravo555 dismissed their stale review September 9, 2025 09:27

Releasing blocker for {"unit: "u", "Temperature": {"unit": "°C"}, "Humidity": {"unit": "%RH"}}, decided during daily that the current behaviour is well-defined.

Comment on lines +104 to +107
self.group_units
.entry(group.to_owned())
.or_default()
.set_unit(measurement.to_owned(), meta);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: If the messages have flat key-value structures, with no nesting, do we still need separate hashmaps for group units? Can't we put all the keys in a single hashmap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, not required. This would simplify the mapping and its memory footprint. However this would also implies building paths (concatenating group and measurement names) each time a grouped measurement value is received.

Comment on lines +103 to +118
Execute Command tedge mqtt pub te/device/main///m/t6 '{ "temperature": 25 }'
Execute Command tedge mqtt pub -r te/device/main///m/t6/meta ''
Execute Command tedge mqtt pub te/device/main///m/t6 '{ "temperature": 298.15 }'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@didier-wenzek Do you think this might introduce a flakey test if the meta topic is not processed before receiving the second temperature measurement?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, it might be worth doing two measurement assertions (one to check that the units are present, and the second to check that the units are not present)

Copy link
Copy Markdown
Contributor Author

@didier-wenzek didier-wenzek Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@didier-wenzek Do you think this might introduce a flakey test if the meta topic is not processed before receiving the second temperature measurement?

Good point. The mapper sequentially processes all the measurement related messages. But the broker might change the message order as they are not published on the same topic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, not flaky.

$ invoke flake-finder --test-name "Measurement units can be cleared" --iterations 100 --outputdir output_ff --clean
------------------------------
Overall: PASSED
Results: 100 iterations, 100 passed, 0 failed
Elapsed time: 0:20:33.596564

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove that useless commit when re-basing 92c40f6

Copy link
Copy Markdown
Contributor

@reubenmiller reubenmiller Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was also surprised that it didn't seem to be flakey, though I pushed a slightly refactored test that also passed 50 flake finder runs (and the test removes the potential race condition)

commit: 40bea55

Copy link
Copy Markdown
Member

@rina23q rina23q left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

@reubenmiller reubenmiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

Signed-off-by: Didier Wenzek <didier.wenzek@free.fr>
Signed-off-by: Didier Wenzek <didier.wenzek@free.fr>
Signed-off-by: Didier Wenzek <didier.wenzek@free.fr>
Signed-off-by: Didier Wenzek <didier.wenzek@free.fr>
Mimicing the structure of grouped measurements as in `"Foo": {"unit: "u", "Temperature": {"unit": "°C"}}`
leads to confusion as units are assigned to `"Foo":20` values as well as `"Foo" : { "Temperature": 20 }`.

Using a flat structure is less confusing as units for `"Foo"` and `"Foo.Temperature"`
are now defined using two independent property sets.

Signed-off-by: Didier Wenzek <didier.wenzek@free.fr>
@didier-wenzek didier-wenzek added this pull request to the merge queue Sep 12, 2025
Merged via the queue into thin-edge:main with commit c2fa11a Sep 12, 2025
34 checks passed
@didier-wenzek didier-wenzek deleted the feat/measurement-units branch September 12, 2025 14:29
@albinsuresh
Copy link
Copy Markdown
Contributor

To ease the discoverability of this feature, wouldn't it be better to mention the metadata support in https://github.com/thin-edge/thin-edge.io/blob/main/docs/src/start/send-measurements.md as well, rather than "hidden" in a few reference docs?

@didier-wenzek
Copy link
Copy Markdown
Contributor Author

To ease the discoverability of this feature, wouldn't it be better to mention the metadata support in https://github.com/thin-edge/thin-edge.io/blob/main/docs/src/start/send-measurements.md as well, rather than "hidden" in a few reference docs?

Good point. I will push a PR to improve that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement User value theme:telemetry Theme: Telemetry data

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants