feat: add support to run shell script when meter is first seen

pull/1113/head
pimlie 2023-11-29 10:49:37 +01:00
rodzic b32236f600
commit db37acbcdc
17 zmienionych plików z 274 dodań i 45 usunięć

Wyświetl plik

@ -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=<cmdline> 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

Wyświetl plik

@ -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"}

Wyświetl plik

@ -458,6 +458,15 @@ static shared_ptr<Configuration> 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 == "") {

Wyświetl plik

@ -57,6 +57,7 @@ void parseMeterConfig(Configuration *c, vector<char> &buf, string file)
string linkmodes;
int poll_interval = 0;
vector<string> telegram_shells;
vector<string> meter_shells;
vector<string> alarm_shells;
vector<string> extra_constant_fields;
vector<string> extra_calculated_fields;
@ -131,6 +132,10 @@ void parseMeterConfig(Configuration *c, vector<char> &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<char> &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<Configuration> 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_"))

Wyświetl plik

@ -101,6 +101,7 @@ struct Configuration
bool fields {};
char separator { ';' };
std::vector<std::string> telegram_shells;
std::vector<std::string> meter_shells;
std::vector<std::string> 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.

Wyświetl plik

@ -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> meter)
{
vector<string> *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<string> 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<string> 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)

Wyświetl plik

@ -46,6 +46,7 @@ private:
vector<MeterInfo> meter_templates_;
vector<shared_ptr<Meter>> meters_;
vector<function<bool(AboutTelegram&,vector<uchar>)>> telegram_listeners_;
function<void(shared_ptr<Meter>)> on_meter_added_;
function<void(Telegram*t,Meter*)> 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> 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<void(shared_ptr<Meter>)> cb)
{
on_meter_added_ = cb;
}
void whenMeterUpdated(std::function<void(Telegram*t,Meter*)> cb)
{
on_meter_updated_ = cb;

Wyświetl plik

@ -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<string> &MeterCommonImplementation::shellCmdlines()
vector<string> &MeterCommonImplementation::shellCmdlinesMeterAdded()
{
return shell_cmdlines_;
return shell_cmdlines_added_;
}
vector<string> &MeterCommonImplementation::shellCmdlinesMeterUpdated()
{
return shell_cmdlines_updated_;
}
vector<string> &MeterCommonImplementation::meterExtraConstantFields()
@ -1665,6 +1679,26 @@ string FieldInfo::renderJson(Meter *m, DVEntry *dve)
return s;
}
void MeterCommonImplementation::createMeterEnv( string *id,
vector<string> *envs,
vector<string> *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<FieldInfo*,set<DVEntry*>> 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)

Wyświetl plik

@ -110,6 +110,7 @@ struct MeterInfo
LinkModeSet link_modes;
int bps {}; // For mbus communication you need to know the baud rate.
vector<string> shells;
vector<string> meter_shells;
vector<string> extra_constant_fields; // Additional static fields that are added to each message.
vector<string> extra_calculated_fields; // Additional field calculated using formulas.
vector<string> 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<string> i, string k, LinkModeSet lms, int baud, vector<string> &s, vector<string> &j, vector<string> &calcfs)
MeterInfo(string b, string n, string e, vector<string> i, string k, LinkModeSet lms, int baud, vector<string> &s, vector<string> &ms, vector<string> &j, vector<string> &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<void(Telegram*t,Meter*)> cb) = 0;
virtual int numUpdates() = 0;
virtual void createMeterEnv(string *id,
vector<string> *envs,
vector<string> *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<string> &shellCmdlines() = 0;
virtual void addShellMeterAdded(std::string cmdline) = 0;
virtual void addShellMeterUpdated(std::string cmdline) = 0;
virtual vector<string> &shellCmdlinesMeterAdded() = 0;
virtual vector<string> &shellCmdlinesMeterUpdated() = 0;
virtual void poll(shared_ptr<BusManager> 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> meter) = 0;
virtual void triggerMeterAdded(shared_ptr<Meter> meter) = 0;
virtual Meter*lastAddedMeter() = 0;
virtual void removeAllMeters() = 0;
virtual void forEachMeter(std::function<void(Meter*)> cb) = 0;
@ -454,6 +463,7 @@ struct MeterManager
virtual bool hasAllMetersReceivedATelegram() = 0;
virtual bool hasMeters() = 0;
virtual void onTelegram(function<bool(AboutTelegram&,vector<uchar>)> cb) = 0;
virtual void whenMeterAdded(std::function<void(shared_ptr<Meter>)> cb) = 0;
virtual void whenMeterUpdated(std::function<void(Telegram*t,Meter*)> cb) = 0;
virtual void pollMeters(shared_ptr<BusManager> bus) = 0;
virtual void analyzeEnabled(bool b, OutputFormat f, string force_driver, string key, bool verbose, int profile) = 0;

Wyświetl plik

@ -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<std::string> &shellCmdlines();
std::vector<std::string> &shellCmdlinesMeterAdded();
std::vector<std::string> &shellCmdlinesMeterUpdated();
std::vector<std::string> &meterExtraConstantFields();
void setMeterType(MeterType mt);
void addLinkMode(LinkMode lm);
@ -162,6 +164,9 @@ protected:
void poll(shared_ptr<BusManager> bus);
bool handleTelegram(AboutTelegram &about, vector<uchar> frame,
bool simulated, string *id, bool *id_match, Telegram *out_analyzed = NULL);
void createMeterEnv(string *id,
vector<string> *envs,
vector<string> *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<string> shell_cmdlines_;
vector<string> shell_cmdlines_added_;
vector<string> shell_cmdlines_updated_;
vector<string> extra_constant_fields_;
time_t poll_interval_ {};
Translate::Lookup mfct_tpl_status_bits_ = NoLookup;

Wyświetl plik

@ -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<string> &envs)
{
vector<string> *shells = &shell_cmdlines_;
if (meter->shellCmdlines().size() > 0) {
shells = &meter->shellCmdlines();
if (meter->shellCmdlinesMeterUpdated().size() > 0) {
shells = &meter->shellCmdlinesMeterUpdated();
}
for (auto &s : *shells) {
vector<string> args;

Wyświetl plik

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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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