diff --git a/crates/m-bus-application-layer/src/data_information.rs b/crates/m-bus-application-layer/src/data_information.rs index 6128a8c..ce9b4c8 100644 --- a/crates/m-bus-application-layer/src/data_information.rs +++ b/crates/m-bus-application-layer/src/data_information.rs @@ -119,10 +119,12 @@ impl DataInformationField { const fn has_extension(&self) -> bool { self.data & 0x80 != 0 } -} -impl DataInformationFieldExtension { - const fn special_function(&self) -> SpecialFunctions { + pub const fn is_special_function(&self) -> bool { + self.data & 0x0F == 0x0F + } + + pub const fn special_function(&self) -> SpecialFunctions { match self.data { 0x0F => SpecialFunctions::ManufacturerSpecific, 0x1F => SpecialFunctions::MoreRecordsFollow, @@ -208,10 +210,7 @@ impl TryFrom<&DataInformationBlock<'_>> for DataInformation { let mut extension_index = 1; let mut tariff = 0; let mut device = 0; - let mut first_dife = None; - if let Some(difes) = possible_difes { - first_dife = difes.clone().next(); let mut tariff_index = 0; for (device_index, dife) in difes.clone().enumerate() { if extension_index > MAXIMUM_DATA_INFORMATION_SIZE { @@ -250,8 +249,8 @@ impl TryFrom<&DataInformationBlock<'_>> for DataInformation { 0b1101 => DataFieldCoding::VariableLength, 0b1110 => DataFieldCoding::BCDDigit12, 0b1111 => DataFieldCoding::SpecialFunctions( - first_dife - .ok_or(DataInformationError::DataTooShort)? + data_information_block + .data_information_field .special_function(), ), _ => unreachable!(), // This case should never occur due to the 4-bit width @@ -293,8 +292,7 @@ impl PartialEq for TextUnit<'_> { #[cfg(feature = "std")] impl std::fmt::Display for TextUnit<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let value: Vec = self.0.iter().copied().rev().collect(); - let value = String::from_utf8(value).unwrap_or_default(); + let value: String = self.0.iter().rev().map(|&b| b as char).collect(); write!(f, "{}", value) } } @@ -302,8 +300,7 @@ impl std::fmt::Display for TextUnit<'_> { #[cfg(feature = "std")] impl From> for String { fn from(value: TextUnit<'_>) -> Self { - let value: Vec = value.0.iter().copied().rev().collect(); - String::from_utf8(value).unwrap_or_default() + value.0.iter().rev().map(|&b| b as char).collect() } } @@ -733,14 +730,25 @@ impl DataFieldCoding { } } - Self::SpecialFunctions(_code) => { - // Special functions parsing based on the code - Err(DataRecordError::DataInformationError( - DataInformationError::Unimplemented { - feature: "Special functions data parsing", - }, - )) - } + Self::SpecialFunctions(code) => match code { + SpecialFunctions::ManufacturerSpecific | SpecialFunctions::MoreRecordsFollow => { + Ok(Data { + value: Some(DataType::ManufacturerSpecific(input)), + size: input.len(), + }) + } + SpecialFunctions::IdleFiller => Ok(Data { + value: None, + size: 0, + }), + SpecialFunctions::GlobalReadoutRequest => Ok(Data { + value: None, + size: 0, + }), + SpecialFunctions::Reserved => Err(DataRecordError::DataInformationError( + DataInformationError::InvalidValueInformation, + )), + }, Self::DateTypeG => { let day = parse_single_or_every!( @@ -1004,6 +1012,24 @@ mod tests { assert_eq!(&parsed, "igal"); } + #[cfg(feature = "std")] + #[test] + fn text_unit_latin1_swedish_characters() { + // "Malmö" in Latin-1 (reversed byte order per M-Bus) + let bytes = [0xF6, 0x6D, 0x6C, 0x61, 0x4D]; // ö m l a M + let text = TextUnit::new(&bytes); + assert_eq!(String::from(text), "Malmö"); + } + + #[cfg(feature = "std")] + #[test] + fn text_unit_latin1_superscript_three() { + // "m³/h" in Latin-1 (reversed byte order per M-Bus) + let bytes = [0x68, 0x2F, 0xB3, 0x6D]; // h / ³ m + let text = TextUnit::new(&bytes); + assert_eq!(String::from(text), "m³/h"); + } + #[test] fn test_invalid_data_information() { let data = [ diff --git a/crates/m-bus-application-layer/src/data_record.rs b/crates/m-bus-application-layer/src/data_record.rs index 48c30f4..f3feb44 100644 --- a/crates/m-bus-application-layer/src/data_record.rs +++ b/crates/m-bus-application-layer/src/data_record.rs @@ -69,43 +69,29 @@ impl<'a> DataRecord<'a> { fixed_data_header: Option<&'a LongTplHeader>, ) -> Result { let data_record_header = DataRecordHeader::try_from(data)?; - let mut header_size = data_record_header.get_size(); - if data_record_header - .raw_data_record_header - .data_information_block - .data_information_field - .data - == 0x0F - { - header_size = 0; - } + let header_size = data_record_header.get_size(); if data.len() < header_size { return Err(DataRecordError::InsufficientData); } let offset = header_size; - let mut data_out = Data { - value: Some(DataType::ManufacturerSpecific( + let data_out = if let Some(data_info) = &data_record_header + .processed_data_record_header + .data_information + { + data_info.data_field_coding.parse( data.get(offset..) .ok_or(DataRecordError::InsufficientData)?, - )), - size: data.len() - offset, - }; - if data_record_header - .raw_data_record_header - .value_information_block - .is_some() - { - if let Some(data_info) = &data_record_header - .processed_data_record_header - .data_information - { - data_out = data_info.data_field_coding.parse( + fixed_data_header, + )? + } else { + Data { + value: Some(DataType::ManufacturerSpecific( data.get(offset..) .ok_or(DataRecordError::InsufficientData)?, - fixed_data_header, - )?; + )), + size: data.len() - offset, } - } + }; let mut record_size = data_record_header.get_size() + data_out.get_size(); if record_size > data.len() { @@ -154,7 +140,7 @@ impl<'a> TryFrom<&'a [u8]> for RawDataRecordHeader<'a> { let mut vifb = None; - if difb.data_information_field.data != 0x0F { + if !difb.data_information_field.is_special_function() { vifb = Some(ValueInformationBlock::try_from( data.get(offset..) .ok_or(DataRecordError::InsufficientData)?, @@ -194,6 +180,14 @@ impl TryFrom<&RawDataRecordHeader<'_>> for ProcessedDataRecordHeader { value_information = Some(v); data_information = Some(d); + } else if raw_data_record_header + .data_information_block + .data_information_field + .is_special_function() + { + data_information = Some(DataInformation::try_from( + &raw_data_record_header.data_information_block, + )?); } Ok(Self { diff --git a/crates/m-bus-application-layer/src/lib.rs b/crates/m-bus-application-layer/src/lib.rs index fb2ef14..c40ac02 100644 --- a/crates/m-bus-application-layer/src/lib.rs +++ b/crates/m-bus-application-layer/src/lib.rs @@ -46,31 +46,51 @@ impl<'a> Iterator for DataRecords<'a> { type Item = Result, DataRecordError>; fn next(&mut self) -> Option { - let mut _more_records_follow = false; - while self.offset < self.data.len() { - match self.data.get(self.offset)? { - 0x1F => { - /* TODO: parse manufacturer specific */ - _more_records_follow = true; - self.offset = self.data.len(); - } - 0x2F => { - self.offset += 1; - } - _ => { - let record = if let Some(long_tpl_header) = self.long_tpl_header { - DataRecord::try_from((self.data.get(self.offset..)?, long_tpl_header)) - } else { - DataRecord::try_from(self.data.get(self.offset..)?) - }; - if let Ok(record) = record { - self.offset += record.get_size(); - return Some(Ok(record)); - } else { + let dif = data_information::DataInformationField::from(*self.data.get(self.offset)?); + + if dif.is_special_function() { + match dif.special_function() { + data_information::SpecialFunctions::IdleFiller => { + self.offset += 1; + } + data_information::SpecialFunctions::ManufacturerSpecific + | data_information::SpecialFunctions::MoreRecordsFollow => { + let remaining = self.data.get(self.offset..)?; self.offset = self.data.len(); + let record = if let Some(long_tpl_header) = self.long_tpl_header { + DataRecord::try_from((remaining, long_tpl_header)) + } else { + DataRecord::try_from(remaining) + }; + return Some(record); + } + data_information::SpecialFunctions::GlobalReadoutRequest => { + let remaining = self.data.get(self.offset..)?; + self.offset += 1; + let record = if let Some(long_tpl_header) = self.long_tpl_header { + DataRecord::try_from((remaining, long_tpl_header)) + } else { + DataRecord::try_from(remaining) + }; + return Some(record); + } + data_information::SpecialFunctions::Reserved => { + self.offset += 1; } } + } else { + let record = if let Some(long_tpl_header) = self.long_tpl_header { + DataRecord::try_from((self.data.get(self.offset..)?, long_tpl_header)) + } else { + DataRecord::try_from(self.data.get(self.offset..)?) + }; + if let Ok(record) = record { + self.offset += record.get_size(); + return Some(Ok(record)); + } else { + self.offset = self.data.len(); + } } } None @@ -1485,4 +1505,12 @@ mod tests { } } } + + #[test] + fn global_readout_request_does_not_consume_following_records() { + // 0x7F followed by a valid 8-bit integer record: DIF=0x01, VIF=0x13, data=0x05 + let data: &[u8] = &[0x7F, 0x01, 0x13, 0x05]; + let records: Vec<_> = DataRecords::new(data, None).flatten().collect(); + assert_eq!(records.len(), 2); + } } diff --git a/crates/m-bus-application-layer/src/value_information.rs b/crates/m-bus-application-layer/src/value_information.rs index 7857777..2d86fae 100644 --- a/crates/m-bus-application-layer/src/value_information.rs +++ b/crates/m-bus-application-layer/src/value_information.rs @@ -736,13 +736,17 @@ fn consume_orthhogonal_vife( 0x0A => labels.push(ValueLabel::AtQuadrant3), 0x0B => labels.push(ValueLabel::AtQuadrant4), 0x0C => labels.push(ValueLabel::DeltaBetweenImportAndExport), - 0x0F => labels.push( + 0x0D => labels.push(ValueLabel::AlternativeNonMetricUnits), + 0x0E => labels.push(ValueLabel::SecondarySensorMeasurement), + 0x0F => labels.push(ValueLabel::HigherResolutionRegister), + 0x10 => labels.push( ValueLabel::AccumulationOfAbsoluteValueBothPositiveAndNegativeContribution, ), 0x11 => labels.push(ValueLabel::DataPresentedWithTypeC), 0x12 => labels.push(ValueLabel::DataPresentedWithTypeD), - 0x13 => labels.push(ValueLabel::DirectionFromCommunicationPartnerToMeter), - 0x14 => labels.push(ValueLabel::DirectionFromMeterToCommunicationPartner), + 0x13 => labels.push(ValueLabel::EndDate), + 0x14 => labels.push(ValueLabel::DirectionFromCommunicationPartnerToMeter), + 0x15 => labels.push(ValueLabel::DirectionFromMeterToCommunicationPartner), _ => labels.push(ValueLabel::Reserved), } } else { @@ -770,11 +774,11 @@ fn consume_orthhogonal_vife( } 0x29 => { units.push(unit!(Increment)); - units.push(unit!(OutputPulseOnChannel0 ^ -1)); + units.push(unit!(InputPulseOnChannel1 ^ -1)); } 0x2A => { units.push(unit!(Increment)); - units.push(unit!(InputPulseOnChannel1 ^ -1)); + units.push(unit!(OutputPulseOnChannel0 ^ -1)); } 0x2B => { units.push(unit!(Increment)); @@ -816,21 +820,28 @@ fn consume_orthhogonal_vife( 0x3A => labels.push(ValueLabel::VifContainsUncorrectedUnitOrValue), 0x3B => labels.push(ValueLabel::AccumulationOnlyIfValueIsPositive), 0x3C => labels.push(ValueLabel::AccumulationOnlyIfValueIsNegative), - 0x3D => labels.push(ValueLabel::NoneMetricUnits), + 0x3D => labels.push(ValueLabel::NonMetricUnits), 0x3E => labels.push(ValueLabel::ValueAtBaseConditions), 0x3F => labels.push(ValueLabel::ObisDeclaration), - 0x40 => labels.push(ValueLabel::UpperLimitValue), - 0x48 => labels.push(ValueLabel::LowerLimitValue), - 0x41 => labels.push(ValueLabel::NumberOfExceedsOfUpperLimitValue), - 0x49 => labels.push(ValueLabel::NumberOfExceedsOfLowerLimitValue), + // E100 u000 where u = 0: Lower; u = 1: Upper + 0x40 => labels.push(ValueLabel::LowerLimitValue), + 0x48 => labels.push(ValueLabel::UpperLimitValue), + // E100 u001 where u = 0: Lower; u = 1: Upper + 0x41 => labels.push(ValueLabel::NumberOfExceedsOfLowerLimitValue), + 0x49 => labels.push(ValueLabel::NumberOfExceedsOfUpperLimitValue), + /* E100 uf1b where + b = 0: Begin; b = 1: End + f = 0: First; b = 1: Last + u = 0: Lower; u = 1: Upper + */ 0x42 => labels.push(ValueLabel::DateOfBeginFirstLowerLimitExceed), - 0x43 => labels.push(ValueLabel::DateOfBeginFirstUpperLimitExceed), + 0x43 => labels.push(ValueLabel::DateOfEndFirstLowerLimitExceed), 0x46 => labels.push(ValueLabel::DateOfBeginLastLowerLimitExceed), - 0x47 => labels.push(ValueLabel::DateOfBeginLastUpperLimitExceed), - 0x4A => labels.push(ValueLabel::DateOfEndLastLowerLimitExceed), - 0x4B => labels.push(ValueLabel::DateOfEndLastUpperLimitExceed), - 0x4E => labels.push(ValueLabel::DateOfEndFirstLowerLimitExceed), - 0x4F => labels.push(ValueLabel::DateOfEndFirstUpperLimitExceed), + 0x47 => labels.push(ValueLabel::DateOfEndLastLowerLimitExceed), + 0x4A => labels.push(ValueLabel::DateOfBeginFirstUpperLimitExceed), + 0x4B => labels.push(ValueLabel::DateOfEndFirstUpperLimitExceed), + 0x4E => labels.push(ValueLabel::DateOfBeginLastUpperLimitExceed), + 0x4F => labels.push(ValueLabel::DateOfEndLastUpperLimitExceed), 0x50 => { labels.push(ValueLabel::DurationOfFirstLowerLimitExceed); units.push(unit!(Second)); @@ -848,35 +859,35 @@ fn consume_orthhogonal_vife( units.push(unit!(Day)); } 0x54 => { - labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); + labels.push(ValueLabel::DurationOfLastLowerLimitExceed); units.push(unit!(Second)); } 0x55 => { - labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); + labels.push(ValueLabel::DurationOfLastLowerLimitExceed); units.push(unit!(Minute)); } 0x56 => { - labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); + labels.push(ValueLabel::DurationOfLastLowerLimitExceed); units.push(unit!(Hour)); } 0x57 => { - labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); + labels.push(ValueLabel::DurationOfLastLowerLimitExceed); units.push(unit!(Day)); } 0x58 => { - labels.push(ValueLabel::DurationOfLastLowerLimitExceed); + labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); units.push(unit!(Second)); } 0x59 => { - labels.push(ValueLabel::DurationOfLastLowerLimitExceed); + labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); units.push(unit!(Minute)); } 0x5A => { - labels.push(ValueLabel::DurationOfLastLowerLimitExceed); + labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); units.push(unit!(Hour)); } 0x5B => { - labels.push(ValueLabel::DurationOfLastLowerLimitExceed); + labels.push(ValueLabel::DurationOfFirstUpperLimitExceed); units.push(unit!(Day)); } 0x5C => { @@ -941,7 +952,9 @@ fn consume_orthhogonal_vife( 0x78..=0x7B => { *decimal_offset_exponent += (v.data & 0b11) as isize - 3; } - 0x7D => labels.push(ValueLabel::MultiplicativeCorrectionFactor103), + 0x7D => { + *decimal_scale_exponent += 3; + } 0x7E => labels.push(ValueLabel::FutureValue), 0x7F => labels.push(ValueLabel::NextVIFEAndDataOfThisBlockAreManufacturerSpecific), _ => labels.push(ValueLabel::Reserved), @@ -1075,7 +1088,8 @@ pub enum ValueLabel { VifContainsUncorrectedUnitOrValue, AccumulationOnlyIfValueIsPositive, AccumulationOnlyIfValueIsNegative, - NoneMetricUnits, + NonMetricUnits, + AlternativeNonMetricUnits, ValueAtBaseConditions, ObisDeclaration, UpperLimitValue, @@ -1105,7 +1119,6 @@ pub enum ValueLabel { DateOfEndLast, DateOfEndFirst, ExtensionOfCombinableOrthogonalVIFE, - MultiplicativeCorrectionFactor103, FutureValue, NextVIFEAndDataOfThisBlockAreManufacturerSpecific, Credit, @@ -1182,8 +1195,11 @@ pub enum ValueLabel { AtQuadrant4, DeltaBetweenImportAndExport, AccumulationOfAbsoluteValueBothPositiveAndNegativeContribution, + SecondarySensorMeasurement, + HigherResolutionRegister, DataPresentedWithTypeC, DataPresentedWithTypeD, + EndDate, DirectionFromCommunicationPartnerToMeter, DirectionFromMeterToCommunicationPartner, RelativeHumidity, @@ -1828,4 +1844,161 @@ mod tests { assert_eq!(vib.get_size(), 1); assert!(vib.value_information_extension.is_none()); } + + #[test] + fn test_combinable_orthogonal_vife_limit_exceed_mappings() { + use crate::value_information::{ + UnitName, ValueInformation, ValueInformationBlock, ValueLabel, + }; + + let parse_orthogonal_vife = |vife_byte: u8| -> ValueInformation { + let data = [0x93, vife_byte]; + let vib = ValueInformationBlock::try_from(data.as_slice()).unwrap(); + ValueInformation::try_from(&vib).unwrap() + }; + + // (vife_byte, expected_label, optional expected unit name) + let cases: &[(u8, ValueLabel, Option)] = &[ + (0x40, ValueLabel::LowerLimitValue, None), + (0x48, ValueLabel::UpperLimitValue, None), + (0x41, ValueLabel::NumberOfExceedsOfLowerLimitValue, None), + (0x49, ValueLabel::NumberOfExceedsOfUpperLimitValue, None), + // E100 uf1b: b=Begin/End, f=First/Last, u=Lower/Upper + (0x42, ValueLabel::DateOfBeginFirstLowerLimitExceed, None), + (0x43, ValueLabel::DateOfEndFirstLowerLimitExceed, None), + (0x46, ValueLabel::DateOfBeginLastLowerLimitExceed, None), + (0x47, ValueLabel::DateOfEndLastLowerLimitExceed, None), + (0x4A, ValueLabel::DateOfBeginFirstUpperLimitExceed, None), + (0x4B, ValueLabel::DateOfEndFirstUpperLimitExceed, None), + (0x4E, ValueLabel::DateOfBeginLastUpperLimitExceed, None), + (0x4F, ValueLabel::DateOfEndLastUpperLimitExceed, None), + // Duration of first lower (0x50-0x53) + ( + 0x50, + ValueLabel::DurationOfFirstLowerLimitExceed, + Some(UnitName::Second), + ), + ( + 0x51, + ValueLabel::DurationOfFirstLowerLimitExceed, + Some(UnitName::Minute), + ), + ( + 0x52, + ValueLabel::DurationOfFirstLowerLimitExceed, + Some(UnitName::Hour), + ), + ( + 0x53, + ValueLabel::DurationOfFirstLowerLimitExceed, + Some(UnitName::Day), + ), + // Duration of last lower (0x54-0x57) + ( + 0x54, + ValueLabel::DurationOfLastLowerLimitExceed, + Some(UnitName::Second), + ), + ( + 0x55, + ValueLabel::DurationOfLastLowerLimitExceed, + Some(UnitName::Minute), + ), + ( + 0x56, + ValueLabel::DurationOfLastLowerLimitExceed, + Some(UnitName::Hour), + ), + ( + 0x57, + ValueLabel::DurationOfLastLowerLimitExceed, + Some(UnitName::Day), + ), + // Duration of first upper (0x58-0x5B) + ( + 0x58, + ValueLabel::DurationOfFirstUpperLimitExceed, + Some(UnitName::Second), + ), + ( + 0x59, + ValueLabel::DurationOfFirstUpperLimitExceed, + Some(UnitName::Minute), + ), + ( + 0x5A, + ValueLabel::DurationOfFirstUpperLimitExceed, + Some(UnitName::Hour), + ), + ( + 0x5B, + ValueLabel::DurationOfFirstUpperLimitExceed, + Some(UnitName::Day), + ), + // Duration of last upper (0x5C-0x5F) + ( + 0x5C, + ValueLabel::DurationOfLastUpperLimitExceed, + Some(UnitName::Second), + ), + ( + 0x5D, + ValueLabel::DurationOfLastUpperLimitExceed, + Some(UnitName::Minute), + ), + ( + 0x5E, + ValueLabel::DurationOfLastUpperLimitExceed, + Some(UnitName::Hour), + ), + ( + 0x5F, + ValueLabel::DurationOfLastUpperLimitExceed, + Some(UnitName::Day), + ), + ]; + + for (vife_byte, expected_label, expected_unit) in cases { + let vi = parse_orthogonal_vife(*vife_byte); + assert!( + vi.labels.contains(expected_label), + "VIFE 0x{vife_byte:02X}: expected label {expected_label:?}, got {:?}", + vi.labels + ); + if let Some(unit_name) = expected_unit { + assert!( + vi.units.iter().any(|u| u.name == *unit_name), + "VIFE 0x{vife_byte:02X}: expected unit {unit_name:?}, got {:?}", + vi.units + ); + } + } + } + + #[test] + fn test_combinable_orthogonal_vife_fc_extension_mappings() { + use crate::value_information::{ValueInformation, ValueInformationBlock, ValueLabel}; + + let parse_fc_vife = |vife_byte: u8| -> ValueInformation { + let data = [0x93, 0xFC, vife_byte]; + let vib = ValueInformationBlock::try_from(data.as_slice()).unwrap(); + ValueInformation::try_from(&vib).unwrap() + }; + + let cases: &[(u8, ValueLabel)] = &[ + (0x02, ValueLabel::AtPhaseL2), + (0x0D, ValueLabel::AlternativeNonMetricUnits), + (0x0E, ValueLabel::SecondarySensorMeasurement), + (0x13, ValueLabel::EndDate), + ]; + + for (vife_byte, expected_label) in cases { + let vi = parse_fc_vife(*vife_byte); + assert!( + vi.labels.contains(expected_label), + "FC VIFE 0x{vife_byte:02X}: expected {expected_label:?}, got {:?}", + vi.labels + ); + } + } } diff --git a/src/mbus_data.rs b/src/mbus_data.rs index 897644d..fd4b169 100644 --- a/src/mbus_data.rs +++ b/src/mbus_data.rs @@ -1633,7 +1633,7 @@ mod tests { let input = "68 3D 3D 68 08 01 72 00 51 20 02 82 4D 02 04 00 88 00 00 04 07 00 00 00 00 0C 15 03 00 00 00 0B 2E 00 00 00 0B 3B 00 00 00 0A 5A 88 12 0A 5E 16 05 0B 61 23 77 00 02 6C 8C 11 02 27 37 0D 0F 60 00 67 16"; let csv_output = parse_to_csv(input, None); - let expected = "FrameType,Function,Address,Identification Number,Manufacturer,Access Number,Status,Security Mode,Version,Device Type,DataPoint1_Value,DataPoint1_Info,DataPoint2_Value,DataPoint2_Info,DataPoint3_Value,DataPoint3_Info,DataPoint4_Value,DataPoint4_Info,DataPoint5_Value,DataPoint5_Info,DataPoint6_Value,DataPoint6_Info,DataPoint7_Value,DataPoint7_Info,DataPoint8_Value,DataPoint8_Info,DataPoint9_Value,DataPoint9_Info,DataPoint10_Value,DataPoint10_Info\nLongFrame,\"RspUd (ACD: false, DFC: false)\",Primary (1),02205100,SLB,0,\"Permanent error, Manufacturer specific 3\",No encryption used,2,Heat Meter (Return),(0)e4[Wh](Energy),\"0,Inst,32-bit Integer\",(3)e-1[m³](Volume),\"0,Inst,BCD 8-digit\",(0)e3[W](Power),\"0,Inst,BCD 6-digit\",(0)e-3[m³h⁻¹](VolumeFlow),\"0,Inst,BCD 6-digit\",(1288)e-1[°C](FlowTemperature),\"0,Inst,BCD 4-digit\",(516)e-1[°C](ReturnTemperature),\"0,Inst,BCD 4-digit\",(7723)e-2[°K](TemperatureDifference),\"0,Inst,BCD 6-digit\",(12/Jan/12)(Date),\"0,Inst,Date Type G\",(3383)[day](OperatingTime),\"0,Inst,16-bit Integer\",\"(Manufacturer Specific: [15, 96, 0])\",None\n"; + let expected = "FrameType,Function,Address,Identification Number,Manufacturer,Access Number,Status,Security Mode,Version,Device Type,DataPoint1_Value,DataPoint1_Info,DataPoint2_Value,DataPoint2_Info,DataPoint3_Value,DataPoint3_Info,DataPoint4_Value,DataPoint4_Info,DataPoint5_Value,DataPoint5_Info,DataPoint6_Value,DataPoint6_Info,DataPoint7_Value,DataPoint7_Info,DataPoint8_Value,DataPoint8_Info,DataPoint9_Value,DataPoint9_Info,DataPoint10_Value,DataPoint10_Info\nLongFrame,\"RspUd (ACD: false, DFC: false)\",Primary (1),02205100,SLB,0,\"Permanent error, Manufacturer specific 3\",No encryption used,2,Heat Meter (Return),(0)e4[Wh](Energy),\"0,Inst,32-bit Integer\",(3)e-1[m³](Volume),\"0,Inst,BCD 8-digit\",(0)e3[W](Power),\"0,Inst,BCD 6-digit\",(0)e-3[m³h⁻¹](VolumeFlow),\"0,Inst,BCD 6-digit\",(1288)e-1[°C](FlowTemperature),\"0,Inst,BCD 4-digit\",(516)e-1[°C](ReturnTemperature),\"0,Inst,BCD 4-digit\",(7723)e-2[°K](TemperatureDifference),\"0,Inst,BCD 6-digit\",(12/Jan/12)(Date),\"0,Inst,Date Type G\",(3383)[day](OperatingTime),\"0,Inst,16-bit Integer\",\"(Manufacturer Specific: [96, 0])\",\"0,Inst,Special Functions (ManufacturerSpecific)\"\n"; assert_eq!(csv_output, expected); } diff --git a/tests/test_other_meters.rs b/tests/test_other_meters.rs index 6cf1338..8659137 100644 --- a/tests/test_other_meters.rs +++ b/tests/test_other_meters.rs @@ -35,7 +35,7 @@ mod tests { assert_eq!(header["short_tpl_header"]["access_number"], 42); let records = json["data_records"].as_array().unwrap(); - assert_eq!(records.len(), 11); + assert_eq!(records.len(), 12); // (label, units, scale_exp, value, storage, tariff, device) let expected: &[(&str, &[(&str, i64)], i64, f64, u64, u64, u64)] = &[ @@ -110,5 +110,28 @@ mod tests { ); } } + + // Record 11: 0x1F = MoreRecordsFollow (manufacturer specific, more records follow) + let rec = &records[11]; + let di = &rec["data_record_header"]["processed_data_record_header"]["data_information"]; + assert_eq!( + di["data_field_coding"]["SpecialFunctions"] + .as_str() + .unwrap(), + "MoreRecordsFollow" + ); + assert!( + rec["data_record_header"]["processed_data_record_header"]["value_information"] + .is_null() + ); + assert_eq!( + rec["data_record_header"]["raw_data_record_header"]["data_information_block"] + ["data_information_field"]["data"], + 0x1F + ); + assert!(rec["data"]["value"]["ManufacturerSpecific"] + .as_array() + .unwrap() + .is_empty()); } }