diff --git a/README.md b/README.md index ed1dd74..cb83e39 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,7 @@ As {options} you can use: --meterfilesnaming=(name|id|name-id) the meter file is the meter's: name, id or name-id --meterfilestimestamp=(never|day|hour|minute|micros) the meter file is suffixed with a timestamp (localtime) with the given resolution. + --metershell= invokes cmdline with env variables the first time a meter is seen since startup --nodeviceexit if no wmbus devices are found, then exit immediately --normal for normal logging --oneshot wait for an update from each meter, then quit diff --git a/simulations/simulation_metershell.txt b/simulations/simulation_metershell.txt new file mode 100644 index 0000000..39a7e5b --- /dev/null +++ b/simulations/simulation_metershell.txt @@ -0,0 +1,4 @@ +telegram=|A244EE4D785634123C067A8F000000|0C1348550000426CE1F14C130000000082046C21298C0413330000008D04931E3A3CFE3300000033000000330000003300000033000000330000003300000033000000330000003300000033000000330000004300000034180000046D0D0B5C2B03FD6C5E150082206C5C290BFD0F0200018C4079678885238310FD3100000082106C01018110FD610002FD66020002FD170000| +{"media":"warm water","meter":"supercom587","name":"MyWarmWater","id":"12345678","total_m3":5.548,"timestamp":"1111-11-11T11:11:11Z"} +telegram=|A244EE4D785634123C067A8F000000|0C1348560000426CE1F14C130000000082046C21298C0413330000008D04931E3A3CFE3300000033000000330000003300000033000000330000003300000033000000330000003300000033000000330000004300000034180000046D0D0B5C2B03FD6C5E150082206C5C290BFD0F0200018C4079678885238310FD3100000082106C01018110FD610002FD66020002FD170000| +{"media":"warm water","meter":"supercom587","name":"MyWarmWater","id":"12345678","total_m3":6.548,"timestamp":"1111-11-11T11:11:11Z"} diff --git a/src/cmdline.cc b/src/cmdline.cc index 90c2291..002d78c 100644 --- a/src/cmdline.cc +++ b/src/cmdline.cc @@ -458,6 +458,15 @@ static shared_ptr parseNormalCommandLine(Configuration *c, int ar i++; continue; } + if (!strncmp(argv[i], "--metershell=", 13)) { + string cmd = string(argv[i]+13); + if (cmd == "") { + error("The meter shell command cannot be empty.\n"); + } + c->meter_shells.push_back(cmd); + i++; + continue; + } if (!strncmp(argv[i], "--alarmshell=", 13)) { string cmd = string(argv[i]+13); if (cmd == "") { diff --git a/src/config.cc b/src/config.cc index b9c4c0a..a634c4d 100644 --- a/src/config.cc +++ b/src/config.cc @@ -57,6 +57,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) string linkmodes; int poll_interval = 0; vector telegram_shells; + vector meter_shells; vector alarm_shells; vector extra_constant_fields; vector extra_calculated_fields; @@ -131,6 +132,10 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) telegram_shells.push_back(p.second); } else + if (p.first == "metershell") { + meter_shells.push_back(p.second); + } + else if (p.first == "alarmshell") { alarm_shells.push_back(p.second); } @@ -199,6 +204,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) mi.extra_constant_fields = extra_constant_fields; mi.extra_calculated_fields = extra_calculated_fields; mi.shells = telegram_shells; + mi.meter_shells = meter_shells; mi.idsc = toIdsCommaSeparated(mi.ids); mi.selected_fields = selected_fields; c->meters.push_back(mi); @@ -636,6 +642,11 @@ void handleShell(Configuration *c, string cmdline) c->telegram_shells.push_back(cmdline); } +void handleMeterShell(Configuration *c, string cmdline) +{ + c->meter_shells.push_back(cmdline); +} + void handleAlarmShell(Configuration *c, string cmdline) { c->alarm_shells.push_back(cmdline); @@ -714,6 +725,7 @@ shared_ptr loadConfiguration(string root, ConfigOverrides overrid else if (p.first == "selectfields") handleSelectedFields(c, p.second); else if (p.first == "shell") handleShell(c, p.second); else if (p.first == "resetafter") handleResetAfter(c, p.second); + else if (p.first == "metershell") handleMeterShell(c, p.second); else if (p.first == "alarmshell") handleAlarmShell(c, p.second); else if (startsWith(p.first, "json_") || startsWith(p.first, "field_")) diff --git a/src/config.h b/src/config.h index 7004d40..938dcf4 100644 --- a/src/config.h +++ b/src/config.h @@ -101,6 +101,7 @@ struct Configuration bool fields {}; char separator { ';' }; std::vector telegram_shells; + std::vector meter_shells; std::vector alarm_shells; int alarm_timeout {}; // Maximum number of seconds between dongle receiving two telegrams. std::string alarm_expected_activity; // Only warn when within these time periods. diff --git a/src/main.cc b/src/main.cc index d5a2b46..b1f7517 100644 --- a/src/main.cc +++ b/src/main.cc @@ -552,6 +552,39 @@ bool start(Configuration *config) // configures the devices according to the specification. bus_manager_ = createBusManager(serial_manager_, meter_manager_); + // When a meter is added, print it, shell it, log it, etc. + meter_manager_->whenMeterAdded( + [&](shared_ptr meter) + { + vector *shells = &config->meter_shells; + if (meter->shellCmdlinesMeterAdded().size() > 0) { + shells = &meter->shellCmdlinesMeterAdded(); + } + + if (shells->size() < 1) { + // Early return when no meter_shell configured by user + return; + } + + vector envs; + + string id = ""; + if (meter->ids().size() > 0) + { + id = meter->idsc().c_str(); + } + + meter->createMeterEnv(&id, &envs, &config->extra_constant_fields); + + for (auto &s : *shells) { + vector args; + args.push_back("-c"); + args.push_back(s); + invokeShell("/bin/sh", args, envs); + } + } + ); + // When a meter is updated, print it, shell it, log it, etc. meter_manager_->whenMeterUpdated( [&](Telegram *t,Meter *meter) diff --git a/src/metermanager.cc b/src/metermanager.cc index c524e1b..5f1aed2 100644 --- a/src/metermanager.cc +++ b/src/metermanager.cc @@ -46,6 +46,7 @@ private: vector meter_templates_; vector> meters_; vector)>> telegram_listeners_; + function)> on_meter_added_; function on_meter_updated_; public: @@ -59,6 +60,12 @@ public: meters_.push_back(meter); meter->setIndex(meters_.size()); meter->onUpdate(on_meter_updated_); + triggerMeterAdded(meter); + } + + void triggerMeterAdded(shared_ptr meter) + { + if (on_meter_added_) on_meter_added_(meter); } Meter *lastAddedMeter() @@ -261,6 +268,11 @@ public: telegram_listeners_.push_back(cb); } + void whenMeterAdded(std::function)> cb) + { + on_meter_added_ = cb; + } + void whenMeterUpdated(std::function cb) { on_meter_updated_ = cb; diff --git a/src/meters.cc b/src/meters.cc index 3aeb941..0ad7991 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -272,7 +272,11 @@ MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, } for (auto s : mi.shells) { - addShell(s); + addShellMeterUpdated(s); + } + for (auto s : mi.meter_shells) + { + addShellMeterAdded(s); } for (auto j : mi.extra_constant_fields) { @@ -283,9 +287,14 @@ MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, force_mfct_index_ = di.forceMfctIndex(); } -void MeterCommonImplementation::addShell(string cmdline) +void MeterCommonImplementation::addShellMeterAdded(string cmdline) { - shell_cmdlines_.push_back(cmdline); + shell_cmdlines_added_.push_back(cmdline); +} + +void MeterCommonImplementation::addShellMeterUpdated(string cmdline) +{ + shell_cmdlines_updated_.push_back(cmdline); } void MeterCommonImplementation::addExtraConstantField(string ecf) @@ -338,9 +347,14 @@ void MeterCommonImplementation::addExtraCalculatedField(string ecf) ); } -vector &MeterCommonImplementation::shellCmdlines() +vector &MeterCommonImplementation::shellCmdlinesMeterAdded() { - return shell_cmdlines_; + return shell_cmdlines_added_; +} + +vector &MeterCommonImplementation::shellCmdlinesMeterUpdated() +{ + return shell_cmdlines_updated_; } vector &MeterCommonImplementation::meterExtraConstantFields() @@ -1665,6 +1679,26 @@ string FieldInfo::renderJson(Meter *m, DVEntry *dve) return s; } +void MeterCommonImplementation::createMeterEnv( string *id, + vector *envs, + vector *extra_constant_fields) +{ + envs->push_back(string("METER_ID="+*id)); + envs->push_back(string("METER_NAME=")+name()); + envs->push_back(string("METER_TYPE=")+driverName().str()); + + // If the configuration has supplied json_address=Roodroad 123 + // then the env variable METER_address will available and have the content "Roodroad 123" + for (string add_json : meterExtraConstantFields()) + { + envs->push_back(string("METER_")+add_json); + } + for (string extra_field : *extra_constant_fields) + { + envs->push_back(string("METER_")+extra_field); + } +} + void MeterCommonImplementation::printMeter(Telegram *t, string *human_readable, string *fields, char separator, @@ -1691,6 +1725,12 @@ void MeterCommonImplementation::printMeter(Telegram *t, media = mediaTypeJSON(t->dll_type, t->dll_mfct); } + string id = ""; + if (t->ids.size() > 0) + { + id = t->ids.back(); + } + string indent = ""; string newline = ""; @@ -1699,19 +1739,13 @@ void MeterCommonImplementation::printMeter(Telegram *t, indent = " "; newline ="\n"; } + string s; s += "{"+newline; s += indent+"\"media\":\""+media+"\","+newline; s += indent+"\"meter\":\""+driverName().str()+"\","+newline; s += indent+"\"name\":\""+name()+"\","+newline; - if (t->ids.size() > 0) - { - s += indent+"\"id\":\""+t->ids.back()+"\","+newline; - } - else - { - s += indent+"\"id\":\"\","+newline; - } + s += indent+"\"id\":\""+id+"\","+newline; // Iterate over the meter field infos... map> founds; // Multiple dventries can match to a single field info. @@ -1842,27 +1876,14 @@ void MeterCommonImplementation::printMeter(Telegram *t, s += "}"; *json = s; + createMeterEnv(&id, envs, extra_constant_fields); + envs->push_back(string("METER_JSON=")+*json); - if (t->ids.size() > 0) - { - envs->push_back(string("METER_ID=")+t->ids.back()); - } - else - { - envs->push_back(string("METER_ID=")); - } - envs->push_back(string("METER_NAME=")+name()); envs->push_back(string("METER_MEDIA=")+media); - envs->push_back(string("METER_TYPE=")+driverName().str()); envs->push_back(string("METER_TIMESTAMP=")+datetimeOfUpdateRobot()); envs->push_back(string("METER_TIMESTAMP_UTC=")+datetimeOfUpdateRobot()); envs->push_back(string("METER_TIMESTAMP_UT=")+unixTimestampOfUpdate()); envs->push_back(string("METER_TIMESTAMP_LT=")+datetimeOfUpdateHumanReadable()); - if (t->about.device != "") - { - envs->push_back(string("METER_DEVICE=")+t->about.device); - envs->push_back(string("METER_RSSI_DBM=")+to_string(t->about.rssi_dbm)); - } for (FieldInfo& fi : field_infos_) { @@ -1883,16 +1904,12 @@ void MeterCommonImplementation::printMeter(Telegram *t, } } - // If the configuration has supplied json_address=Roodroad 123 - // then the env variable METER_address will available and have the content "Roodroad 123" - for (string add_json : meterExtraConstantFields()) + if (t->about.device != "") { - envs->push_back(string("METER_")+add_json); - } - for (string extra_field : *extra_constant_fields) - { - envs->push_back(string("METER_")+extra_field); + envs->push_back(string("METER_DEVICE=")+t->about.device); + envs->push_back(string("METER_RSSI_DBM=")+to_string(t->about.rssi_dbm)); } + } void MeterCommonImplementation::setExpectedTPLSecurityMode(TPLSecurityMode tsm) diff --git a/src/meters.h b/src/meters.h index 424806e..1fc9986 100644 --- a/src/meters.h +++ b/src/meters.h @@ -110,6 +110,7 @@ struct MeterInfo LinkModeSet link_modes; int bps {}; // For mbus communication you need to know the baud rate. vector shells; + vector meter_shells; vector extra_constant_fields; // Additional static fields that are added to each message. vector extra_calculated_fields; // Additional field calculated using formulas. vector selected_fields; // Usually set to the default fields, but can be override in meter config. @@ -124,7 +125,7 @@ struct MeterInfo string str(); DriverName driverName(); - MeterInfo(string b, string n, string e, vector i, string k, LinkModeSet lms, int baud, vector &s, vector &j, vector &calcfs) + MeterInfo(string b, string n, string e, vector i, string k, LinkModeSet lms, int baud, vector &s, vector &ms, vector &j, vector &calcfs) { bus = b; name = n; @@ -133,6 +134,7 @@ struct MeterInfo idsc = toIdsCommaSeparated(ids); key = k; shells = s; + meter_shells = ms; extra_constant_fields = j; extra_calculated_fields = calcfs; link_modes = lms; @@ -147,6 +149,7 @@ struct MeterInfo idsc = ""; key = ""; shells.clear(); + meter_shells.clear(); extra_constant_fields.clear(); extra_calculated_fields.clear(); link_modes.clear(); @@ -414,6 +417,9 @@ struct Meter virtual void onUpdate(std::function cb) = 0; virtual int numUpdates() = 0; + virtual void createMeterEnv(string *id, + vector *envs, + vector *more_json) = 0; virtual void printMeter(Telegram *t, string *human_readable, string *fields, char separator, @@ -431,8 +437,10 @@ struct Meter virtual MeterKeys *meterKeys() = 0; virtual void addExtraCalculatedField(std::string ecf) = 0; - virtual void addShell(std::string cmdline) = 0; - virtual vector &shellCmdlines() = 0; + virtual void addShellMeterAdded(std::string cmdline) = 0; + virtual void addShellMeterUpdated(std::string cmdline) = 0; + virtual vector &shellCmdlinesMeterAdded() = 0; + virtual vector &shellCmdlinesMeterUpdated() = 0; virtual void poll(shared_ptr bus) = 0; virtual FieldInfo *findFieldInfo(string vname, Quantity xuantity) = 0; @@ -447,6 +455,7 @@ struct MeterManager { virtual void addMeterTemplate(MeterInfo &mi) = 0; virtual void addMeter(shared_ptr meter) = 0; + virtual void triggerMeterAdded(shared_ptr meter) = 0; virtual Meter*lastAddedMeter() = 0; virtual void removeAllMeters() = 0; virtual void forEachMeter(std::function cb) = 0; @@ -454,6 +463,7 @@ struct MeterManager virtual bool hasAllMetersReceivedATelegram() = 0; virtual bool hasMeters() = 0; virtual void onTelegram(function)> cb) = 0; + virtual void whenMeterAdded(std::function)> cb) = 0; virtual void whenMeterUpdated(std::function cb) = 0; virtual void pollMeters(shared_ptr bus) = 0; virtual void analyzeEnabled(bool b, OutputFormat f, string force_driver, string key, bool verbose, int profile) = 0; diff --git a/src/meters_common_implementation.h b/src/meters_common_implementation.h index cf32d12..1765959 100644 --- a/src/meters_common_implementation.h +++ b/src/meters_common_implementation.h @@ -95,9 +95,11 @@ protected: void triggerUpdate(Telegram *t); void setExpectedELLSecurityMode(ELLSecurityMode dsm); void setExpectedTPLSecurityMode(TPLSecurityMode tsm); - void addShell(std::string cmdline); + void addShellMeterAdded(std::string cmdline); + void addShellMeterUpdated(std::string cmdline); void addExtraConstantField(std::string ecf); - std::vector &shellCmdlines(); + std::vector &shellCmdlinesMeterAdded(); + std::vector &shellCmdlinesMeterUpdated(); std::vector &meterExtraConstantFields(); void setMeterType(MeterType mt); void addLinkMode(LinkMode lm); @@ -162,6 +164,9 @@ protected: void poll(shared_ptr bus); bool handleTelegram(AboutTelegram &about, vector frame, bool simulated, string *id, bool *id_match, Telegram *out_analyzed = NULL); + void createMeterEnv(string *id, + vector *envs, + vector *more_json); // Add this json "key"="value" strings. void printMeter(Telegram *t, string *human_readable, string *fields, char separator, @@ -225,7 +230,8 @@ private: time_t datetime_of_update_ {}; time_t datetime_of_poll_ {}; LinkModeSet link_modes_ {}; - vector shell_cmdlines_; + vector shell_cmdlines_added_; + vector shell_cmdlines_updated_; vector extra_constant_fields_; time_t poll_interval_ {}; Translate::Lookup mfct_tpl_status_bits_ = NoLookup; diff --git a/src/printer.cc b/src/printer.cc index 0ab6af0..83b0654 100644 --- a/src/printer.cc +++ b/src/printer.cc @@ -51,7 +51,7 @@ void Printer::print(Telegram *t, Meter *meter, meter->printMeter(t, &human_readable, &fields, separator_, &json, &envs, more_json, selected_fields, pretty_print_json_); - if (shell_cmdlines_.size() > 0 || meter->shellCmdlines().size() > 0) { + if (shell_cmdlines_.size() > 0 || meter->shellCmdlinesMeterUpdated().size() > 0) { printShells(meter, envs); printed = true; } @@ -69,8 +69,8 @@ void Printer::print(Telegram *t, Meter *meter, void Printer::printShells(Meter *meter, vector &envs) { vector *shells = &shell_cmdlines_; - if (meter->shellCmdlines().size() > 0) { - shells = &meter->shellCmdlines(); + if (meter->shellCmdlinesMeterUpdated().size() > 0) { + shells = &meter->shellCmdlinesMeterUpdated(); } for (auto &s : *shells) { vector args; diff --git a/test.sh b/test.sh index 951e2aa..98f13b0 100755 --- a/test.sh +++ b/test.sh @@ -181,6 +181,15 @@ if [ "$?" != "0" ]; then RC="1"; fi ./tests/test_bad_driver.sh $PROG if [ "$?" != "0" ]; then RC="1"; fi +./tests/test_metershell_env.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + +./tests/test_metershell.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + +./tests/test_metershell2.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + if [ -x ../additional_tests.sh ] then (cd ..; ./additional_tests.sh $PROG) diff --git a/tests/config14/wmbusmeters.conf b/tests/config14/wmbusmeters.conf new file mode 100644 index 0000000..b55c8a9 --- /dev/null +++ b/tests/config14/wmbusmeters.conf @@ -0,0 +1,5 @@ +loglevel=debug +device=simulations/simulation_metershell.txt +logtelegrams=false +format=json +metershell=echo "IF THIS IS STOREd THEN THERE IS A BUG" > /tmp/wmbusmeters_metershell1_test \ No newline at end of file diff --git a/tests/config14/wmbusmeters.d/Vatten b/tests/config14/wmbusmeters.d/Vatten new file mode 100644 index 0000000..ca6b6b5 --- /dev/null +++ b/tests/config14/wmbusmeters.d/Vatten @@ -0,0 +1,6 @@ +name=Vatten +type=supercom587 +id=12345678 +key= +metershell=echo "TESTING METERSHELL $METER_ID" > /tmp/wmbusmeters_metershell1_test +shell=echo "TESTING SHELL $METER_ID" > /tmp/wmbusmeters_metershell2_test \ No newline at end of file diff --git a/tests/test_metershell.sh b/tests/test_metershell.sh new file mode 100755 index 0000000..3207211 --- /dev/null +++ b/tests/test_metershell.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +PROG="$1" + +mkdir -p testoutput +TEST=testoutput + +TESTNAME="Test metershell invocation" +TESTRESULT="ERROR" + +$PROG --metershell='echo "METER SETUP FOR: $METER_ID"' --shell='echo "METER DATA FOR $METER_ID"' simulations/simulation_metershell.txt MWW supercom587 12345678 "" \ + 2> $TEST/test_stderr.txt > $TEST/test_output.txt +if [ "$?" = "0" ] +then + cat $TEST/test_output.txt | sed 's/....-..-.. ..:..:..$/1111-11-11 11:11:11/g' > $TEST/test_responses.txt + + # Test that metershell is only ran once, but shell for every telegram + echo "METER SETUP FOR: 12345678" > $TEST/test_expected.txt + echo "METER DATA FOR 12345678" >> $TEST/test_expected.txt + echo "METER DATA FOR 12345678" >> $TEST/test_expected.txt + + diff $TEST/test_expected.txt $TEST/test_responses.txt + if [ "$?" = "0" ] + then + echo OK: $TESTNAME + TESTRESULT="OK" + fi +fi + +if [ "$TESTRESULT" = "ERROR" ] +then + echo ERROR: $TESTNAME + exit 1 +fi diff --git a/tests/test_metershell2.sh b/tests/test_metershell2.sh new file mode 100755 index 0000000..2c2e01c --- /dev/null +++ b/tests/test_metershell2.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +PROG="$1" +TEST=testoutput +mkdir -p $TEST + +TESTNAME="Test metershell in config file" +TESTRESULT="ERROR" + +$PROG --useconfig=tests/config14 2> $TEST/test_stderr.txt > $TEST/test_output.txt + +if [ "$?" = "0" ] +then + INFO=$(cat /tmp/wmbusmeters_metershell1_test) + EXPECTED='TESTING METERSHELL 12345678' + if [ "$INFO" = "$EXPECTED" ] + then + echo "OK: $TESTNAME (--metershell output)" + TESTRESULT="OK" + else + echo "Expected: $EXPECTED" + echo "Got : $INFO" + fi + + INFO=$(cat /tmp/wmbusmeters_metershell2_test) + EXPECTED='TESTING SHELL 12345678' + if [ "$INFO" = "$EXPECTED" ] + then + echo "OK: $TESTNAME (--shell output)" + TESTRESULT="OK" + else + echo "Expected: $EXPECTED" + echo "Got : $INFO" + fi +fi + +if [ "$TESTRESULT" = "ERROR" ] +then + echo ERROR: $TESTNAME + exit 1 +fi diff --git a/tests/test_metershell_env.sh b/tests/test_metershell_env.sh new file mode 100755 index 0000000..6b10459 --- /dev/null +++ b/tests/test_metershell_env.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +PROG="$1" + +mkdir -p testoutput +TEST=testoutput + +TESTNAME="Test metershell environment" +TESTRESULT="ERROR" + +export MY_ENV_TEST="hello world" + +$PROG --metershell='echo MY_ENV_TEST="$MY_ENV_TEST"' --shell='echo -n ""' simulations/simulation_metershell.txt MWW supercom587 12345678 "" > $TEST/test_output.txt 2> $TEST/test_stderr.txt +if [ "$?" = "0" ] +then + echo 'MY_ENV_TEST=hello world' > $TEST/test_expected.txt + diff $TEST/test_expected.txt $TEST/test_output.txt + if [ "$?" = "0" ] + then + echo OK: $TESTNAME + TESTRESULT="OK" + fi +fi + +if [ "$TESTRESULT" = "ERROR" ] +then + echo ERROR: $TESTNAME + exit 1 +fi