From: German Service Network Date: Sun, 19 Jan 2025 13:27:16 +0000 (+0100) Subject: Create EnergyLogger4000-Reader X-Git-Url: https://git.gsnw.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ca664356ffd77b61de9509ef8f87a1e237cd959e;p=EnergyLogger4000-Reader.git Create EnergyLogger4000-Reader --- ca664356ffd77b61de9509ef8f87a1e237cd959e diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c557b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7b6d023 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "EnergyLogger4000-Reader" +version = "0.1.0" +edition = "2021" + +[dependencies] +getopts = "0.2" +chrono = "0.4" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..eef2813 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# Voltcraft Energy Logger 4000 Reader + +Voltcraft Energy Logger 4000 Reader is a tool for processing the Voltcraft Energy Logger 4000 binary data which can be downloaded from the device using an SD card. + +The code is certainly not perfect, but kept as simple as possible + +## Usage + +$: `EnergyLogger4000-Reader -h` + +``` +Usage: EnergyLogger4000-Reader [options] + +Options: + -f, --file NAME Read file + -d, --directory NAME + Read files from directory + -h, --help Print this help menu + -v, --version Output version information and exit +``` + +### Examples + +You can load a single file + +``` +EnergyLogger4000-Reader -f B08F9CD2.BIN +``` + +Or a complete directory +ATTENTION: Only one complete data set from the SD card should be included at any one time + +``` +EnergyLogger4000-Reader -d /mnt/ +``` + +# Known bugs + +* An absolute path must always be specified. Unfortunately, a path such as `~/B08F9CD2.BIN` or `~/mypath/` does not work. + +# Reference + +* http://wiki.td-er.nl/index.php?title=Energy_Logger_3500 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9eab18a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,356 @@ +use std::{fs, env}; +use std::io::{self, Read}; + +use getopts::Options; +use chrono::{NaiveDateTime, Duration}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const PROGRAM: &str = env!("CARGO_PKG_NAME"); + +#[derive(Debug)] +#[allow(dead_code)] +struct InfoDay { + day: usize, + value: f32, +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Info { + total_power_consumption: f32, + total_recorded_time: f32, + total_on_time: f32, + total_kwh_today_min: Vec, + total_recorded_time_today_min: Vec, + total_on_time_today_min: Vec, + unit_id: u8, + tariff1: f64, + tariff2: f64, + timestamp: String, +} + +#[derive(Debug)] +#[allow(dead_code)] +struct DataSample { + timestamp: String, + voltage: f32, + current: f32, + power_factor: f32, +} + +#[derive(Debug)] +#[allow(dead_code)] +struct Data { + timestamp: String, + data_samples: Vec, +} + +fn print_usage(opts: Options) { + let brief = format!("Usage: {} [options]", PROGRAM); + print!("{}", opts.usage(&brief)); +} + +fn print_version() { + println!("{} - v{}", PROGRAM, VERSION); +} + +#[allow(dead_code)] +fn print_debug_buffer(buffer: &[u8]) { + println!("All bytes from the file:"); + println!("Number of bytes: {}", buffer.len()); + for (index, byte) in buffer.iter().enumerate() { + println!("Byte {}: 0x{:02X}", index, byte); + } +} + +fn print_info_file(info: &Info) { + println!("--- INFO ---"); + println!("Unit ID: {}", info.unit_id); + println!("Timestamp: {}", info.timestamp); + println!("Tarif 1: {}", info.tariff1); + println!("Tarif 2: {}", info.tariff2); + println!("Total power comsumption: {} kWh", info.total_power_consumption); + println!("Total recorded time {} h", info.total_recorded_time); + println!("Total on time {} h", info.total_on_time); + if info.total_kwh_today_min.len() == 10 && info.total_recorded_time_today_min.len() == 10 && info.total_on_time_today_min.len() == 10 { + for i in 0..10 { + println!("- Day: {}", i + 1); + println!("-- Total kWh today min: {} kWh", info.total_kwh_today_min[i].value); + println!("-- Total recorded time today min: {} h", info.total_recorded_time_today_min[i].value); + println!("-- Total on time today min: {} h", info.total_on_time_today_min[i].value); + } + } +} + +fn print_data_file(data: &Data) { + for record in &data.data_samples { + print!("[{}] ", record.timestamp); + print!("U={}V ", record.voltage); + print!("I={}mA ", record.current); + // \u{03C6} print sign cosPhi + println!("cos\u{03C6}={}%", record.power_factor); + } +} + +fn read_info_file(buffer: &[u8]) -> Result> { + + let total_power_consumption = ((buffer[5] as u32) * 256 * 256 + (buffer[6] as u32) * 256 + (buffer[7] as u32)) as f32 / 1000.0; + let total_recorded_time = ((buffer[8] as u32) * 256 * 256 + (buffer[9] as u32) * 256 + (buffer[10] as u32)) as f32 / 100.0; + let total_on_time = ((buffer[11] as u32) * 256 * 256 + (buffer[12] as u32) * 256 + (buffer[13] as u32)) as f32 / 100.0; + + let mut total_kwh_today_min: Vec = Vec::new(); + let total_kwh_today_min_start_index = 14; + let total_kwh_today_min_record_size = 3; // Read next 3 Bytes + let total_kwh_today_min_num_records = 10; + + for day in 0..total_kwh_today_min_num_records { + let offset = total_kwh_today_min_start_index + day * total_kwh_today_min_record_size; + let record_bytes = &buffer[offset..offset + total_kwh_today_min_record_size]; + let loop_total_kwh_today_min = ((record_bytes[0] as u32) * 256 * 256 + (record_bytes[1] as u32) * 256 + (record_bytes[2] as u32)) as f32 / 1000.0; + total_kwh_today_min.push(InfoDay { + day: day, + value: loop_total_kwh_today_min, + }); + } + + let mut total_recorded_time_today_min: Vec = Vec::new(); + let total_recorded_time_today_min_start_index = 44; + let total_recorded_time_today_min_record_size = 2; // Read next 2 Bytes + let total_recorded_time_today_min_records = 10; + + for day in 0..total_recorded_time_today_min_records { + let offset = total_recorded_time_today_min_start_index + day * total_recorded_time_today_min_record_size; + let record_bytes = &buffer[offset..offset + total_recorded_time_today_min_record_size]; + let loop_total_recorded_time_today_min= ((record_bytes[0] as u16) * 256 + (record_bytes[1] as u16)) as f32 / 100.0; + total_recorded_time_today_min.push(InfoDay { + day: day, + value: loop_total_recorded_time_today_min, + }); + } + + let mut total_on_time_today_min: Vec = Vec::new(); + let total_on_time_today_min_start_index = 64; + let total_on_time_today_min_record_size = 2; // Read next 2 Bytes + let total_on_time_today_min_records = 10; + + for day in 0..total_on_time_today_min_records { + let offset = total_on_time_today_min_start_index + day * total_on_time_today_min_record_size; + let record_bytes = &buffer[offset..offset + total_on_time_today_min_record_size]; + let loop_total_on_time_today_min= ((record_bytes[0] as u16) * 256 + (record_bytes[1] as u16)) as f32 / 100.0; + total_on_time_today_min.push(InfoDay { + day: day, + value: loop_total_on_time_today_min, + }); + } + + let unit_id = buffer[84]; + + let tariff1 = (buffer[85] as u32) * 256 * 256 *256 + (buffer[86] as u32) * 256 * 256 + (buffer[87] as u32) * 256 + (buffer[88] as u32); + let mut decoded_tariff1 = 0.0; + for i in 0..4 { + let byte = ((tariff1 >> (8 * (3 - i))) & 0xFF) as u32; + decoded_tariff1 += (byte as f64) * 10f64.powi(-(i as i32)); + } + + let tariff2 = (buffer[89] as u32) * 256 * 256 *256 + (buffer[90] as u32) * 256 * 256 + (buffer[91] as u32) * 256 + (buffer[92] as u32); + let mut decoded_tariff2 = 0.0; + for i in 0..4 { + let byte = ((tariff2 >> (8 * (3 - i))) & 0xFF) as u32; + decoded_tariff2 += (byte as f64) * 10f64.powi(-(i as i32)); + } + + let time_hour = buffer[93]; + let time_minute = buffer[94]; + let date_month = buffer[95]; + let date_day = buffer[96]; + let date_year = buffer[97]; + + let timestamp = NaiveDateTime::parse_from_str(&format!("20{:02}-{:02}-{:02} {:02}:{:02}:00", date_year, date_month, date_day, time_hour, time_minute), "%Y-%m-%d %H:%M:%S").expect("Invalid Date/Time-Format"); + + if buffer[98..102] != [0xFF, 0xFF, 0xFF, 0xFF] { + let error_message =format!("Info file has no end of file code"); + return Err(Box::::from(error_message)); + } + + let result = Info { + total_power_consumption: total_power_consumption, + total_recorded_time: total_recorded_time, + total_on_time: total_on_time, + total_kwh_today_min: total_kwh_today_min, + total_recorded_time_today_min: total_recorded_time_today_min, + total_on_time_today_min, + unit_id: unit_id, + tariff1: decoded_tariff1, + tariff2: decoded_tariff2, + timestamp: timestamp.to_string(), + }; + + Ok(result) +} + +fn read_data_file(buffer: &[u8]) -> Result> { + let date_month = buffer[3]; + let date_day = buffer[4]; + let date_year = buffer[5]; + let time_hour = buffer[6]; + let time_minute = buffer[7]; + + let timestamp = NaiveDateTime::parse_from_str(&format!("20{:02}-{:02}-{:02} {:02}:{:02}:00", date_year, date_month, date_day, time_hour, time_minute), "%Y-%m-%d %H:%M:%S").expect("Invalid Date/Time-Format"); + + let mut data_samples: Vec = Vec::new(); + let mut start_timestamp = timestamp; + let mut buffer_index = 8; + while buffer_index + 4 < buffer.len() { + if buffer[buffer_index..].iter().take(4).all(|&b| b == 0xFF) { + break; + } + + // 2 Bytes: Voltage (in tenths of volt) = byte(0) * 256 + byte(1) + let voltage: f32 = ((buffer[buffer_index] as u16) * 256 + (buffer[buffer_index + 1] as u16)) as f32 / 10.0; + + // 2 Bytes: Current (in mA) = byte(2) * 256 + byte(3) + let current: f32 = ((buffer[buffer_index + 2] as u16) * 256 + (buffer[buffer_index + 3] as u16)) as f32 / 100.0; + + // 1 Byte: PowerFactor (in procent) = byte(4) + let power_factor = buffer[buffer_index + 4] as f32 / 100.0; + + data_samples.push(DataSample { + timestamp: start_timestamp.to_string(), + voltage: voltage, + current: current, + power_factor: power_factor, + }); + + start_timestamp += Duration::seconds(60); + buffer_index += 5; + } + + let result = Data { + timestamp: timestamp.to_string(), + data_samples: data_samples, + }; + + Ok(result) +} + +fn main() -> io::Result<()> { + let args: Vec = env::args().collect(); + + let mut opts = Options::new(); + opts.optopt("f", "file", "Read file", "NAME"); + opts.optopt("d", "directory", "Read files from directory", "NAME"); + opts.optflag("h", "help", "Print this help menu"); + opts.optflag("v", "version", "Output version information and exit"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => { m } + Err(f) => { panic!("{}", f.to_string()) } + }; + + if matches.opt_present("h") { + print_usage(opts); + return Ok(()); + } + + if matches.opt_present("v") { + print_version(); + return Ok(()); + } + + if let Some(load_file) = matches.opt_str("f") { + if load_file.is_empty() { + eprintln!("[Error] Option -f or --file has no parameter"); + return Ok(()); + } + + let mut file = fs::File::open(&load_file).unwrap(); + let metadata = file.metadata().unwrap(); + + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + if metadata.len() == 102 { + if buffer.len() >= 5 { + if &buffer[0..5] == b"INFO:" { + match read_info_file(&buffer) { + Ok(data) => { + print_info_file(&data); + } + Err(err) => { + eprintln!("[Error] {}", err); + } + } + } + } + } else if metadata.len() != 102 { + if buffer.len() >= 3 { + if &buffer[0..3] == [0xE0, 0xC5, 0xEA] { + match read_data_file(&buffer) { + Ok(data) => { + print_data_file(&data); + } + Err(err) => { + eprintln!("[Error] {}", err); + } + } + } + } + } + } + + if let Some(load_directory) = matches.opt_str("d") { + if load_directory.is_empty() { + eprintln!("[Error] Option -d or --directory has no parameter"); + return Ok(()); + } + + let entries = fs::read_dir(load_directory).unwrap(); + + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + if let Some(filename) = path.file_name() { + if let Some(filename_str) = filename.to_str() { + if filename_str.len() == 12 && filename_str.ends_with(".BIN") { + let _filename_prefix = &filename_str[..8]; + let mut file = fs::File::open(&path).unwrap(); + let metadata = file.metadata().unwrap(); + + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).unwrap(); + + if metadata.len() == 102 { + if buffer.len() >= 5 { + if &buffer[0..5] == b"INFO:" { + match read_info_file(&buffer) { + Ok(data) => { + print_info_file(&data); + } + Err(err) => { + eprintln!("[Error] {}", err); + } + } + } + } + } else if metadata.len() != 102 { + if buffer.len() >= 3 { + if &buffer[0..3] == [0xE0, 0xC5, 0xEA] { + match read_data_file(&buffer) { + Ok(data) => { + print_data_file(&data); + } + Err(err) => { + eprintln!("[Error] {}", err); + } + } + } + } + } + } + } + } + } + } + } + Ok(()) +} \ No newline at end of file