From 8168e66d6a89999db9e0597f9a3146fc4fa9093a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 8 Jan 2022 15:50:15 +0100 Subject: [PATCH] Refactor aventieswm driver. --- src/meter_aventieswm.cc | 253 +++++++++-------------------- src/meter_detection.h | 1 - src/meters.cc | 94 ++++++++++- src/meters.h | 11 +- src/meters_common_implementation.h | 27 ++- src/translatebits.cc | 2 + src/translatebits.h | 3 + tests/test_unix_timestamp.sh | 10 +- 8 files changed, 204 insertions(+), 197 deletions(-) diff --git a/src/meter_aventieswm.cc b/src/meter_aventieswm.cc index 812505a..d1679d0 100644 --- a/src/meter_aventieswm.cc +++ b/src/meter_aventieswm.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2020 Fredrik Öhrström + Copyright (C) 2020-2021 Fredrik Öhrström This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,196 +24,105 @@ using namespace std; -struct MeterAventiesWM : public virtual MeterCommonImplementation { - MeterAventiesWM(MeterInfo &mi); - - // Total water counted through the meter - double totalWaterConsumption(Unit u); - bool hasTotalWaterConsumption(); - - double consumptionAtSetDate(Unit u); +struct MeterAventiesWM : public virtual MeterCommonImplementation +{ + MeterAventiesWM(MeterInfo &mi, DriverInfo &di); private: - void processContent(Telegram *t); - - string errorFlagsHumanReadable(); double total_water_consumption_m3_ {}; double consumption_at_set_date_m3_[14]; - uint16_t error_flags_; - + string error_flags_; }; -shared_ptr createAventiesWM(MeterInfo &mi) +static bool ok = registerDriver([](DriverInfo&di) { - return shared_ptr(new MeterAventiesWM(mi)); -} + di.setName("aventieswm"); + di.setMeterType(MeterType::WaterMeter); + di.setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_IV); + di.addLinkMode(LinkMode::T1); + di.addDetection(MANUFACTURER_AAA, 0x07, 0x25); + di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new MeterAventiesWM(mi, di)); }); +}); -MeterAventiesWM::MeterAventiesWM(MeterInfo &mi) : - MeterCommonImplementation(mi, "aventieswm") +MeterAventiesWM::MeterAventiesWM(MeterInfo &mi, DriverInfo &di) : + MeterCommonImplementation(mi, di) { - setMeterType(MeterType::WaterMeter); - - setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_IV); - - addLinkMode(LinkMode::T1); - - addPrint("total", Quantity::Volume, - [&](Unit u){ return totalWaterConsumption(u); }, - "The total water consumption recorded by this meter.", - true, true); - -/* addPrint("set_date", Quantity::Text, - [&](){ return setDate(); }, - "The most recent billing period date.", - false, true); -*/ + addFieldWithExtractor( + "total", + Quantity::Volume, + NoDifVifKey, + VifScaling::Auto, + MeasurementType::Instantaneous, + ValueInformation::Volume, + StorageNr(0), + TariffNr(0), + IndexNr(1), + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + "The total water consumption recorded by this meter.", + SET_FUNC(total_water_consumption_m3_, Unit::M3), + GET_FUNC(total_water_consumption_m3_, Unit::M3)); for (int i=1; i<=14; ++i) { string msg, info; strprintf(msg, "consumption_at_set_date_%d", i); strprintf(info, "Water consumption at the %d billing period date.", i); - addPrint(msg, Quantity::Volume, - [this,i](Unit u){ return consumption_at_set_date_m3_[i-1]; }, - info, - false, true); + addFieldWithExtractor( + msg, + Quantity::Volume, + NoDifVifKey, + VifScaling::Auto, + MeasurementType::Instantaneous, + ValueInformation::Volume, + StorageNr(i), + TariffNr(0), + IndexNr(1), + PrintProperty::JSON, + info, + SET_FUNC(consumption_at_set_date_m3_[i-1], Unit::M3), + GET_FUNC(consumption_at_set_date_m3_[i-1], Unit::M3)); } - addPrint("error_flags", Quantity::Text, - [&](){ return errorFlagsHumanReadable(); }, - "Error flags.", - true, true); + addStringFieldWithExtractorAndLookup( + "error_flags", + Quantity::Text, + DifVifKey("02FD17"), + MeasurementType::Unknown, + ValueInformation::Any, + AnyStorageNr, + AnyTariffNr, + IndexNr(1), + PrintProperty::JSON | PrintProperty::FIELD, + "Error flags.", + SET_STRING_FUNC(error_flags_), + GET_STRING_FUNC(error_flags_), + { + { + { + "ERROR_FLAGS", + Translate::Type::BitToString, + 0xffff, + { + { 0x01, "MEASUREMENT" }, + { 0x02, "SABOTAGE" }, + { 0x04, "BATTERY" }, + { 0x08, "CS" }, + { 0x10, "HF" }, + { 0x20, "RESET" } + } + }, + }, + }); } -string MeterAventiesWM::errorFlagsHumanReadable() -{ - string s; - if (error_flags_ & 0x01) s += "MEASUREMENT "; - if (error_flags_ & 0x02) s += "SABOTAGE "; - if (error_flags_ & 0x04) s += "BATTERY "; - if (error_flags_ & 0x08) s += "CS "; - if (error_flags_ & 0x10) s += "HF "; - if (error_flags_ & 0x20) s += "RESET "; - - if (s.length() > 0) { - s.pop_back(); - return s; - } - - if (error_flags_ != 0 && s.length() == 0) { - // Some higher bits are set that we do not know about! Fall back to printing the number! - strprintf(s, "0x%04X", error_flags_); - } - return s; -} +// Test: Votten aventieswm 61070071 A004EB23329A477F1DD2D7820B56EB3D +// telegram=76442104710007612507727100076121042507B5006005E2E95A3C2A1279A5415E6732679B43369FD5FDDDD783EEEBB48236D34E7C94AF0A18A5FDA5F7D64111EB42D4D891622139F2952F9D12A20088DFA4CF8123871123EE1F6C1DCEA414879DDB4E05E508F1826D7EFBA6964DF804C9261EA23BBF03 +// {"media":"water","meter":"aventieswm","name":"Votten","id":"61070071","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"error_flags":"","timestamp":"1111-11-11T11:11:11Z"} +// |Votten;61070071;466.472000;;1111-11-11 11:11.11 -void MeterAventiesWM::processContent(Telegram *t) -{ - /* - (q400) 17: 2f2f decrypt check bytes - (q400) 19: 04 dif (32 Bit Integer/Binary Instantaneous value) - (q400) 1a: 13 vif (Volume l) - (q400) 1b: * 1F480000 total consumption (18.463000 m3) - (q400) 1f: 43 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 20: 14 vif (Volume 10⁻² m³) - (q400) 21: * 360700 consumption at set date (18.460000 m3) - (q400) 24: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 25: 01 dife (subunit=0 tariff=0 storagenr=2) - (q400) 26: 14 vif (Volume 10⁻² m³) - (q400) 27: FE0600 - (q400) 2a: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 2b: 01 dife (subunit=0 tariff=0 storagenr=3) - (q400) 2c: 14 vif (Volume 10⁻² m³) - (q400) 2d: DD0600 - (q400) 30: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 31: 02 dife (subunit=0 tariff=0 storagenr=4) - (q400) 32: 14 vif (Volume 10⁻² m³) - (q400) 33: C30600 - (q400) 36: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 37: 02 dife (subunit=0 tariff=0 storagenr=5) - (q400) 38: 14 vif (Volume 10⁻² m³) - (q400) 39: BD0600 - (q400) 3c: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 3d: 03 dife (subunit=0 tariff=0 storagenr=6) - (q400) 3e: 14 vif (Volume 10⁻² m³) - (q400) 3f: BC0600 - (q400) 42: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 43: 03 dife (subunit=0 tariff=0 storagenr=7) - (q400) 44: 14 vif (Volume 10⁻² m³) - (q400) 45: 000000 - (q400) 48: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 49: 04 dife (subunit=0 tariff=0 storagenr=8) - (q400) 4a: 14 vif (Volume 10⁻² m³) - (q400) 4b: BB0600 - (q400) 4e: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 4f: 04 dife (subunit=0 tariff=0 storagenr=9) - (q400) 50: 14 vif (Volume 10⁻² m³) - (q400) 51: BB0600 - (q400) 54: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 55: 05 dife (subunit=0 tariff=0 storagenr=10) - (q400) 56: 14 vif (Volume 10⁻² m³) - (q400) 57: BA0600 - (q400) 5a: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 5b: 05 dife (subunit=0 tariff=0 storagenr=11) - (q400) 5c: 14 vif (Volume 10⁻² m³) - (q400) 5d: B80600 - (q400) 60: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 61: 06 dife (subunit=0 tariff=0 storagenr=12) - (q400) 62: 14 vif (Volume 10⁻² m³) - (q400) 63: 220600 - (q400) 66: C3 dif (24 Bit Integer/Binary Instantaneous value storagenr=1) - (q400) 67: 06 dife (subunit=0 tariff=0 storagenr=13) - (q400) 68: 14 vif (Volume 10⁻² m³) - (q400) 69: DC0500 - (q400) 6c: 83 dif (24 Bit Integer/Binary Instantaneous value) - (q400) 6d: 07 dife (subunit=0 tariff=0 storagenr=14) - (q400) 6e: 14 vif (Volume 10⁻² m³) - (q400) 6f: CD0500 - (q400) 72: 02 dif (16 Bit Integer/Binary Instantaneous value) - (q400) 73: FD vif (Second extension FD of VIF-codes) - (q400) 74: 17 vife (Error flags (binary)) - (q400) 75: 0000 */ - int offset; - string key; - - if(findKey(MeasurementType::Unknown, ValueInformation::Volume, 0, 0, &key, &t->values)) { - extractDVdouble(&t->values, key, &offset, &total_water_consumption_m3_); - t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_m3_); - } - - for (int i=1; i<=14; ++i) - { - if (findKey(MeasurementType::Unknown, ValueInformation::Volume, i, 0, &key, &t->values)) - { - string info; - strprintf(info, " consumption at set date %d (%%f m3)", i); - extractDVdouble(&t->values, key, &offset, &consumption_at_set_date_m3_[i-1]); - t->addMoreExplanation(offset, info.c_str(), consumption_at_set_date_m3_[i-1]); - } - } - - key = "02FD17"; - if (hasKey(&t->values, key)) { - extractDVuint16(&t->values, "02FD17", &offset, &error_flags_); - t->addMoreExplanation(offset, " error flags (%04X)", error_flags_); - } - -} - -double MeterAventiesWM::totalWaterConsumption(Unit u) -{ - assertQuantity(u, Quantity::Volume); - return convert(total_water_consumption_m3_, Unit::M3, u); -} - -bool MeterAventiesWM::hasTotalWaterConsumption() -{ - return true; -} - -double MeterAventiesWM::consumptionAtSetDate(Unit u) -{ - return consumption_at_set_date_m3_[0]; -} +// Test: Votten aventieswm 61070071 NOKEY +// telegram=76442104710007612507727100076121042507B50060052F2F0413281E0700431404B60083011440B300C30114A5AF00830214CBAC00C3021463A8008303149EA500C3031433A200830414C79F00C304148F9C00830514989900C30514CF9700830614269400C30614069100830714C88B0002FD171111 +// {"media":"water","meter":"aventieswm","name":"Votten","id":"61070071","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"error_flags":"MEASUREMENT HF UNKNOWN_ERROR_FLAGS(0x1100)","timestamp":"1111-11-11T11:11:11Z"} +// |Votten;61070071;466.472000;MEASUREMENT HF UNKNOWN_ERROR_FLAGS(0x1100);1111-11-11 11:11.11 diff --git a/src/meter_detection.h b/src/meter_detection.h index c55de4e..1fee98a 100644 --- a/src/meter_detection.h +++ b/src/meter_detection.h @@ -22,7 +22,6 @@ // meter driver, manufacturer, media, version // #define METER_DETECTION \ - X(AVENTIESWM,MANUFACTURER_AAA, 0x07, 0x25) \ X(AVENTIESHCA,MANUFACTURER_AAA, 0x08, 0x55) \ X(APATOR08, 0x8614/*APT?*/, 0x03, 0x03) \ X(APATOR162, MANUFACTURER_APA, 0x06, 0x05) \ diff --git a/src/meters.cc b/src/meters.cc index b8c090a..14caf8e 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -602,7 +602,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, NULL, NULL, NULL, - NULL + NULL, + NoLookup )); } @@ -633,7 +634,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, Unit NULL, NULL, NULL, - NULL + NULL, + NoLookup )); } @@ -662,7 +664,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, NULL, NULL, NULL, - NULL + NULL, + NoLookup )); } @@ -739,7 +742,8 @@ void MeterCommonImplementation::addFieldWithExtractor( setValueFunc, NULL, extract, - NULL + NULL, + NoLookup )); } @@ -817,7 +821,87 @@ void MeterCommonImplementation::addStringFieldWithExtractor( NULL, setValueFunc, NULL, - extract + extract, + NoLookup + )); +} + +void MeterCommonImplementation::addStringFieldWithExtractorAndLookup( + string vname, + Quantity vquantity, + DifVifKey dif_vif_key, + MeasurementType mt, + ValueInformation vi, + StorageNr s, + TariffNr t, + IndexNr i, + int print_properties, + string help, + function setValueFunc, + function getValueFunc, + Translate::Lookup lookup) +{ + string default_unit = unitToStringLowerCase(defaultUnitForQuantity(vquantity)); + string field_name = vname+"_"+default_unit; + fields_.push_back(field_name); + + // Compose the extract function. + function extract = + [](FieldInfo *fi, Meter *m, Telegram *t) + { + bool found = false; + string key = fi->difVifKey().str(); + int offset {}; + if (key == "") + { + // Search for key. + bool ok = findKeyWithNr(fi->measurementType(), + fi->valueInformation(), + fi->storageNr().intValue(), + fi->tariffNr().intValue(), + fi->indexNr().intValue(), + &key, + &t->values); + if (!ok) return false; + } + uint64_t extracted_bits {}; + if (extractDVlong(&t->values, key, &offset, &extracted_bits)) + { + string translated_bits = fi->lookup().translate(extracted_bits); + fi->setValueString(translated_bits); + t->addMoreExplanation(offset, fi->renderJsonText()); + found = true; + } + else + { + assert(0); + } + return found; + }; + + prints_.push_back( + FieldInfo(vname, + vquantity, + defaultUnitForQuantity(vquantity), + dif_vif_key, + VifScaling::None, + mt, + vi, + s, + t, + i, + help, + (print_properties & PrintProperty::FIELD) != 0, + (print_properties & PrintProperty::JSON) != 0, + (print_properties & PrintProperty::IMPORTANT) != 0, + field_name, + NULL, + getValueFunc, + NULL, + setValueFunc, + NULL, + extract, + lookup )); } diff --git a/src/meters.h b/src/meters.h index 7554f11..ab6ef8c 100644 --- a/src/meters.h +++ b/src/meters.h @@ -21,6 +21,7 @@ #include"dvparser.h" #include"util.h" #include"units.h" +#include"translatebits.h" #include"wmbus.h" #include @@ -52,7 +53,6 @@ LIST_OF_METER_TYPES X(unknown, 0, UnknownMeter, UNKNOWN, Unknown) \ X(apator08, T1_bit, WaterMeter, APATOR08, Apator08) \ X(apator162, C1_bit|T1_bit, WaterMeter, APATOR162, Apator162) \ - X(aventieswm, T1_bit, WaterMeter, AVENTIESWM, AventiesWM) \ X(aventieshca,T1_bit, HeatCostAllocationMeter, AVENTIESHCA, AventiesHCA) \ X(bfw240radio,T1_bit, HeatCostAllocationMeter, BFW240RADIO, BFW240Radio) \ X(cma12w, C1_bit|T1_bit, TempHygroMeter, CMA12W, CMa12w) \ @@ -299,7 +299,8 @@ struct FieldInfo function set_value_double, function set_value_string, function extract_double, - function extract_string + function extract_string, + Translate::Lookup lookup ) : vname_(vname), xuantity_(xuantity), @@ -321,7 +322,8 @@ struct FieldInfo set_value_double_(set_value_double), set_value_string_(set_value_string), extract_double_(extract_double), - extract_string_(extract_string) + extract_string_(extract_string), + lookup_(lookup) {} string vname() { return vname_; } @@ -354,6 +356,8 @@ struct FieldInfo string renderJson(vector *additional_conversions); string renderJsonText(); + Translate::Lookup& lookup() { return lookup_; } + private: string vname_; // Value name, like: total current previous target @@ -378,6 +382,7 @@ private: function set_value_string_; // Call back to set the value string in the c++ object function extract_double_; // Extract field from telegram and insert into meter. function extract_string_; // Extract field from telegram and insert into meter. + Translate::Lookup lookup_; }; struct BusManager; diff --git a/src/meters_common_implementation.h b/src/meters_common_implementation.h index 640047e..a105563 100644 --- a/src/meters_common_implementation.h +++ b/src/meters_common_implementation.h @@ -88,8 +88,8 @@ protected: void addPrint(string vname, Quantity vquantity, function getValueFunc, string help, bool field, bool json); -#define SET_FUNC(varname,to_unit) {[&](Unit from_unit, double d){varname = convert(d, from_unit, to_unit);}} -#define GET_FUNC(varname,from_unit) {[&](Unit to_unit){return convert(varname, from_unit, to_unit);}} +#define SET_FUNC(varname,to_unit) {[=](Unit from_unit, double d){varname = convert(d, from_unit, to_unit);}} +#define GET_FUNC(varname,from_unit) {[=](Unit to_unit){return convert(varname, from_unit, to_unit);}} void addFieldWithExtractor( string vname, // Name of value without unit, eg total @@ -106,8 +106,8 @@ protected: function setValueFunc, // Use the SET macro above. function getValueFunc); // Use the GET macro above. -#define SET_STRING_FUNC(varname) {[&](string s){varname = s;}} -#define GET_STRING_FUNC(varname) {[&](){return varname; }} +#define SET_STRING_FUNC(varname) {[=](string s){varname = s;}} +#define GET_STRING_FUNC(varname) {[=](){return varname; }} void addStringFieldWithExtractor( string vname, // Name of value without unit, eg total @@ -123,11 +123,20 @@ protected: function setValueFunc, // Use the SET_STRING macro above. function getValueFunc); // Use the GET_STRING macro above. - // Decode a bit field and print - void addPrintTextFromBits(string vname, // Name of field, no suffix unit added since it is text. - string help, // An explanation of the field. - int props, // json,field,important - function getValueFunc); + void addStringFieldWithExtractorAndLookup( + string vname, // Name of value without unit, eg total + Quantity vquantity, // Value belongs to this quantity. + DifVifKey dif_vif_key, // You can hardocde a dif vif header here or use NoDifVifKey + MeasurementType mt, // If not using a hardcoded key, search for mt,vi,s,t and i instead. + ValueInformation vi, + StorageNr s, + TariffNr t, + IndexNr i, + int print_properties, // Should this be printed by default in fields,json and hr. + string help, + function setValueFunc, // Use the SET_STRING macro above. + function getValueFunc, // Use the GET_STRING macro above. + Translate::Lookup lookup); // Translate the bits/indexes. // The default implementation of poll does nothing. // Override for mbus meters that need to be queried and likewise for C2/T2 wmbus-meters. diff --git a/src/translatebits.cc b/src/translatebits.cc index 15e3e51..f57fa89 100644 --- a/src/translatebits.cc +++ b/src/translatebits.cc @@ -97,3 +97,5 @@ string Lookup::translate(uint64_t bits) while (s.back() == ' ') s.pop_back(); return s; } + +Lookup NoLookup = {}; diff --git a/src/translatebits.h b/src/translatebits.h index a9000cd..6ba39fe 100644 --- a/src/translatebits.h +++ b/src/translatebits.h @@ -49,7 +49,10 @@ namespace Translate std::vector rules; std::string translate(uint64_t bits); + bool hasLookups() { return rules.size() > 0; } }; }; +extern Translate::Lookup NoLookup; + #endif diff --git a/tests/test_unix_timestamp.sh b/tests/test_unix_timestamp.sh index e6ef7dc..b082121 100755 --- a/tests/test_unix_timestamp.sh +++ b/tests/test_unix_timestamp.sh @@ -8,18 +8,14 @@ TEST=testoutput TESTNAME="Test unix timestamp in fields" TESTRESULT="ERROR" -METERS="Votten aventieswm 61070071 A004EB23329A477F1DD2D7820B56EB3D" +METERS="Votten aventieswm 61070071 A004EB23329A477F1DD2D7820B56EB3D" cat simulations/simulation_unix_timestamp.txt | grep '^{' > $TEST/test_expected.txt -# Wait for the second to switch before starting the run. This will avoid spurious errors -# where the second barrier switches inside the test. -PREV=$(date +%s) -NOW=$PREV -while [ "$NOW" = "$PREV" ] ; do NOW=$(date "+%s") ; done +NOW=$(date +%s) cat simulations/simulation_unix_timestamp.txt | grep '^|' | sed 's/^|//' | sed "s/UT/${NOW}/" > $TEST/test_expected.txt -$PROG --format=fields --separator='*' --field_extra=extra_info --selectfields=total_m3,timestamp_ut,timestamp_utc,timestamp_lt,name,id,extra simulations/simulation_t1.txt $METERS > $TEST/test_output.txt 2> $TEST/test_stderr.txt +$PROG --format=fields --separator='*' --field_extra=extra_info --selectfields=total_m3,timestamp_ut,timestamp_utc,timestamp_lt,name,id,extra simulations/simulation_unix_timestamp.txt $METERS > $TEST/test_output.txt 2> $TEST/test_stderr.txt if [ "$?" = "0" ] then