Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 60 additions & 37 deletions crates/m-bus-application-layer/src/value_information.rs
Original file line number Diff line number Diff line change
Expand Up @@ -587,44 +587,47 @@ impl TryFrom<&ValueInformationBlock> for ValueInformation {
match first_vife_data & 0x7F {
0b0 => populate!(Watt / h, 3, dec: 5, Energy),
0b000_0001 => populate!(Watt / h, 3, dec: 6, Energy),
0b000_0010 => populate!(ReactiveWatt * h, 1, dec: 3, Energy),
0b000_0011 => populate!(ReactiveWatt * h, 1, dec: 4, Energy),
0b000_0010 => populate!(ReactiveWatt * h, 1, dec: 3, ReactiveEnergy),
0b000_0011 => populate!(ReactiveWatt * h, 1, dec: 4, ReactiveEnergy),
0b000_0100 => populate!(ApparentWatt * h, 1, dec: 3, ApparentEnergy),
0b000_0101 => populate!(ApparentWatt * h, 1, dec: 4, ApparentEnergy),
0b000_0110 => {
labels.push(CoefficientOfPerformance);
decimal_scale_exponent = -1;
}
0b000_1000 => populate!(Joul, 1, dec: 8, Energy),
0b000_1001 => populate!(Joul, 1, dec: 9, Energy),
0b000_1100 => populate!(Joul, 1, dec: 5, Energy),
0b000_1101 => populate!(Joul, 1, dec: 6, Energy),
0b000_1110 => populate!(Joul, 1, dec: 7, Energy),
0b000_1111 => populate!(Joul, 1, dec: 8, Energy),
0b001_0000 => populate!(Meter, 3, dec: 2),
0b001_0001 => populate!(Meter, 3, dec: 3),
0b001_0100 => populate!(ReactiveWatt, 1, dec: -3),
0b001_0101 => populate!(ReactiveWatt, 1, dec: -2),
0b001_0110 => populate!(ReactiveWatt, 1, dec: -1),
0b001_0111 => populate!(ReactiveWatt, 1, dec: 0),
0b001_1000 => populate!(Tonne, 1, dec: 2),
0b001_1001 => populate!(Tonne, 1, dec: 3),
0b000_1100 => populate!(Calorie, 1, dec: 5, Energy),
0b000_1101 => populate!(Calorie, 1, dec: 6, Energy),
0b000_1110 => populate!(Calorie, 1, dec: 7, Energy),
0b000_1111 => populate!(Calorie, 1, dec: 8, Energy),
0b001_0000 => populate!(Meter, 3, dec: 2, Volume),
0b001_0001 => populate!(Meter, 3, dec: 3, Volume),
0b001_0100 => populate!(ReactiveWatt, 1, dec: 0, ReactivePower),
0b001_0101 => populate!(ReactiveWatt, 1, dec: 1, ReactivePower),
0b001_0110 => populate!(ReactiveWatt, 1, dec: 2, ReactivePower),
0b001_0111 => populate!(ReactiveWatt, 1, dec: 3, ReactivePower),
0b001_1000 => populate!(Tonne, 1, dec: 2, Mass),
0b001_1001 => populate!(Tonne, 1, dec: 3, Mass),
0b001_1010 => populate!(Percent, 1, dec: -1, RelativeHumidity),
0b001_1011 => populate!(Percent, 1, dec: 0, RelativeHumidity),
0b010_0001 => populate!(Feet, 3, dec: -1),
0b010_0010 => populate!(AmericanGallon, 1, dec: -1),
0b010_0011 => populate!(AmericanGallon, 1, dec: 0),
0b010_0100 => populate!(AmericanGallon / min, 1, dec: -3),
0b010_0101 => populate!(AmericanGallon / min, 1, dec: 0),
0b010_0110 => populate!(AmericanGallon / h, 1, dec: 0),
0b010_1000 => populate!(Watt, 1, dec: 5),
0b010_1001 => populate!(Watt, 1, dec: 6),
0b010_0000 => populate!(Feet, 3, dec: 0, Volume),
0b010_0001 => populate!(Feet, 3, dec: -1, Volume),
0b010_0011 => populate!(Degree, 1, dec: -1, PhaseItoU),
0b010_1000 => populate!(Watt, 1, dec: 5, Power),
0b010_1001 => populate!(Watt, 1, dec: 6, Power),
0b010_1010 => populate!(Degree, 1, dec: -1, PhaseUtoU),
0b010_1011 => populate!(Degree, 1, dec: -1, PhaseUtoI),
0b010_1100 => populate!(Hertz, 1, dec: -3),
0b010_1101 => populate!(Hertz, 1, dec: -2),
0b010_1110 => populate!(Hertz, 1, dec: -1),
0b010_1111 => populate!(Hertz, 1, dec: 0),
0b011_0000 => populate!(Joul / h, 1, dec: -8),
0b011_0001 => populate!(Joul / h, 1, dec: -7),
0b011_0100 => populate!(ApparentWatt / h, 1, dec: 0),
0b011_0101 => populate!(ApparentWatt / h, 1, dec: 1),
0b011_0110 => populate!(ApparentWatt / h, 1, dec: 2),
0b011_0111 => populate!(ApparentWatt / h, 1, dec: 3),
0b010_1100 => populate!(Hertz, 1, dec: -3, Frequency),
0b010_1101 => populate!(Hertz, 1, dec: -2, Frequency),
0b010_1110 => populate!(Hertz, 1, dec: -1, Frequency),
0b010_1111 => populate!(Hertz, 1, dec: 0, Frequency),
0b011_0000 => populate!(Joul / h, 1, dec: 8, Power),
0b011_0001 => populate!(Joul / h, 1, dec: 9, Power),
0b011_0100 => populate!(ApparentWatt, 1, dec: 0, ApparentPower),
0b011_0101 => populate!(ApparentWatt, 1, dec: 1, ApparentPower),
0b011_0110 => populate!(ApparentWatt, 1, dec: 2, ApparentPower),
0b011_0111 => populate!(ApparentWatt, 1, dec: 3, ApparentPower),
0b101_1000 => populate!(Fahrenheit, 1, dec: -3, FlowTemperature),
0b101_1001 => populate!(Fahrenheit, 1, dec: -2, FlowTemperature),
0b101_1010 => populate!(Fahrenheit, 1, dec: -1, FlowTemperature),
Expand Down Expand Up @@ -1160,6 +1163,12 @@ pub enum ValueLabel {
DataContainerForManufacturerSpecificProtocol,
CurrentlySelectedApplication,
Energy,
ReactiveEnergy,
ApparentEnergy,
CoefficientOfPerformance,
ReactivePower,
Frequency,
ApparentPower,
AtPhaseL1,
AtPhaseL2,
AtPhaseL3,
Expand All @@ -1181,6 +1190,7 @@ pub enum ValueLabel {
MoistureLevel,
PhaseUtoU,
PhaseUtoI,
PhaseItoU,
ColdWarmTemperatureLimit,
CumulativeMaximumOfActivePower,
ResultingRatingFactor,
Expand Down Expand Up @@ -1299,6 +1309,7 @@ pub enum UnitName {
HCAUnit,
Fahrenheit,
AmericanGallon,
Calorie,
}

#[cfg(feature = "std")]
Expand Down Expand Up @@ -1345,6 +1356,7 @@ impl fmt::Display for UnitName {
UnitName::HCAUnit => write!(f, "HCAUnit"),
UnitName::Fahrenheit => write!(f, "°F"),
UnitName::AmericanGallon => write!(f, "UsGal"),
UnitName::Calorie => write!(f, "cal"),
}
}
}
Expand Down Expand Up @@ -1630,13 +1642,24 @@ mod tests {
use crate::value_information::UnitName;
use crate::value_information::{ValueInformation, ValueInformationBlock, ValueLabel};

// VIF=0xFB VIFE=0x22: US gallon, 10^-1
// VIF=0xFB VIFE=0x20 (E010 0000): ft³, dec: 0
let vi = ValueInformation::try_from(
&ValueInformationBlock::try_from([0xFB, 0x22].as_slice()).unwrap(),
&ValueInformationBlock::try_from([0xFB, 0x20].as_slice()).unwrap(),
)
.unwrap();
assert_eq!(vi.units[0].name, UnitName::Feet);
assert_eq!(vi.units[0].exponent, 3);
assert_eq!(vi.decimal_scale_exponent, 0);
assert!(vi.labels.contains(&ValueLabel::Volume));

// VIF=0xFB VIFE=0x23 (E010 0011): Phase angle I-U, 0.1°
let vi = ValueInformation::try_from(
&ValueInformationBlock::try_from([0xFB, 0x23].as_slice()).unwrap(),
)
.unwrap();
assert_eq!(vi.units[0].name, UnitName::AmericanGallon);
assert_eq!(vi.units[0].name, UnitName::Degree);
assert_eq!(vi.decimal_scale_exponent, -1);
assert!(vi.labels.contains(&ValueLabel::PhaseItoU));

// VIF=0xFB VIFE=0x70: °F cold/warm temp limit, 10^-3
let vi = ValueInformation::try_from(
Expand All @@ -1646,9 +1669,9 @@ mod tests {
assert_eq!(vi.units[0].name, UnitName::Fahrenheit);
assert_eq!(vi.decimal_scale_exponent, -3);

// VIF=0xFB VIFE=0x20 (E010 0000): Reserved per EN 13757-3 — should not error
// VIF=0xFB VIFE=0x22 (E010 0010): Reserved — should not error
let vi = ValueInformation::try_from(
&ValueInformationBlock::try_from([0xFB, 0x20].as_slice()).unwrap(),
&ValueInformationBlock::try_from([0xFB, 0x22].as_slice()).unwrap(),
)
.unwrap();
assert!(vi.labels.contains(&ValueLabel::Reserved));
Expand Down
114 changes: 114 additions & 0 deletions tests/test_other_meters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#![cfg(all(feature = "std", feature = "serde"))]
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]

use m_bus_parser::mbus_data::MbusData;
use m_bus_parser::WiredFrame;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_electricity_meter() {
let input = "68 65 65 68 08 00 72 78 56 34 12 74 52 C7 02 2A 00 00 00 04 05 73 00 00 00 04 FB 82 75 00 00 00 00 04 2A 8F 00 00 00 04 FB 97 72 AF FF FF FF 04 FB B7 72 A5 00 00 00 02 FD BA 73 64 03 84 80 80 40 FD 48 00 00 00 00 04 FD 48 F7 02 00 00 84 40 FD 59 77 00 00 00 84 80 40 FD 59 00 00 00 00 84 C0 40 FD 59 00 00 00 00 1F F9 16";
let bytes: Vec<u8> = input
.split_whitespace()
.map(|s| u8::from_str_radix(s, 16).unwrap())
.collect();

let mbus_data = MbusData::<WiredFrame>::try_from(bytes.as_slice()).unwrap();
let json = serde_json::to_value(&mbus_data).unwrap();

let header =
&json["user_data"]["VariableDataStructureWithLongTplHeader"]["long_tpl_header"];
assert_eq!(header["device_type"], "ElectricityMeter");
assert_eq!(header["identification_number"]["number"], 12345678);
assert_eq!(header["version"], 199);
assert_eq!(header["manufacturer"]["Ok"]["code"][0], "T");
assert_eq!(header["manufacturer"]["Ok"]["code"][1], "S");
assert_eq!(header["manufacturer"]["Ok"]["code"][2], "T");
assert_eq!(header["short_tpl_header"]["access_number"], 42);

let records = json["data_records"].as_array().unwrap();
assert_eq!(records.len(), 11);

// (label, units, scale_exp, value, storage, tariff, device)
let expected: &[(&str, &[(&str, i64)], i64, f64, u64, u64, u64)] = &[
("Energy", &[("Watt", 1), ("Hour", 1)], 2, 115.0, 0, 0, 0),
(
"ReactiveEnergy",
&[("ReactiveWatt", 1), ("Hour", 1)],
2,
0.0,
0,
0,
0,
),
("Power", &[("Watt", 1)], -1, 143.0, 0, 0, 0),
("ReactivePower", &[("ReactiveWatt", 1)], -1, -81.0, 0, 0, 0),
("ApparentPower", &[("ApparentWatt", 1)], -1, 165.0, 0, 0, 0),
("Dimensionless", &[], -3, 868.0, 0, 0, 0),
("Voltage", &[("Volt", 1)], -1, 0.0, 0, 0, 4),
("Voltage", &[("Volt", 1)], -1, 759.0, 0, 0, 0),
("Current", &[("Ampere", 1)], -3, 119.0, 0, 0, 1),
("Current", &[("Ampere", 1)], -3, 0.0, 0, 0, 2),
("Current", &[("Ampere", 1)], -3, 0.0, 0, 0, 3),
];

for (i, (label, units, exponent, value, storage, tariff, device)) in
expected.iter().enumerate()
{
let rec = &records[i];
let hdr = &rec["data_record_header"]["processed_data_record_header"];
let vi = &hdr["value_information"];
let di = &hdr["data_information"];

assert_eq!(
vi["labels"][0].as_str().unwrap(),
*label,
"record {i} label"
);
assert_eq!(
vi["decimal_scale_exponent"].as_i64().unwrap(),
*exponent,
"record {i} exponent"
);
assert_eq!(
rec["data"]["value"]["Number"].as_f64().unwrap(),
*value,
"record {i} value"
);
assert_eq!(
di["function_field"], "InstantaneousValue",
"record {i} function"
);
assert_eq!(
di["storage_number"].as_u64().unwrap(),
*storage,
"record {i} storage"
);
assert_eq!(di["tariff"].as_u64().unwrap(), *tariff, "record {i} tariff");
assert_eq!(di["device"].as_u64().unwrap(), *device, "record {i} device");

let json_units = vi["units"].as_array().unwrap();
assert_eq!(json_units.len(), units.len(), "record {i} unit count");
for (j, (name, exp)) in units.iter().enumerate() {
assert_eq!(
json_units[j]["name"].as_str().unwrap(),
*name,
"record {i} unit {j} name"
);
assert_eq!(
json_units[j]["exponent"].as_i64().unwrap(),
*exp,
"record {i} unit {j} exponent"
);
}
}
}
}
Loading