diff --git a/Makefile b/Makefile index 90b0778..f69b88b 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ PROG_OBJS:=\ $(BUILD)/cmdline.o \ $(BUILD)/config.o \ $(BUILD)/dvparser.o \ + $(BUILD)/formula.o \ $(BUILD)/mbus_rawtty.o \ $(BUILD)/meters.o \ $(BUILD)/manufacturer_specificities.o \ @@ -289,6 +290,9 @@ testd: testdriver: @./tests/test_drivers.sh build/wmbusmeters driver_${DRIVER}.cc +testdriverd: + @./tests/test_drivers.sh build_debug/wmbusmeters driver_${DRIVER}.cc + update_manufacturers: iconv -f utf-8 -t ascii//TRANSLIT -c DLMS_Flagids.csv -o tmp.flags cat tmp.flags | grep -v ^# | cut -f 1 > list.flags diff --git a/simulations/simulation_t1.txt b/simulations/simulation_t1.txt index 0d8d829..7df3a6d 100644 --- a/simulations/simulation_t1.txt +++ b/simulations/simulation_t1.txt @@ -157,8 +157,8 @@ telegram=|5E4409077372727210077A710050052F2F_046D0110A92704130000000004933B00000 # Test electricity meter with eBZ wMB E01. telegram=|5B445A149922992202378C20F6900F002C25BC9E0000BF48954821BC508D72992299225A140102F6003007102F2F040330F92A0004A9FF01FF24000004A9FF026A29000004A9FF03460600000DFD11063132333435362F2F2F2F2F2F| -{"media":"electricity","meter":"ebzwmbe","name":"Elen1","id":"22992299","total_energy_consumption_kwh":2816.304,"current_power_consumption_kw":0.21679,"current_power_consumption_phase1_kw":0.09471,"current_power_consumption_phase2_kw":0.10602,"current_power_consumption_phase3_kw":0.01606,"customer":"123456","timestamp":"1111-11-11T11:11:11Z"} -|Elen1;22992299;2816.304000;0.216790;0.094710;0.106020;0.016060;1111-11-11 11:11.11 +{"media":"electricity","meter":"ebzwmbe","name":"Elen1","id":"22992299","total_energy_consumption_kwh":2816.304,"current_power_consumption_phase1_kw":0.09471,"current_power_consumption_phase2_kw":0.10602,"current_power_consumption_phase3_kw":0.01606,"customer":"654321","current_power_consumption_kw":0.21679,"timestamp":"1111-11-11T11:11:11Z"} +|Elen1;22992299;2816.304;0.21679;0.09471;0.10602;0.01606;1111-11-11 11:11.11 # Test electricity meter with ESYS-WM20 diff --git a/src/driver_ebzwmbe.cc b/src/driver_ebzwmbe.cc new file mode 100644 index 0000000..75523ef --- /dev/null +++ b/src/driver_ebzwmbe.cc @@ -0,0 +1,102 @@ +/* + Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later) + + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include"meters_common_implementation.h" + +namespace +{ + struct Driver : public virtual MeterCommonImplementation + { + Driver(MeterInfo &mi, DriverInfo &di); + }; + + static bool ok = registerDriver([](DriverInfo&di) + { + di.setName("ebzwmbe"); + di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,current_power_consumption_phase1_kw,current_power_consumption_phase2_kw,current_power_consumption_phase3_kw,timestamp"); + di.setMeterType(MeterType::ElectricityMeter); + di.addLinkMode(LinkMode::T1); + di.addDetection(MANUFACTURER_EBZ, 0x02, 0x01); + di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); }); + }); + + Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di) + { + addNumericFieldWithExtractor( + "total_energy_consumption", + "The total energy consumption recorded by this meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Energy, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::EnergyWh) + ); + + addNumericFieldWithExtractor( + "current_power_consumption_phase1", + "Current power consumption at phase 1.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Power, + VifScaling::Auto, + FieldMatcher::build() + .set(DifVifKey("04A9FF01")) + ); + + addNumericFieldWithExtractor( + "current_power_consumption_phase2", + "Current power consumption at phase 2.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Power, + VifScaling::Auto, + FieldMatcher::build() + .set(DifVifKey("04A9FF02")) + ); + + addNumericFieldWithExtractor( + "current_power_consumption_phase3", + "Current power consumption at phase 3.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Power, + VifScaling::Auto, + FieldMatcher::build() + .set(DifVifKey("04A9FF03")) + ); + + addStringFieldWithExtractor( + "customer", + "Customer name.", + PrintProperty::JSON, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Customer) + ); + + addNumericFieldWithCalculator( + "current_power_consumption", + "Calculated sum of power consumption of all phases.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Power, + "current_power_consumption_phase1_kw + current_power_consumption_phase2_kw + current_power_consumption_phase3_kw" + ); + } +} + +// Test: Elen1 ebzwmbe 22992299 NOKEY +// telegram=|5B445A149922992202378C20F6900F002C25BC9E0000BF48954821BC508D72992299225A140102F6003007102F2F040330F92A0004A9FF01FF24000004A9FF026A29000004A9FF03460600000DFD11063132333435362F2F2F2F2F2F| +// {"media":"electricity","meter":"ebzwmbe","name":"Elen1","id":"22992299","total_energy_consumption_kwh":2816.304,"current_power_consumption_phase1_kw":0.09471,"current_power_consumption_phase2_kw":0.10602,"current_power_consumption_phase3_kw":0.01606,"customer":"654321","current_power_consumption_kw":0.21679,"timestamp":"1111-11-11T11:11:11Z"} +// |Elen1;22992299;2816.304;0.21679;0.09471;0.10602;0.01606;1111-11-11 11:11.11 diff --git a/src/driver_hydroclima.cc b/src/driver_hydroclima.cc index 8ec2059..356f744 100644 --- a/src/driver_hydroclima.cc +++ b/src/driver_hydroclima.cc @@ -149,14 +149,14 @@ namespace if (i+1 >= len) return; average_ambient_temperature_ = toTemperature(bytes[i+1], bytes[i]); - info = renderJsonOnlyDefaultUnit("average_ambient_temperature"); + info = renderJsonOnlyDefaultUnit("average_ambient_temperature", Quantity::Temperature); t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL, "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str()); i+=2; if (i+1 >= len) return; max_ambient_temperature_ = toTemperature(bytes[i+1], bytes[i]); - info = renderJsonOnlyDefaultUnit("max_ambient_temperature"); + info = renderJsonOnlyDefaultUnit("max_ambient_temperature", Quantity::Temperature); t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL, "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str()); i+=2; @@ -175,14 +175,14 @@ namespace if (i+1 >= len) return; average_ambient_temperature_last_month_ = toTemperature(bytes[i+1], bytes[i]); - info = renderJsonOnlyDefaultUnit("average_ambient_temperature_last_month"); + info = renderJsonOnlyDefaultUnit("average_ambient_temperature_last_month", Quantity::Temperature); t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL, "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str()); i+=2; if (i+1 >= len) return; average_heater_temperature_last_month_ = toTemperature(bytes[i+1], bytes[i]); - info = renderJsonOnlyDefaultUnit("average_heater_temperature_last_month"); + info = renderJsonOnlyDefaultUnit("average_heater_temperature_last_month", Quantity::Temperature); t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL, "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str()); i+=2; diff --git a/src/driver_lansendw.cc b/src/driver_lansendw.cc index c4c2962..88c5d8f 100644 --- a/src/driver_lansendw.cc +++ b/src/driver_lansendw.cc @@ -131,17 +131,17 @@ void MeterLansenDW::processContent(Telegram *t) if (extractDVuint16(&t->dv_entries, "02FD1B", &offset, &info_codes_)) { - t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("status")); + t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("status", Quantity::Text)); } if (extractDVdouble(&t->dv_entries, "0EFD3A", &offset, &pulse_counter_a_, false)) { - t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("counter_a")); + t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("counter_a", Quantity::Counter)); } if (extractDVdouble(&t->dv_entries, "8E40FD3A", &offset, &pulse_counter_b_, false)) { - t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("counter_b")); + t->addMoreExplanation(offset, renderJsonOnlyDefaultUnit("counter_b", Quantity::Counter)); } } diff --git a/src/dvparser.cc b/src/dvparser.cc index 4568a61..1a787d7 100644 --- a/src/dvparser.cc +++ b/src/dvparser.cc @@ -427,6 +427,8 @@ bool parseDV(Telegram *t, if (variable_length) { DEBUG_PARSER("(dvparser debug) varlen %02x\n", *(data+0)); datalen = *(data); + t->addExplanationAndIncrementPos(data, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X varlen=%d", *(data+0), datalen); + remaining--; // Drop the length byte. } DEBUG_PARSER("(dvparser debug) remaining data %d len=%d\n", remaining, datalen); if (remaining < datalen) @@ -435,10 +437,6 @@ bool parseDV(Telegram *t, datalen = remaining-1; } - // Skip the length byte in the variable length data. - if (variable_length) { - t->addExplanationAndIncrementPos(data, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X varlen=%d", *(data+0), datalen); - } string value = bin2hex(data, data_end, datalen); int offset = start_parse_here+data-data_start; @@ -454,10 +452,10 @@ bool parseDV(Telegram *t, DVEntry *dve = &(*dv_entries)[key].second; - /*if (isDebugEnabled()) + if (isTraceEnabled()) { - debug("(dvparser) entry %s\n", dve->str().c_str()); - }*/ + trace("[DVPARSER] entry %s\n", dve->str().c_str()); + } assert(key == dve->dif_vif_key.str()); diff --git a/src/dvparser.h b/src/dvparser.h index a5072d5..6ebc565 100644 --- a/src/dvparser.h +++ b/src/dvparser.h @@ -52,6 +52,7 @@ X(ParameterSet,0x7D0B,0x7D0B, Quantity::Text, Unit::TXT) \ X(ModelVersion,0x7D0C,0x7D0C, Quantity::Text, Unit::TXT) \ X(SoftwareVersion,0x7D0F,0x7D0F, Quantity::Text, Unit::TXT) \ + X(Customer,0x7D11,0x7D11, Quantity::Text, Unit::TXT) \ X(ErrorFlags,0x7D17,0x7D17, Quantity::Text, Unit::TXT) \ X(DigitalInput,0x7D1B,0x7D1B, Quantity::Text, Unit::TXT) \ X(DurationOfTariff,0x7D31,0x7D33, Quantity::Time, Unit::Hour) \ diff --git a/src/formula.cc b/src/formula.cc new file mode 100644 index 0000000..16fbc7a --- /dev/null +++ b/src/formula.cc @@ -0,0 +1,562 @@ +/* + Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later) + + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include"formula.h" +#include"formula_implementation.h" +#include"meters.h" +#include"units.h" + +#include +#include + +NumericFormula::~NumericFormula() +{ +} + +NumericFormulaConstant::~NumericFormulaConstant() +{ +} + +NumericFormulaField::~NumericFormulaField() +{ +} + +NumericFormulaAddition::~NumericFormulaAddition() +{ +} + +double NumericFormulaConstant::calculate(Unit to) +{ + return convert(constant_, unit(), to); +} + +double NumericFormulaField::calculate(Unit to) +{ + return meter_->getNumericValue(field_info_, to); +} + +double NumericFormulaAddition::calculate(Unit to) +{ + double sum = 0; + + sum += left_->calculate(to); + sum += right_->calculate(to); + + return sum; +} + +const char *toString(TokenType tt) +{ + switch (tt) { + case TokenType::SPACE: return "SPACE"; + case TokenType::NUMBER: return "NUMBER"; + case TokenType::LPAR: return "LPAR"; + case TokenType::RPAR: return "RPAR"; + case TokenType::PLUS: return "PLUS"; + case TokenType::UNIT: return "UNIT"; + case TokenType::FIELD: return "FIELD"; + } + return "?"; +} + +string Token::str(const string &s) +{ + string v = s.substr(start, len); + return tostrprintf("%s(%s)", toString(type), v.c_str()); +} + +double Token::val(const string &s) +{ + string v = s.substr(start, len); + return atof(v.c_str()); +} + +string Token::vals(const string &s) +{ + return s.substr(start, len); +} + +Unit Token::unit(const string &s) +{ + string v = s.substr(start, len); + return toUnit(v); +} + +void FormulaImplementation::clear() +{ + valid_ = true; + while (!ops_.empty()) ops_.pop(); + tokens_.clear(); + formula_ = ""; + meter_ = NULL; +} + +size_t FormulaImplementation::findSpace(size_t i) +{ + size_t len = 0; + while(isspace(formula_[i])) + { + i++; + len++; + } + return len; +} + +size_t FormulaImplementation::findNumber(size_t i) +{ + size_t len = 0; + size_t start = i; + int num_dots = 0; + while (true) + { + if (i >= formula_.length()) break; + char c = formula_[i]; + if ((c < '0' || c > '9') && c != '.') break; + if (c == '.') + { + if (start == i) return 0; // Numbers do not start with a dot. + num_dots++; + } + i++; + len++; + } + + if (num_dots > 1) return 0; // More than one decimal dot is an error. + + return len; +} + +size_t FormulaImplementation::findPlus(size_t i) +{ + if (i >= formula_.length()) return 0; + + char c = formula_[i]; + if (c == '+') return 1; + + return 0; +} + +size_t FormulaImplementation::findLPar(size_t i) +{ + if (i >= formula_.length()) return 0; + + char c = formula_[i]; + if (c == '(') return 1; + + return 0; +} + +size_t FormulaImplementation::findRPar(size_t i) +{ + if (i >= formula_.length()) return 0; + + char c = formula_[i]; + if (c == ')') return 1; + + return 0; +} + +bool is_letter(char c) +{ + return c >= 'a' && c <= 'z'; +} + +bool is_letter_or_underscore(char c) +{ + return c == '_' || (c >= 'a' && c <= 'z'); +} + +bool is_letter_digit_or_underscore(char c) +{ + return c == '_' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); +} + +size_t FormulaImplementation::findUnit(size_t i) +{ + if (i >= formula_.length()) return 0; + + size_t len = formula_.length(); + char c = formula_[i]; + + // All units start with a lower case a-z, followed by more letters and _ underscores. + if (!is_letter(c)) return 0; + +#define X(cname,lcname,hrname,quantity,explanation) \ + if ( (i+sizeof(#lcname)-1 <= len) && \ + !is_letter_or_underscore(formula_[i+sizeof(#lcname)-1]) && \ + !strncmp(#lcname, formula_.c_str()+i, sizeof(#lcname)-1)) return sizeof(#lcname)-1; +LIST_OF_UNITS +#undef X + + return 0; +} + +size_t FormulaImplementation::findField(size_t i) +{ + size_t len = 0; + size_t start = i; + + // All field names are lower case a-z. + if (!is_letter(formula_[start])) return 0; + + while (true) + { + if (i >= formula_.length()) break; + char c = formula_[i]; + // After first letter field names can contain more letters digits and underscores. + if (!is_letter_digit_or_underscore(c)) break; + i++; + len++; + } + + return len; +} + +bool FormulaImplementation::tokenize() +{ + size_t i = 0; + + while (i 0) { i+=len; continue; } // No token added for whitespace. + + len = findNumber(i); + if (len > 0) { tokens_.push_back(Token(TokenType::NUMBER, i, len)); i+=len; continue; } + + len = findLPar(i); + if (len > 0) { tokens_.push_back(Token(TokenType::LPAR, i, len)); i+=len; continue; } + + len = findRPar(i); + if (len > 0) { tokens_.push_back(Token(TokenType::RPAR, i, len)); i+=len; continue; } + + len = findPlus(i); + if (len > 0) { tokens_.push_back(Token(TokenType::PLUS, i, len)); i+=len; continue; } + + len = findUnit(i); + if (len > 0) { tokens_.push_back(Token(TokenType::UNIT, i, len)); i+=len; continue; } + + len = findField(i); + if (len > 0) { tokens_.push_back(Token(TokenType::FIELD, i, len)); i+=len; continue; } + + break; + } + + // Interrupted early, thus there was an error tokenizing. + if (i < formula_.length()) return false; + + return true; +} + +size_t FormulaImplementation::parseOps(size_t i) +{ + Token *tok = LA(i); + Token *next= LA(i+1); + if (tok == NULL) return i; + + if (tok->type == TokenType::FIELD) + { + handleField(tok); + return i+1; + } + + if (tok->type == TokenType::PLUS) + { + size_t next = parseOps(i+1); + handleAddition(); + return next; + } + + if (tok->type == TokenType::LPAR) + { + return parsePar(i); + } + + if (tok->type == TokenType::RPAR) + { + return i; + } + + if (next == NULL) return i; + + if (tok->type == TokenType::NUMBER && next->type == TokenType::UNIT) + { + handleConstant(tok, next); + return i+2; + } + + return i; +} + +size_t FormulaImplementation::parsePar(size_t i) +{ + Token *tok = LA(i); + assert(tok->type == TokenType::LPAR); + + i++; + for (;;) + { + tok = LA(i); + if (tok == NULL) break; + if (tok->type == TokenType::RPAR) break; + size_t next = parseOps(i); + if (next == i) break; + i = next; + } + + if (tok == NULL) + { + warning("No closing parenthesis found!\n"); + return i; + } + + if (tok->type != TokenType::RPAR) + { + warning("Expected parenthesis but got xx intead.\n"); + return i; + } + return i+1; +} + +void FormulaImplementation::handleConstant(Token *number, Token *unit) +{ + double c = number->val(formula_); + Unit u = unit->unit(formula_); + + if (u == Unit::Unknown) + { + warning("Unknown unit \"%s\" in formula:\n%s\n", unit->vals(formula_).c_str(), formula_.c_str()); + return; + } + //debug("(formula) push constant %f %s\n", c, unitToStringLowerCase(u).c_str()); + doConstant(u, c); +} + +void FormulaImplementation::handleAddition() +{ + //debug("(formula) push addition\n"); + doAddition(); +} + +void FormulaImplementation::handleField(Token *field) +{ + string field_name = field->vals(formula_); // Full field: total_m3 + //debug("(formula) push field %s\n", field_name.c_str()); + string vname; // Without unit: total + Unit unit; + bool ok = extractUnit(field_name, &vname, &unit); + + debug("(formula) handle field %s into %s %s\n", field_name.c_str(), vname.c_str(), unitToStringLowerCase(unit).c_str()); + + if (!ok) + { + warning("Could not extract a valid unit from field name \"%s\"\n", field_name.c_str()); + return; + } + + Quantity q = toQuantity(unit); + FieldInfo *f = meter_->findFieldInfo(vname, q); + + if (f == NULL) + { + warning("No such field found \"%s\" (%s %s)\n", field_name.c_str(), vname.c_str(), toString(q)); + return; + } + + ok = doField(unit, meter_, f); + if (!ok) + { + warning("Could not use field \"%s\" (%s %s)\n", field_name.c_str(), vname.c_str(), toString(q)); + return; + } +} + +bool FormulaImplementation::go() +{ + size_t i = 0; + + for (;;) + { + size_t next = parseOps(i); + if (next == i) break; + i = next; + } + + return i == tokens_.size(); +} + +Token *FormulaImplementation::LA(size_t i) +{ + if (i < 0 || i >= tokens_.size()) return NULL; + return &tokens_[i]; +} + +bool FormulaImplementation::parse(Meter *m, const string &f) +{ + bool ok; + + meter_ = m; + formula_ = f; + + debug("(formula) parsing \"%s\"\n", formula_.c_str()); + + ok = tokenize(); + if (!ok) return false; + + if (isDebugEnabled()) + { + debug("(formula) tokens: "); + for (Token &t : tokens_) + { + debug("%s ", t.str(formula_).c_str()); + } + debug("\n"); + } + + ok = go(); + if (!ok) return false; + + if (isDebugEnabled()) + { + debug("(formula) %s\n", tree().c_str()); + } + return true; +} + +bool FormulaImplementation::valid() +{ + return valid_ == true && ops_.size() == 1; +} + +double FormulaImplementation::calculate(Unit to) +{ + if (!valid_) + { + warning("(formula) not valid returning nan!\n"); + return std::nan(""); + } + if (ops_.size() != 1) + { + warning("(formula) does not have a single op (has %zu), not valid returning nan!\n", ops_.size()); + return std::nan(""); + } + + return ops_.top().get()->calculate(to); +} + +bool FormulaImplementation::doConstant(Unit u, double c) +{ + ops_.push(unique_ptr(new NumericFormulaConstant(u, c))); + return true; +} + +bool FormulaImplementation::doAddition() +{ + if (ops_.size() < 2) { valid_ = false; return false;} + + Unit right_unit = ops_.top()->unit(); + + unique_ptr right_node = std::move(ops_.top()); + ops_.pop(); + + Unit left_unit = ops_.top()->unit(); + + unique_ptr left_node = std::move(ops_.top()); + ops_.pop(); + + ops_.push(unique_ptr(new NumericFormulaAddition(left_unit, left_node, right_node))); + + if (!canConvert(left_unit, right_unit)) + { + valid_ = false; + return false; + } + + return true; +} + +bool FormulaImplementation::doField(Unit u, Meter *m, FieldInfo *fi) +{ + if (!canConvert(u, fi->defaultUnit())) + { + valid_ = false; + return false; + } + ops_.push(unique_ptr(new NumericFormulaField(u, m, fi))); + return true; +} + +Formula *newFormula() +{ + return new FormulaImplementation(); +} + +Formula::~Formula() +{ +} + +FormulaImplementation::~FormulaImplementation() +{ +} + +string FormulaImplementation::str() +{ + return ops_.top()->str(); +} + +string FormulaImplementation::tree() +{ + string t = ops_.top()->tree(); + if (t.back() == ' ') t.pop_back(); + return t; +} + +string NumericFormulaConstant::str() +{ + return tostrprintf("%.15g %s", constant_, unit()); +} + +string NumericFormulaConstant::tree() +{ + return tostrprintf(" ", constant_, unitToStringLowerCase(unit()).c_str()); +} + +string NumericFormulaAddition::str() +{ + string left = left_->tree(); + string right = right_->tree(); + return left+" + "+right; +} + +string NumericFormulaAddition::tree() +{ + string left = left_->tree(); + string right = right_->tree(); + return " "; +} + +string NumericFormulaField::str() +{ + return field_info_->vname()+"_"+unitToStringLowerCase(field_info_->defaultUnit()); +} + +string NumericFormulaField::tree() +{ + return "vname()+"_"+unitToStringLowerCase(field_info_->defaultUnit())+"> "; +} diff --git a/src/formula.h b/src/formula.h new file mode 100644 index 0000000..f8c85c3 --- /dev/null +++ b/src/formula.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later) + + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef FORMULA_H +#define FORMULA_H + +#include +#include + +#include"units.h" + +struct Meter; +struct FieldInfo; + +struct Formula +{ + virtual bool parse(Meter *m, const std::string &f) = 0; + virtual bool valid() = 0; + virtual double calculate(Unit to) = 0; + virtual void clear() = 0; + // Return a regenerated formula string. + virtual std::string str() = 0; + // Return the formula in a format where the tree structure is explicit. + virtual std::string tree() = 0; + + virtual ~Formula() = 0; +}; + +Formula *newFormula(); + +#endif diff --git a/src/formula_implementation.h b/src/formula_implementation.h new file mode 100644 index 0000000..251dde0 --- /dev/null +++ b/src/formula_implementation.h @@ -0,0 +1,160 @@ +/* + Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later) + + 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef FORMULA_IMPLEMENTATION +#define FORMULA_IMPLEMENTATION + +#include"formula.h" +#include"meters.h" + +#include + +struct NumericFormula +{ + NumericFormula(Unit u) : unit_(u) { } + Unit unit() { return unit_; } + virtual double calculate(Unit to) = 0; + virtual string str() = 0; + virtual string tree() = 0; + virtual ~NumericFormula() = 0; + + private: + + Unit unit_; +}; + +struct NumericFormulaConstant : public NumericFormula +{ + NumericFormulaConstant(Unit u, double c) : NumericFormula(u), constant_(c) {} + double calculate(Unit to); + string str(); + string tree(); + ~NumericFormulaConstant(); + + private: + + double constant_; +}; + +struct NumericFormulaField : public NumericFormula +{ + NumericFormulaField(Unit u, Meter *m, FieldInfo *fi) : NumericFormula(u), meter_(m), field_info_(fi) {} + double calculate(Unit to); + string str(); + string tree(); + ~NumericFormulaField(); + + private: + + Meter *meter_; + FieldInfo *field_info_; +}; + +struct NumericFormulaAddition : public NumericFormula +{ + NumericFormulaAddition(Unit u, + unique_ptr &a, + unique_ptr &b) + : NumericFormula(u), left_(std::move(a)), right_(std::move(b)) {} + + double calculate(Unit to); + string str(); + string tree(); + + ~NumericFormulaAddition(); + +private: + + std::unique_ptr left_; + std::unique_ptr right_; +}; + +enum class TokenType +{ + SPACE, + LPAR, + RPAR, + NUMBER, + PLUS, + UNIT, + FIELD +}; + +const char *toString(TokenType tt); + +struct Token +{ + Token(TokenType t, size_t s, size_t l) : type(t), start(s), len(l) {} + + TokenType type; + size_t start; + size_t len; + + string str(const string &s); + string vals(const string &s); + double val(const string &s); + Unit unit(const string &s); +}; + +struct FormulaImplementation : public Formula +{ + bool parse(Meter *m, const string &f); + bool valid(); + double calculate(Unit to); + void clear(); + string str(); + string tree(); + + // Pushes a constant on the stack. + bool doConstant(Unit u, double c); + // Pushes a field read on the stack. Returns false if the field is not found in the meter. + bool doField(Unit u, Meter *m, FieldInfo *fi); + // Pops the two top nodes of the stack and pushes an addition (using these members) on the stack. + // The target unit will be the first unit of the two operands. If incompatible units, then it will + // return false. + bool doAddition(); + + ~FormulaImplementation(); + +private: + + bool tokenize(); + bool go(); + size_t findSpace(size_t i); + size_t findNumber(size_t i); + size_t findUnit(size_t i); + size_t findPlus(size_t i); + size_t findLPar(size_t i); + size_t findRPar(size_t i); + size_t findField(size_t i); + + Token *LA(size_t i); + size_t parseOps(size_t i); + size_t parsePar(size_t i); + + void handleConstant(Token *number, Token *unit); + void handleAddition(); + void handleField(Token *field); + + bool valid_ = true; + std::stack> ops_; + std::vector tokens_; + std::string formula_; // To be parsed. + Meter *meter_; // To be referenced when parsing. +}; + +#endif diff --git a/src/meter_detection.h b/src/meter_detection.h index de2768b..1f0a706 100644 --- a/src/meter_detection.h +++ b/src/meter_detection.h @@ -28,7 +28,6 @@ // #define METER_DETECTION \ X(CCx01, MANUFACTURER_GSS, 0x02, 0x01) \ - X(EBZWMBE, MANUFACTURER_EBZ, 0x37, 0x02) \ X(EURISII, MANUFACTURER_INE, 0x08, 0x55) \ X(EHZP, MANUFACTURER_EMH, 0x02, 0x02) \ X(ESYSWM, MANUFACTURER_ESY, 0x37, 0x30) \ diff --git a/src/meter_ebzwmbe.cc b/src/meter_ebzwmbe.cc deleted file mode 100644 index e1718a9..0000000 --- a/src/meter_ebzwmbe.cc +++ /dev/null @@ -1,180 +0,0 @@ -/* - Copyright (C) 2020 Fredrik Öhrström (gpl-3.0-or-later) - - 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 - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include"dvparser.h" -#include"meters.h" -#include"meters_common_implementation.h" -#include"wmbus.h" -#include"wmbus_utils.h" -#include"util.h" - -struct MeterEBZWMBE : public virtual MeterCommonImplementation -{ - MeterEBZWMBE(MeterInfo &mi); - - double totalEnergyConsumption(Unit u); - double currentPowerConsumption(Unit u); - double currentPowerConsumptionPhase1(Unit u); - double currentPowerConsumptionPhase2(Unit u); - double currentPowerConsumptionPhase3(Unit u); - -private: - - void processContent(Telegram *t); - - double total_energy_kwh_ {}; - double current_power_kw_ {}; - double current_power_phase1_kw_ {}; - double current_power_phase2_kw_ {}; - double current_power_phase3_kw_ {}; - string customer_; -}; - -MeterEBZWMBE::MeterEBZWMBE(MeterInfo &mi) : - MeterCommonImplementation(mi, "ebzwmbe") -{ - setMeterType(MeterType::ElectricityMeter); - - setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_NO_IV); - - // The eBZ wWMB E01 is an addons to the electricity meters - // media 0x37 Radio converter (meter side) - - addLinkMode(LinkMode::T1); - - addPrint("total_energy_consumption", Quantity::Energy, - [&](Unit u){ return totalEnergyConsumption(u); }, - "The total energy consumption recorded by this meter.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("current_power_consumption", Quantity::Power, - [&](Unit u){ return currentPowerConsumption(u); }, - "Current power consumption.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("current_power_consumption_phase1", Quantity::Power, - [&](Unit u){ return currentPowerConsumptionPhase1(u); }, - "Current power consumption phase 1.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("current_power_consumption_phase2", Quantity::Power, - [&](Unit u){ return currentPowerConsumptionPhase2(u); }, - "Current power consumption phase 2.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("current_power_consumption_phase3", Quantity::Power, - [&](Unit u){ return currentPowerConsumptionPhase3(u); }, - "Current power consumption phase 3.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("customer", Quantity::Text, - [&](){ return customer_; }, - "Customer name.", - PrintProperty::JSON); -} - -shared_ptr createEBZWMBE(MeterInfo &mi) -{ - return shared_ptr(new MeterEBZWMBE(mi)); -} - -double MeterEBZWMBE::totalEnergyConsumption(Unit u) -{ - assertQuantity(u, Quantity::Energy); - return convert(total_energy_kwh_, Unit::KWH, u); -} - -double MeterEBZWMBE::currentPowerConsumption(Unit u) -{ - assertQuantity(u, Quantity::Power); - return convert(current_power_kw_, Unit::KW, u); -} - -double MeterEBZWMBE::currentPowerConsumptionPhase1(Unit u) -{ - assertQuantity(u, Quantity::Power); - return convert(current_power_phase1_kw_, Unit::KW, u); -} - -double MeterEBZWMBE::currentPowerConsumptionPhase2(Unit u) -{ - assertQuantity(u, Quantity::Power); - return convert(current_power_phase2_kw_, Unit::KW, u); -} - -double MeterEBZWMBE::currentPowerConsumptionPhase3(Unit u) -{ - assertQuantity(u, Quantity::Power); - return convert(current_power_phase3_kw_, Unit::KW, u); -} - -void MeterEBZWMBE::processContent(Telegram *t) -{ - /* - (ebzwmbe) 2e: 04 dif (32 Bit Integer/Binary Instantaneous value) - (ebzwmbe) 2f: 03 vif (Energy Wh) - (ebzwmbe) 30: * 30F92A00 total energy (2816.304000 kwh) - (ebzwmbe) 34: 04 dif (32 Bit Integer/Binary Instantaneous value) - (ebzwmbe) 35: A9 vif (Power 10⁻² W) - (ebzwmbe) 36: FF vife (additive correction constant: unit of VIF * 10^0) - (ebzwmbe) 37: 01 vife (?) - (ebzwmbe) 38: * FF240000 current power phase 1 (0.094710 kwh) - (ebzwmbe) 3c: 04 dif (32 Bit Integer/Binary Instantaneous value) - (ebzwmbe) 3d: A9 vif (Power 10⁻² W) - (ebzwmbe) 3e: FF vife (additive correction constant: unit of VIF * 10^0) - (ebzwmbe) 3f: 02 vife (?) - (ebzwmbe) 40: * 6A290000 current power phase 2 (0.000000 kwh) - (ebzwmbe) 44: 04 dif (32 Bit Integer/Binary Instantaneous value) - (ebzwmbe) 45: A9 vif (Power 10⁻² W) - (ebzwmbe) 46: FF vife (additive correction constant: unit of VIF * 10^0) - (ebzwmbe) 47: 03 vife (?) - (ebzwmbe) 48: * 46060000 current power phase 3 (0.000000 kwh) - (ebzwmbe) 4c: 0D dif (variable length Instantaneous value) - (ebzwmbe) 4d: FD vif (Second extension of VIF-codes) - (ebzwmbe) 4e: 11 vife (Customer) - (ebzwmbe) 4f: 06 varlen=6 - (ebzwmbe) 50: * 313233343536 customer (123456) - */ - int offset; - string key; - - if (findKey(MeasurementType::Instantaneous, VIFRange::EnergyWh, 0, 0, &key, &t->dv_entries)) { - extractDVdouble(&t->dv_entries, key, &offset, &total_energy_kwh_); - t->addMoreExplanation(offset, " total energy (%f kwh)", total_energy_kwh_); - } - - extractDVdouble(&t->dv_entries, "04A9FF01", &offset, ¤t_power_phase1_kw_); - t->addMoreExplanation(offset, " current power phase 1 (%f kwh)", current_power_phase1_kw_); - - extractDVdouble(&t->dv_entries, "04A9FF02", &offset, ¤t_power_phase2_kw_); - t->addMoreExplanation(offset, " current power phase 2 (%f kwh)", current_power_phase2_kw_); - - extractDVdouble(&t->dv_entries, "04A9FF03", &offset, ¤t_power_phase3_kw_); - t->addMoreExplanation(offset, " current power phase 3 (%f kwh)", current_power_phase3_kw_); - - current_power_kw_ = current_power_phase1_kw_ + current_power_phase2_kw_ + current_power_phase3_kw_; - t->addMoreExplanation(offset, " current power (%f kw)", current_power_kw_); - - string tmp; - extractDVHexString(&t->dv_entries, "0DFD11", &offset, &tmp); - if (tmp.length() > 0) { - vector bin; - hex2bin(tmp, &bin); - customer_ = safeString(bin); - } - t->addMoreExplanation(offset, " customer (%s)", customer_.c_str()); -} diff --git a/src/meters.cc b/src/meters.cc index 6fd2429..82b94fc 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -846,7 +846,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, NULL, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -866,7 +867,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, Unit NULL, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -887,7 +889,8 @@ void MeterCommonImplementation::addPrint(string vname, Quantity vquantity, getValueFunc, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -919,7 +922,8 @@ void MeterCommonImplementation::addNumericFieldWithExtractor( NULL, setValueFunc, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -944,7 +948,37 @@ void MeterCommonImplementation::addNumericFieldWithExtractor(string vname, NULL, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ + )); +} + +void MeterCommonImplementation::addNumericFieldWithCalculator(string vname, + string help, + PrintProperties print_properties, + Quantity vquantity, + string formula, + Unit use_unit) +{ + Formula *f = newFormula(); + bool ok = f->parse(this, formula); + assert(ok); + + field_infos_.push_back( + FieldInfo(field_infos_.size(), + vname, + vquantity, + use_unit == Unit::Unknown ? defaultUnitForQuantity(vquantity) : use_unit, + VifScaling::Auto, + NULL, + help, + print_properties, + NULL, + NULL, + NULL, + NULL, + NoLookup, /* Lookup table */ + f /* Formula */ )); } @@ -969,7 +1003,8 @@ void MeterCommonImplementation::addNumericField( NULL, setValueFunc, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -1000,7 +1035,8 @@ void MeterCommonImplementation::addStringFieldWithExtractor( getValueFunc, NULL, setValueFunc, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -1022,7 +1058,8 @@ void MeterCommonImplementation::addStringFieldWithExtractor(string vname, NULL, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -1054,7 +1091,8 @@ void MeterCommonImplementation::addStringFieldWithExtractorAndLookup( getValueFunc, NULL, setValueFunc, - lookup + lookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -1077,7 +1115,8 @@ void MeterCommonImplementation::addStringFieldWithExtractorAndLookup(string vnam NULL, NULL, NULL, - lookup + lookup, + NULL /* Formula */ )); } @@ -1098,7 +1137,8 @@ void MeterCommonImplementation::addStringField(string vname, NULL, NULL, NULL, - NoLookup + NoLookup, /* Lookup table */ + NULL /* Formula */ )); } @@ -1761,6 +1801,8 @@ bool MeterCommonImplementation::handleTelegram(AboutTelegram &about, vectorrenderJsonOnlyDefaultUnit(this); @@ -1995,7 +2050,7 @@ string FieldInfo::renderJsonText(Meter *m) return renderJson(m, NULL); } -string FieldInfo::generateFieldName(DVEntry *dve) +string FieldInfo::generateFieldName() { if (xuantity_ == Quantity::Text) { @@ -2566,24 +2621,38 @@ bool isValidKey(string& key, MeterDriver mt) return hex2bin(key, &tmp); } - void FieldInfo::performExtraction(Meter *m, Telegram *t, DVEntry *dve) { if (xuantity_ == Quantity::Text) { + // Extract a string. extractString(m, t, dve); } - else + else if (!hasFormula()) { + // Extract a numeric. extractNumeric(m, t, dve); } } +void FieldInfo::performCalculation(Meter *m) +{ + assert(hasFormula()); + + double value = formula_->calculate(defaultUnit()); + m->setNumericValue(this, defaultUnit(), value); +} + bool FieldInfo::hasMatcher() { return matcher_.active == true; } +bool FieldInfo::hasFormula() +{ + return formula_ != NULL; +} + bool FieldInfo::matches(DVEntry *dve) { return matcher_.matches(*dve); @@ -2638,7 +2707,7 @@ bool FieldInfo::extractNumeric(Meter *m, Telegram *t, DVEntry *dve) assert(key == "" || dve->dif_vif_key.str() == key); // Generate the json field name: - string field_name = generateFieldName(dve); + string field_name = generateFieldName(); double extracted_double_value = NAN; if (dve->extractDouble(&extracted_double_value, @@ -2748,7 +2817,7 @@ bool FieldInfo::extractString(Meter *m, Telegram *t, DVEntry *dve) assert(key == "" || dve->dif_vif_key.str() == key); // Generate the json field name: - string field_name = generateFieldName(dve); + string field_name = generateFieldName(); uint64_t extracted_bits {}; if (lookup_.hasLookups() || (print_properties_.hasJOINTPLSTATUS())) @@ -2795,6 +2864,7 @@ bool FieldInfo::extractString(Meter *m, Telegram *t, DVEntry *dve) matcher_.vif_range == VIFRange::FabricationNo || matcher_.vif_range == VIFRange::ModelVersion || matcher_.vif_range == VIFRange::SoftwareVersion || + matcher_.vif_range == VIFRange::Customer || matcher_.vif_range == VIFRange::ParameterSet) { string extracted_id; diff --git a/src/meters.h b/src/meters.h index 8bd7148..9a3476d 100644 --- a/src/meters.h +++ b/src/meters.h @@ -19,6 +19,7 @@ #define METER_H_ #include"dvparser.h" +#include"formula.h" #include"util.h" #include"units.h" #include"translatebits.h" @@ -60,7 +61,6 @@ LIST_OF_METER_TYPES #define LIST_OF_METERS \ X(auto, 0, AutoMeter, AUTO, Auto) \ X(unknown, 0, UnknownMeter, UNKNOWN, Unknown) \ - X(ebzwmbe, T1_bit, ElectricityMeter, EBZWMBE, EBZWMBE) \ X(eurisii, T1_bit, HeatCostAllocationMeter, EURISII, EurisII) \ X(ehzp, T1_bit, ElectricityMeter, EHZP, EHZP) \ X(esyswm, T1_bit, ElectricityMeter, ESYSWM, ESYSWM) \ @@ -328,7 +328,8 @@ struct FieldInfo function get_string_value_override, function set_numeric_value_override, function set_string_value_override, - Translate::Lookup lookup + Translate::Lookup lookup, + Formula *formula ) : index_(index), vname_(vname), @@ -342,7 +343,8 @@ struct FieldInfo get_string_value_override_(get_string_value_override), set_numeric_value_override_(set_numeric_value_override), set_string_value_override_(set_string_value_override), - lookup_(lookup) + lookup_(lookup), + formula_(formula) {} int index() { return index_; } @@ -367,9 +369,12 @@ struct FieldInfo bool extractNumeric(Meter *m, Telegram *t, DVEntry *dve = NULL); bool extractString(Meter *m, Telegram *t, DVEntry *dve = NULL); bool hasMatcher(); + bool hasFormula(); bool matches(DVEntry *dve); void performExtraction(Meter *m, Telegram *t, DVEntry *dve); + void performCalculation(Meter *m); + string renderJsonOnlyDefaultUnit(Meter *m); string renderJson(Meter *m, vector *additional_conversions); string renderJsonText(Meter *m); @@ -377,7 +382,7 @@ struct FieldInfo // A FieldInfo can be declared to handle any number of storage fields of a certain range. // The vname is then a pattern total_at_month_{storagenr-32} that gets translated into // total_at_month_2 (for the dventry with storage nr 34.) - string generateFieldName(DVEntry *dve); + string generateFieldName(); // Check if the meter object stores a value for this field. bool hasValue(Meter *m); @@ -403,7 +408,11 @@ private: function set_numeric_value_override_; // Call back to set the value in the c++ object function set_string_value_override_; // Call back to set the value string in the c++ object + // Lookup bits to strings. Translate::Lookup lookup_; + + // For calculated fields. + unique_ptr formula_; }; struct BusManager; @@ -469,8 +478,8 @@ struct Meter virtual vector &shellCmdlines() = 0; virtual void poll(shared_ptr bus) = 0; - virtual FieldInfo *findFieldInfo(string vname) = 0; - virtual string renderJsonOnlyDefaultUnit(string vname) = 0; + virtual FieldInfo *findFieldInfo(string vname, Quantity xuantity) = 0; + virtual string renderJsonOnlyDefaultUnit(string vname, Quantity xuantity) = 0; virtual ~Meter() = default; }; diff --git a/src/meters_common_implementation.h b/src/meters_common_implementation.h index 4d7d38e..1c23995 100644 --- a/src/meters_common_implementation.h +++ b/src/meters_common_implementation.h @@ -142,7 +142,15 @@ protected: Quantity vquantity, // Value belongs to this quantity, this quantity determines the default unit. VifScaling vif_scaling, // How should any Vif value be scaled. FieldMatcher matcher, - Unit use_unit = Unit::Unknown); // If specified use this unit instead. + Unit use_unit = Unit::Unknown); // If specified use this unit for the json field instead instead of the default unit. + + void addNumericFieldWithCalculator( + string vname, // Name of value without unit, eg "total" "total_month{storagenr}" + string help, // Information about this field. + PrintProperties print_properties, // Should this be printed by default in fields,json and hr. + Quantity vquantity, // Value belongs to this quantity, this quantity determines the default unit. + string formula, // The formula can reference the other fields and + them together. + Unit use_unit = Unit::Unknown); // If specified use this unit for the json field instead instead of the default unit. void addNumericField( string vname, // Name of value without unit, eg total @@ -221,10 +229,12 @@ protected: // since Json is assumed to be decoded by a program and the current timestamp which is the // same as timestamp_utc, can always be decoded/recoded into local time or a unix timestamp. - FieldInfo *findFieldInfo(string vname); - string renderJsonOnlyDefaultUnit(string vname); + FieldInfo *findFieldInfo(string vname, Quantity xuantity); + string renderJsonOnlyDefaultUnit(string vname, Quantity xuantity); void processFieldExtractors(Telegram *t); + void processFieldCalculators(); + virtual void processContent(Telegram *t); void setNumericValue(FieldInfo *fi, Unit u, double v); diff --git a/src/testinternals.cc b/src/testinternals.cc index 051ebc6..f8acf00 100644 --- a/src/testinternals.cc +++ b/src/testinternals.cc @@ -19,6 +19,7 @@ #include"aescmac.h" #include"cmdline.h" #include"config.h" +#include"formula_implementation.h" #include"meters.h" #include"printer.h" #include"serial.h" @@ -33,13 +34,13 @@ using namespace std; int test_crc(); int test_dvparser(); -int test_test(); +int test_devices(); int test_linkmodes(); void test_ids(); void test_addresses(); void test_kdf(); void test_periods(); -void test_devices(); +void test_device_parsing(); void test_meters(); void test_months(); void test_aes(); @@ -52,44 +53,66 @@ void test_ascii_detection(); void test_status_join(); void test_status_sort(); void test_field_matcher(); +void test_units(); +void test_formulas_building(); +void test_formulas_parsing(); + +bool test(const char *test_name, const char *pattern) +{ + if (pattern == NULL) return true; + bool ok = strstr(test_name, pattern) != NULL; + if (ok) printf("Test %s\n", test_name); + return ok; +} int main(int argc, char **argv) { - if (argc > 1) { - if (!strcmp(argv[1], "--debug")) + const char *pattern = NULL; + + int i = 1; + while (i < argc) { + if (!strcmp(argv[i], "--debug")) { debugEnabled(true); } - if (!strcmp(argv[1], "--trace")) + else + if (!strcmp(argv[i], "--trace")) { debugEnabled(true); traceEnabled(true); } + else + { + pattern = argv[i]; + } + i++; } onExit([](){}); - test_crc(); - test_dvparser(); - test_test(); - test_devices(); - test_meters(); - /* - test_linkmodes();*/ - test_ids(); + if (test("crc", pattern)) test_crc(); + if (test("dvparser", pattern)) test_dvparser(); + if (test("devices", pattern)) test_devices(); + if (test("device_parsing", pattern)) test_device_parsing(); + if (test("meters", pattern)) test_meters(); +// test_linkmodes(); + if (test("ids", pattern)) test_ids(); // test_addresses(); - test_kdf(); - test_periods(); - test_months(); - test_aes(); - test_sbc(); - test_hex(); - test_translate(); - test_slip(); - test_dvs(); - test_ascii_detection(); - test_status_join(); - test_status_sort(); - test_field_matcher(); + if (test("kdf", pattern)) test_kdf(); + if (test("periods", pattern)) test_periods(); + if (test("months", pattern)) test_months(); + if (test("aes", pattern)) test_aes(); + if (test("sbc", pattern)) test_sbc(); + if (test("hex", pattern)) test_hex(); + if (test("translate", pattern)) test_translate(); + if (test("slip", pattern)) test_slip(); + if (test("dvs", pattern)) test_dvs(); + if (test("ascii_detection", pattern)) test_ascii_detection(); + if (test("status_join", pattern)) test_status_join(); + if (test("status_sort", pattern)) test_status_sort(); + if (test("field_matcher", pattern)) test_field_matcher(); + if (test("units", pattern)) test_units(); + if (test("formulas_building", pattern)) test_formulas_building(); + if (test("formulas_parsing", pattern)) test_formulas_parsing(); return 0; } @@ -242,7 +265,7 @@ int test_dvparser() return 0; } -int test_test() +int test_devices() { shared_ptr manager = createSerialCommunicationManager(0, false); @@ -663,7 +686,7 @@ void testd(string arg, bool xok, string xalias, string xfile, string xtype, stri } } -void test_devices() +void test_device_parsing() { testd("Bus_4711=/dev/ttyUSB0:im871a[12345678]:9600:868.95M:c1,t1", true, "Bus_4711", // alias @@ -1537,3 +1560,248 @@ void test_field_matcher() } } + +void test_unit(string in, bool expected_ok, string expected_vname, Unit expected_unit) +{ + Unit unit; + string vname; + + bool ok = extractUnit(in, &vname, &unit); + + if (ok != expected_ok || + vname != expected_vname || + unit != expected_unit) + { + printf("ERROR expected ok=%d vname=%s unit=%s but got\n" + " but got ok=%d vname=%s unit=%s\n", + expected_ok, expected_vname.c_str(), unitToStringUpperCase(expected_unit).c_str(), + ok, vname.c_str(), unitToStringUpperCase(unit).c_str()); + } +} + +void test_units() +{ + test_unit("total_kwh", true, "total", Unit::KWH); + test_unit("total_", false, "", Unit::Unknown); + test_unit("total", false, "", Unit::Unknown); + test_unit("", false, "", Unit::Unknown); + test_unit("_c", false, "", Unit::Unknown); + + test_unit("work__c", true, "work_", Unit::C); + + test_unit("water_c", true, "water", Unit::C); + test_unit("walk_counter", true, "walk", Unit::COUNTER); + test_unit("work_kvarh", true, "work", Unit::KVARH); + + test_unit("current_power_consumption_phase1_kw", true, "current_power_consumption_phase1", Unit::KW); + +} + + +void test_formulas_building() +{ + unique_ptr f = unique_ptr(new FormulaImplementation()); + + assert(f->doConstant(Unit::KWH, 17)); + assert(f->doConstant(Unit::KWH, 1)); + assert(f->doAddition()); + double v = f->calculate(Unit::KWH); + if (v != 18.0) + { + printf("ERROR in test formula 1 expected 18.0 but got %lf\n", v); + } + + f->clear(); + assert(f->doConstant(Unit::KWH, 10)); + v = f->calculate(Unit::MJ); + if (v != 36.0) + { + printf("ERROR in test formula 2 expected 36.0 but got %lf\n", v); + } + + f->clear(); + assert(f->doConstant(Unit::GJ, 10)); + assert(f->doConstant(Unit::MJ, 10)); + assert(f->doAddition()); + v = f->calculate(Unit::GJ); + if (v != 10.01) + { + printf("ERROR in test formula 3 expected 10.01 but got %lf\n", v); + } + + f->clear(); + assert(f->doConstant(Unit::C, 10)); + assert(f->doConstant(Unit::C, 20)); + assert(f->doAddition()); + assert(f->doConstant(Unit::C, 22)); + assert(f->doAddition()); + v = f->calculate(Unit::C); + if (v != 52) + { + printf("ERROR in test formula 4 expected 52 but got %lf\n", v); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////// + + { + MeterInfo mi; + mi.parse("testur", "multical21", "12345678", ""); + shared_ptr meter = createMeter(&mi); + FieldInfo *fi_flow = meter->findFieldInfo("flow_temperature", Quantity::Temperature); + FieldInfo *fi_ext = meter->findFieldInfo("external_temperature", Quantity::Temperature); + assert(fi_flow != NULL); + assert(fi_ext != NULL); + + vector frame; + hex2bin("2a442d2c785634121B168d2091d37cac217f2d7802ff207100041308190000441308190000615B1f616713", &frame); + + Telegram t; + MeterKeys mk; + t.parse(frame, &mk, true); + + string id; + bool match; + meter->handleTelegram(t.about, frame, true, &id, &match, &t); + + f->clear(); + + assert(f->doField(Unit::C, meter.get(), fi_flow)); + v = f->calculate(Unit::C); + if (v != 31) + { + printf("ERROR in test formula 5 expected 31 but got %lf\n", v); + } + + f->clear(); + + assert(f->doField(Unit::C, meter.get(), fi_flow)); + assert(f->doField(Unit::C, meter.get(), fi_ext)); + assert(f->doAddition()); + v = f->calculate(Unit::C); + if (v != 50) + { + printf("ERROR in test formula 6 expected 50 but got %lf\n", v); + } + + + // Check that trying to add a field reference expecting a non-convertible unit, will fail! + f->clear(); + assert(false == f->doField(Unit::M3, meter.get(), fi_flow)); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////// + + { + MeterInfo mi; + mi.parse("testur", "ebzwmbe", "22992299", ""); + shared_ptr meter = createMeter(&mi); + FieldInfo *fi_p1 = meter->findFieldInfo("current_power_consumption_phase1", Quantity::Power); + FieldInfo *fi_p2 = meter->findFieldInfo("current_power_consumption_phase2", Quantity::Power); + FieldInfo *fi_p3 = meter->findFieldInfo("current_power_consumption_phase3", Quantity::Power); + assert(fi_p1 != NULL); + assert(fi_p2 != NULL); + assert(fi_p3 != NULL); + + vector frame; + hex2bin("5B445a149922992202378c20f6900f002c25Bc9e0000BBBBBBBBBBBBBBBB72992299225a140102f6003007102f2f040330f92a0004a9ff01ff24000004a9ff026a29000004a9ff03460600000dfd11063132333435362f2f2f2f2f2f", &frame); + + Telegram t; + MeterKeys mk; + t.parse(frame, &mk, true); + + string id; + bool match; + meter->handleTelegram(t.about, frame, true, &id, &match, &t); + + unique_ptr f = unique_ptr(new FormulaImplementation()); + + f->clear(); + + assert(f->doField(Unit::KW, meter.get(), fi_p1)); + assert(f->doField(Unit::KW, meter.get(), fi_p2)); + assert(f->doAddition()); + assert(f->doField(Unit::KW, meter.get(), fi_p3)); + assert(f->doAddition()); + + v = f->calculate(Unit::KW); + if (v != 0.21679) + { + printf("ERROR in test formula 7 expected 0.21679 but got %lf\n", v); + } + } +} + +void test_formula_tree(FormulaImplementation *f, Meter *m, string formula, string tree) +{ + f->clear(); + f->parse(m, formula); + string t = f->tree(); + if (t != tree) + { + printf("ERROR when parsing \"%s\" expected tree to be \"%s\"\nbut got \"%s\"\n", + formula.c_str(), tree.c_str(), t.c_str()); + } +} + +void test_formula_value(FormulaImplementation *f, Meter *m, string formula, double val, Unit unit) +{ + f->clear(); + + bool ok = f->parse(m, formula); + assert(ok); + + double v = f->calculate(unit); + debug("(formula) %s\n", f->tree().c_str()); + + if (v != val) + { + printf("ERROR when evaluating \"%s\"\nERROR expected %.15g but got %.15g\n", formula.c_str(), val, v); + } +} + +void test_formulas_parsing() +{ + { + MeterInfo mi; + mi.parse("testur", "ebzwmbe", "22992299", ""); + shared_ptr meter = createMeter(&mi); + + vector frame; + hex2bin("5B445a149922992202378c20f6900f002c25Bc9e0000BBBBBBBBBBBBBBBB72992299225a140102f6003007102f2f040330f92a0004a9ff01ff24000004a9ff026a29000004a9ff03460600000dfd11063132333435362f2f2f2f2f2f", &frame); + + Telegram t; + MeterKeys mk; + t.parse(frame, &mk, true); + + string id; + bool match; + meter->handleTelegram(t.about, frame, true, &id, &match, &t); + + unique_ptr f = unique_ptr(new FormulaImplementation()); + + test_formula_value(f.get(), meter.get(), + "10 kwh + 100 kwh", + 110, + Unit::KWH); + + test_formula_value(f.get(), meter.get(), + "current_power_consumption_phase1_kw + " + "current_power_consumption_phase2_kw + " + "current_power_consumption_phase3_kw + " + "100 kw", + 100.21679, + Unit::KW); + + test_formula_tree(f.get(), meter.get(), + "5 c + 7 c + 10 c", + " > >"); + + test_formula_tree(f.get(), meter.get(), + "(5 c + 7 c) + 10 c", + " > >"); + + test_formula_tree(f.get(), meter.get(), + "5 c + (7 c + 10 c)", + " > >"); + } +} diff --git a/src/units.cc b/src/units.cc index 77b62af..bc30e2a 100644 --- a/src/units.cc +++ b/src/units.cc @@ -19,6 +19,7 @@ #include"util.h" #include #include +#include using namespace std; @@ -81,6 +82,16 @@ LIST_OF_CONVERSIONS return 0; } +Unit whenMultiplied(Unit left, Unit right) +{ + return Unit::Unknown; +} + +double multiply(double l, Unit left, double r, Unit right) +{ + return 0; +} + bool isQuantity(Unit u, Quantity q) { #define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return Quantity::quantity == q; @@ -90,6 +101,15 @@ LIST_OF_UNITS return false; } +Quantity toQuantity(Unit u) +{ +#define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return Quantity::quantity; +LIST_OF_UNITS +#undef X + + return Quantity::Unknown; +} + void assertQuantity(Unit u, Quantity q) { if (!isQuantity(u, q)) @@ -117,7 +137,7 @@ LIST_OF_QUANTITIES Unit toUnit(string s) { -#define X(cname,lcname,hrname,quantity,explanation) if (s == #cname) return Unit::cname; +#define X(cname,lcname,hrname,quantity,explanation) if (s == #cname || s == #lcname) return Unit::cname; LIST_OF_UNITS #undef X @@ -190,3 +210,30 @@ string valueToString(double v, Unit u) if (s.length() == 0) return "0"; return s; } + +bool extractUnit(const string &s, string *vname, Unit *u) +{ + size_t pos; + string vn; + const char *c; + + if (s.length() < 3) goto err; + + pos = s.rfind('_'); + + if (pos == string::npos) goto err; + if (pos+1 >= s.length()) goto err; + vn = s.substr(0,pos); + pos++; + + c = s.c_str()+pos; + +#define X(cname,lcname,hrname,quantity,explanation) if (!strcmp(c, #lcname)) { *u = Unit::cname; *vname = vn; return true; } +LIST_OF_UNITS +#undef X + +err: +*vname = ""; +*u = Unit::Unknown; +return false; +} diff --git a/src/units.h b/src/units.h index a33efbe..5a0eded 100644 --- a/src/units.h +++ b/src/units.h @@ -90,9 +90,14 @@ LIST_OF_QUANTITIES bool canConvert(Unit from, Unit to); double convert(double v, Unit from, Unit to); +Unit whenMultiplied(Unit left, Unit right); +double multiply(double l, Unit left, double r, Unit right); + +// Either uppercase KWH or lowercase kwh works here. Unit toUnit(std::string s); const char *toString(Quantity q); bool isQuantity(Unit u, Quantity q); +Quantity toQuantity(Unit u); void assertQuantity(Unit u, Quantity q); Unit defaultUnitForQuantity(Quantity q); std::string unitToStringHR(Unit u); @@ -102,4 +107,6 @@ std::string valueToString(double v, Unit u); Unit replaceWithConversionUnit(Unit u, std::vector cs); +bool extractUnit(const std::string &s, std::string *vname, Unit *u); + #endif diff --git a/src/util.cc b/src/util.cc index 1776804..400d13e 100644 --- a/src/util.cc +++ b/src/util.cc @@ -453,6 +453,10 @@ bool isDebugEnabled() { return debug_enabled_; } +bool isTraceEnabled() { + return trace_enabled_; +} + bool isLogTelegramsEnabled() { return log_telegrams_enabled_; } diff --git a/src/util.h b/src/util.h index 21c995d..8c25b6a 100644 --- a/src/util.h +++ b/src/util.h @@ -115,6 +115,7 @@ bool isInternalTestingEnabled(); bool isVerboseEnabled(); bool isDebugEnabled(); +bool isTraceEnabled(); bool isLogTelegramsEnabled(); void debugPayload(std::string intro, std::vector &payload); diff --git a/src/wmbus.cc b/src/wmbus.cc index 7dc1792..fba14dd 100644 --- a/src/wmbus.cc +++ b/src/wmbus.cc @@ -789,7 +789,6 @@ void Telegram::addExplanationAndIncrementPos(vector::iterator &pos, int l vsnprintf(buf, 1023, fmt, args); va_end(args); - Explanation e(parsed.size(), len, buf, k, u); explanations.push_back(e); parsed.insert(parsed.end(), pos, pos+len);