diff --git a/src/cmdline.cc b/src/cmdline.cc index 014ba10..e78bb37 100644 --- a/src/cmdline.cc +++ b/src/cmdline.cc @@ -485,6 +485,18 @@ static shared_ptr parseNormalCommandLine(Configuration *c, int ar i++; continue; } + if (!strncmp(argv[i], "--calculate_", 12)) + { + // For example: --calculate_adjusted_kwh='total_kwh + 12345 kwh' + string extra_calculated_field = string(argv[i]+12); + if (extra_calculated_field == "") { + error("The calculated field command cannot be empty.\n"); + } + debug("(cmdline) add calculated field %s\n", extra_calculated_field.c_str()); + c->extra_calculated_fields.push_back(extra_calculated_field); + i++; + continue; + } if (!strncmp(argv[i], "--listenvs=", 11)) { c->list_shell_envs = true; c->list_meter = string(argv[i]+11); diff --git a/src/config.cc b/src/config.cc index dcb1a09..3c1d8a5 100644 --- a/src/config.cc +++ b/src/config.cc @@ -59,6 +59,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) vector telegram_shells; vector alarm_shells; vector extra_constant_fields; + vector extra_calculated_fields; vector selected_fields; debug("(config) loading meter file %s\n", file.c_str()); @@ -152,6 +153,12 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) string keyvalue = p.first.substr(off)+"="+p.second; extra_constant_fields.push_back(keyvalue); } + else + if (startsWith(p.first, "calculate_")) + { + string keyvalue = p.first.substr(10)+"="+p.second; + extra_calculated_fields.push_back(keyvalue); + } else warning("Found invalid key \"%s\" in meter config file\n", p.first.c_str()); @@ -195,6 +202,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) } if (use) { mi.extra_constant_fields = extra_constant_fields; + mi.extra_calculated_fields = extra_calculated_fields; mi.shells = telegram_shells; mi.idsc = toIdsCommaSeparated(mi.ids); mi.selected_fields = selected_fields; diff --git a/src/config.h b/src/config.h index a370734..16e267a 100644 --- a/src/config.h +++ b/src/config.h @@ -136,6 +136,7 @@ struct Configuration std::vector selected_fields; std::vector meters; std::vector extra_constant_fields; // Additional constant fields to always add to json. + std::vector extra_calculated_fields; // Additional calculated fields to always add to json. // These extra constant fields can also be part of selected with selectfields. std::vector send_bus_content; // Telegrams used to wake up a meter for reading or mbus read-out requests. std::set probe_for; // Which devices should be probed for? DEVICE_AUTO means all. diff --git a/src/driver_em24.cc b/src/driver_em24.cc index bba7e61..fa458aa 100644 --- a/src/driver_em24.cc +++ b/src/driver_em24.cc @@ -133,13 +133,15 @@ namespace .set(DifVifKey("04FB82F53C")) ); + /* addNumericFieldWithCalculator( "total_apparent_energy_consumption", "Calculated: the total apparent energy consumption.", PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, - Quantity::Energy, + Quantity::Apparent_Energy, "total_energy_consumption_kwh + 99 kwh" ); + */ } } diff --git a/src/main.cc b/src/main.cc index 843f4d8..1381bfc 100644 --- a/src/main.cc +++ b/src/main.cc @@ -454,9 +454,10 @@ void setup_log_file(Configuration *config) void setup_meters(Configuration *config, MeterManager *manager) { - for (auto &m : config->meters) + for (MeterInfo &m : config->meters) { m.conversions = config->conversions; + m.extra_calculated_fields = config->extra_calculated_fields; if (m.usesPolling() || driverNeedsPolling(m.driver, m.driver_name)) { diff --git a/src/meters.cc b/src/meters.cc index 4637239..181e5ff 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -738,10 +738,12 @@ MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, { hex2bin(mi.key, &meter_keys_.confidentiality_key); } - for (auto s : mi.shells) { + for (auto s : mi.shells) + { addShell(s); } - for (auto j : mi.extra_constant_fields) { + for (auto j : mi.extra_constant_fields) + { addExtraConstantField(j); } } @@ -765,10 +767,12 @@ MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, { hex2bin(mi.key, &meter_keys_.confidentiality_key); } - for (auto s : mi.shells) { + for (auto s : mi.shells) + { addShell(s); } - for (auto j : mi.extra_constant_fields) { + for (auto j : mi.extra_constant_fields) + { addExtraConstantField(j); } @@ -794,6 +798,39 @@ void MeterCommonImplementation::addExtraConstantField(string ecf) extra_constant_fields_.push_back(ecf); } +void MeterCommonImplementation::addExtraCalculatedField(string ecf) +{ + verbose("(meter) Adding calculated field: %s\n", ecf.c_str()); + + vector parts = splitString(ecf, '='); + + if (parts.size() != 2) + { + warning("Invalid formula for calculated field. %s\n", ecf.c_str()); + return; + } + + string vname; + Unit unit; + + bool ok = extractUnit(parts[0], &vname, &unit); + if (!ok) + { + warning("Could not extract a valid unit from calculated field name %s\n", parts[0].c_str()); + return; + } + + Quantity quantity = toQuantity(unit); + + addNumericFieldWithCalculator( + vname, + "Calculated: "+ecf, + PrintProperty::JSON | PrintProperty::FIELD, + quantity, + parts[1] + ); +} + vector &MeterCommonImplementation::shellCmdlines() { return shell_cmdlines_; @@ -965,6 +1002,15 @@ void MeterCommonImplementation::addNumericFieldWithCalculator(string vname, { Formula *f = newFormula(); bool ok = f->parse(this, formula); + if (!ok) + { + string err = f->errors(); + warning("Warning! Ignoring calculated field %s because parse failed:\n%s", + vname.c_str(), + err.c_str()); + delete f; + return; + } assert(ok); field_infos_.push_back( @@ -2417,6 +2463,10 @@ shared_ptr createMeter(MeterInfo *mi) { shared_ptr newm = di->construct(*mi); newm->addConversions(mi->conversions); + for (string &j : mi->extra_calculated_fields) + { + newm->addExtraCalculatedField(j); + } newm->setPollInterval(mi->poll_interval); if (mi->selected_fields.size() > 0) { diff --git a/src/meters.h b/src/meters.h index fb1aece..7b09d26 100644 --- a/src/meters.h +++ b/src/meters.h @@ -159,6 +159,7 @@ struct MeterInfo int bps {}; // For mbus communication you need to know the baud rate. vector shells; vector extra_constant_fields; // Additional static fields that are added to each message. + vector extra_calculated_fields; // Additional field calculated using formulas. vector conversions; // Additional units desired in json. vector selected_fields; // Usually set to the default fields, but can be override in meter config. @@ -172,7 +173,7 @@ struct MeterInfo string str(); DriverName driverName(); - MeterInfo(string b, string n, MeterDriver d, string e, vector i, string k, LinkModeSet lms, int baud, vector &s, vector &j) + MeterInfo(string b, string n, MeterDriver d, string e, vector i, string k, LinkModeSet lms, int baud, vector &s, vector &j, vector &calcfs) { bus = b; name = n; @@ -183,6 +184,7 @@ struct MeterInfo key = k; shells = s; extra_constant_fields = j; + extra_calculated_fields = calcfs; link_modes = lms; bps = baud; } @@ -197,6 +199,7 @@ struct MeterInfo key = ""; shells.clear(); extra_constant_fields.clear(); + extra_calculated_fields.clear(); link_modes.clear(); bps = 0; } @@ -471,6 +474,7 @@ struct Meter virtual MeterKeys *meterKeys() = 0; virtual void addConversions(std::vector cs) = 0; + virtual void addExtraCalculatedField(std::string ecf) = 0; virtual vector& conversions() = 0; virtual void addShell(std::string cmdline) = 0; virtual vector &shellCmdlines() = 0; diff --git a/src/meters_common_implementation.h b/src/meters_common_implementation.h index 1c23995..247f521 100644 --- a/src/meters_common_implementation.h +++ b/src/meters_common_implementation.h @@ -67,6 +67,7 @@ struct MeterCommonImplementation : public virtual Meter void setPollInterval(time_t interval); time_t pollInterval(); bool usesPolling(); + void addExtraCalculatedField(std::string ef); void onUpdate(function cb); int numUpdates(); diff --git a/test.sh b/test.sh index ef48259..d0a1a35 100755 --- a/test.sh +++ b/test.sh @@ -84,6 +84,9 @@ if [ "$?" != "0" ]; then RC="1"; fi tests/test_conversions.sh $PROG if [ "$?" != "0" ]; then RC="1"; fi +tests/test_formulas.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + tests/test_fields.sh $PROG if [ "$?" != "0" ]; then RC="1"; fi diff --git a/tests/test_formulas.sh b/tests/test_formulas.sh new file mode 100755 index 0000000..ba9ebd9 --- /dev/null +++ b/tests/test_formulas.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +PROG="$1" + +rm -rf testoutput +mkdir -p testoutput +TEST=testoutput + +TESTNAME="Test formulas" +TESTRESULT="ERROR" + +$PROG --format=json \ + --calculate_sumtemp_c='external_temperature_c+flow_temperature_c' \ + --calculate_addtemp_c='external_temperature_c + 1100 c' \ + 23442D2C998734761B168D2087D19EAD217F1779EDA86AB6_710008190000081900007F13 \ + MyTapWater multical21 76348799 "" \ + > $TEST/test_output.txt + +cat > $TEST/test_expected.txt < $TEST/test_responses.txt + diff $TEST/test_expected.txt $TEST/test_responses.txt + if [ "$?" = "0" ] + then + echo "OK: $TESTNAME" + TESTRESULT="OK" + fi +fi + +if [ "$TESTRESULT" = "ERROR" ]; then echo ERROR: $TESTNAME; exit 1; fi