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);