/* Copyright (C) 2017-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"bus.h" #include"cmdline.h" #include"config.h" #include"meters.h" #include"printer.h" #include"rtlsdr.h" #include"serial.h" #include"shell.h" #include"threads.h" #include"util.h" #include"version.h" #include"wmbus.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; int main(int argc, char **argv); shared_ptr create_printer(Configuration *config); SpecifiedDevice *find_specified_device_from_detected(Configuration *c, Detected *d); void list_fields(Configuration *config, string meter_type); void list_shell_envs(Configuration *config, string meter_type); void list_meters(Configuration *config); void list_units(); void log_start_information(Configuration *config); void oneshot_check(Configuration *config, Telegram *t, Meter *meter); void regular_checkup(Configuration *config); bool start(Configuration *config); void start_using_config_files(string root, bool is_daemon, ConfigOverrides overrides); void start_daemon(string pid_file, ConfigOverrides overrides); void setup_log_file(Configuration *config); void setup_meters(Configuration *config, MeterManager *manager); void write_pid(string pid_file, int pid); // The serial communication manager takes care of // monitoring the file descrtiptors for the ttys, // background shells, files and stdin. It also invokes // regular callbacks used for monitoring alarms and // detecting new devices. shared_ptr serial_manager_; // Manage registered meters. shared_ptr meter_manager_; // Manage bus devices that receive telegrams or send commands to meters. shared_ptr bus_manager_; // The printer renders the telegrams to: json, fields or shell calls. shared_ptr printer_; int main(int argc, char **argv) { tzset(); // Load the current timezone. auto config = parseCommandLine(argc, argv); if (config->version) { printf("wmbusmeters: " VERSION "\n"); printf(COMMIT "\n"); exit(0); } if (config->license) { const char * authors = #include"authors.h" const char * license = R"LICENSE( 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 . You can download the source here: https://github.com/wmbusmeters/wmbusmeters But you can also request the source from the person/company that provided you with this binary. Read the full license for all details. )LICENSE"; printf("%s%s", authors, license); exit(0); } if (config->list_shell_envs) { list_shell_envs(config.get(), config->list_meter); exit(0); } if (config->list_fields) { list_fields(config.get(), config->list_meter); exit(0); } if (config->list_meters) { list_meters(config.get()); exit(0); } if (config->list_units) { list_units(); exit(0); } if (config->need_help) { printf("wmbusmeters version: " VERSION "\n"); const char *short_manual = #include"short_manual.h" puts(short_manual); exit(0); } if (config->daemon) { start_daemon(config->pid_file, config->overrides); exit(0); } if (config->useconfig) { start_using_config_files(config->config_root, false, config->overrides); exit(0); } else { // We want the data visible in the log file asap! setbuf(stdout, NULL); start(config.get()); exit(0); } error("(main) internal error\n"); } shared_ptr create_printer(Configuration *config) { return shared_ptr(new Printer(config->json, config->pretty_print_json, config->fields, config->separator, config->meterfiles, config->meterfiles_dir, config->use_logfile, config->logfile, config->telegram_shells, config->meterfiles_action == MeterFileType::Overwrite, config->meterfiles_naming, config->meterfiles_timestamp)); } void list_shell_envs(Configuration *config, string meter_driver) { string ignore1, ignore2, ignore3; vector envs; Telegram t; t.about.device = "?"; MeterInfo mi; shared_ptr meter; DriverInfo di; mi.driver_name = meter_driver; if (!lookupDriverInfo(meter_driver, &di)) { error("No such driver %s\n", meter_driver.c_str()); } meter = di.construct(mi); printf("METER_DEVICE\n" "METER_ID\n" "METER_JSON\n" "METER_MEDIA\n" "METER_TYPE\n" "METER_NAME\n" "METER_RSSI_DBM\n" "METER_TIMESTAMP\n" "METER_TIMESTAMP_LT\n" "METER_TIMESTAMP_UT\n" "METER_TIMESTAMP_UTC\n"); for (auto &fi : meter->fieldInfos()) { if (fi.vname() == "") continue; string name = fi.vname(); std::transform(name.begin(), name.end(), name.begin(), ::toupper); if (fi.xuantity() != Quantity::Text) { printf("METER_%s_%s\n",name.c_str(), unitToStringUpperCase(fi.displayUnit()).c_str()); } else { printf("METER_%s\n",name.c_str()); } } for (string &s : meter->extraConstantFields()) { string name = s; std::transform(name.begin(), name.end(), name.begin(), ::toupper); printf("METER_%s\n", name.c_str()); } } void list_fields(Configuration *config, string meter_driver) { MeterInfo mi; shared_ptr meter; DriverInfo di; mi.driver_name = meter_driver; if (!lookupDriverInfo(meter_driver, &di)) { error("No such driver %s\n", meter_driver.c_str()); } meter = di.construct(mi); int width = 13; // Width of timestamp_utc for (FieldInfo &fi : meter->fieldInfos()) { if (fi.printProperties().hasHIDE()) continue; string name = fi.vname(); if (fi.xuantity() != Quantity::Text) { name += "_"+unitToStringLowerCase(defaultUnitForQuantity(fi.xuantity())); } if ((int)name.size() > width) width = name.size(); } string id = padLeft("id", width); printf("%s The meter id number.\n", id.c_str()); string name = padLeft("name", width); printf("%s Your name for the meter.\n", name.c_str()); string media = padLeft("media", width); printf("%s What does the meter measure?\n", media.c_str()); string meterr = padLeft("meter", width); printf("%s Meter driver.\n", meterr.c_str()); string timestamp = padLeft("timestamp", width); printf("%s Timestamp when wmbusmeters received the telegram. Local time for hr/fields UTC for json.\n", timestamp.c_str()); string timestamp_ut = padLeft("timestamp_ut", width); printf("%s Unix timestamp when wmbusmeters received the telegram.\n", timestamp_ut.c_str()); string timestamp_lt = padLeft("timestamp_lt", width); printf("%s Local time when wmbusmeters received the telegram.\n", timestamp_lt.c_str()); string timestamp_utc = padLeft("timestamp_utc", width); printf("%s UTC time when wmbusmeters received the telegram.\n", timestamp_utc.c_str()); string device = padLeft("device", width); printf("%s The wmbus device that received the telegram.\n", device.c_str()); string rssi = padLeft("rssi_dbm", width); printf("%s The rssi for the received telegram as reported by the device.\n", rssi.c_str()); for (auto &fi : meter->fieldInfos()) { if (fi.printProperties().hasHIDE()) continue; if (fi.vname() == "") continue; string name = fi.vname(); if (fi.xuantity() != Quantity::Text) { name += "_"+unitToStringLowerCase(defaultUnitForQuantity(fi.xuantity())); } string fn = padLeft(name, width); printf("%s %s\n", fn.c_str(), fi.help().c_str()); } } void list_meters(Configuration *config) { for (DriverInfo *di : allDrivers()) { string mname = di->name().str(); const char *info = toString(di->type()); if (config->list_meters_search == "" || \ stringFoundCaseIgnored(info, config->list_meters_search) || \ stringFoundCaseIgnored(mname.c_str(), config->list_meters_search)) \ printf("%-14s %s\n", mname.c_str(), info); } } struct TmpUnit { string suff, expl, name, quantity, si; }; void list_units() { vector units; set quantities; // X(KVARH,kvarh,"kVARh",Reactive_Energy,"kilo volt amperes reactive hour") int width = 1; int total = 50; int tmp = 0; #define X(upn,lcn,name,quantity,expl) \ tmp = strlen(#lcn); if (tmp > width) { width = tmp; } \ { \ SIUnit si(Unit::upn); \ string sis = si.str(); \ units.push_back( { #lcn, expl, name, #quantity, sis } ); \ } \ quantities.insert(#quantity); LIST_OF_UNITS #undef X sort(units.begin(), units.end(), [](const TmpUnit & a, const TmpUnit & b) -> bool { return a.suff > b.suff; }); bool first = true; for (const string &q : quantities) { if (!first) { printf("\n"); } first = false; printf("%s ", q.c_str()); size_t l = total-strlen(q.c_str()); for (size_t i=0; i 0 && space < 100) { printf(" "); space--; } printf("%s\n", si.c_str()); } } } } void log_start_information(Configuration *config) { verbose("(wmbusmeters) version: " VERSION "\n"); if (config->exitafter != 0) { verbose("(config) wmbusmeters will exit after %d seconds\n", config->exitafter); } if (config->meterfiles) { verbose("(config) store meter files in: \"%s\"\n", config->meterfiles_dir.c_str()); } for (SpecifiedDevice &specified_device : config->supplied_bus_devices) { verbose("(config) using device: %s \n", specified_device.str().c_str()); } verbose("(config) number of meters: %d\n", config->meters.size()); } void oneshot_check(Configuration *config, Telegram *t, Meter *meter) { if (!config->oneshot) return; if (meter_manager_->hasAllMetersReceivedATelegram()) { // All meters have received at least one update! Stop! verbose("(main) all meters have received at least one update, stopping.\n"); serial_manager_->stop(); } } time_t last_info_print_ = 0; void regular_checkup(Configuration *config) { if (config->daemon) { time_t now = time(NULL); if (now - last_info_print_ > 3600*24) { last_info_print_= now; size_t peak_rss = getPeakRSS(); size_t curr_rss = getCurrentRSS(); string prss = humanReadableTwoDecimals(peak_rss); // Log memory usage once per day. notice_timestamp("(memory) rss %zu peak %s\n", curr_rss, prss.c_str()); } } meter_manager_->pollMeters(bus_manager_); if (serial_manager_ && config) { bus_manager_->detectAndConfigureWmbusDevices(config, DetectionType::ALL_BUT_SFS); } bus_manager_->regularCheckup(); bus_manager_->sendQueue(); } void setup_log_file(Configuration *config) { if (config->use_logfile) { verbose("(wmbusmeters) using log file %s\n", config->logfile.c_str()); if (config->logfile == "syslog") { enableSyslog(); return; } bool ok = enableLogfile(config->logfile, config->daemon); if (!ok) { if (config->daemon) { warning("Could not open log file %s will use syslog instead.\n", config->logfile.c_str()); } else { error("Could not open log file %s\n", config->logfile.c_str()); } } } else { disableLogfile(); } } void setup_meters(Configuration *config, MeterManager *manager) { for (MeterInfo &m : config->meters) { m.extra_calculated_fields.insert(m.extra_calculated_fields.end(), config->extra_calculated_fields.begin(), config->extra_calculated_fields.end()); if (m.usesPolling() || driverNeedsPolling(m.driver_name)) { // A polling meter must be defined from the start. auto meter = createMeter(&m); manager->addMeter(meter); } else { // Non polling meters are added lazily, when // the first telegram arrives. Just add a template // here. manager->addMeterTemplate(m); } } } bool start(Configuration *config) { // Configure where the logging information should end up. setup_log_file(config); if (config->meters.size() == 0) { if (config->num_wmbus_devices > 0 && config->all_device_linkmodes_specified.empty()) { error("Wmbus devices found but no meters supplied. You must supply which link modes to listen to. Eg. auto:c1\n"); } } // Configure settings. silentLogging(config->silent); verboseEnabled(config->verbose); debugEnabled(config->debug); traceEnabled(config->trace); logTelegramsEnabled(config->logtelegrams); if (config->addtimestamps == AddLogTimestamps::NotSet) { // Default to important log timestamps when starting a daemon. if (config->daemon) config->addtimestamps = AddLogTimestamps::Important; } setLogTimestamps(config->addtimestamps); internalTestingEnabled(config->internaltesting); stderrEnabled(config->use_stderr_for_log); setAlarmShells(config->alarm_shells); setIgnoreDuplicateTelegrams(config->ignore_duplicate_telegrams); log_start_information(config); // Create the manager monitoring all filedescriptors and invoking callbacks. serial_manager_ = createSerialCommunicationManager(config->exitafter, true); // If our software unexpectedly exits, then stop the manager, to try // to achive a nice shutdown. onExit(call(serial_manager_.get(),stop)); /* Detected d; d.specified_device.file = "/dev/ttyUSB0"; d.found_file = "/dev/ttyUSB0"; d.specified_device.type = BusDeviceType::DEVICE_IU880B; detectIU880B(&d, serial_manager_); */ // Create the printer object that knows how to translate // telegrams into json, fields that are written into log files // or sent to shell invocations. printer_ = create_printer(config); // The meter manager knows about specified device templates // and creates meters on demand when the telegram arrives // or on startup for 2-way communication meters like mbus or T2. meter_manager_ = createMeterManager(config->daemon); meter_manager_->analyzeEnabled(config->analyze, config->analyze_format, config->analyze_driver, config->analyze_key, config->analyze_verbose, config->analyze_profile); // The bus manager detects new/lost wmbus devices and // configures the devices according to the specification. bus_manager_ = createBusManager(serial_manager_, meter_manager_); // When a meter is updated, print it, shell it, log it, etc. meter_manager_->whenMeterUpdated( [&](Telegram *t,Meter *meter) { printer_->print(t, meter, &config->extra_constant_fields, &config->selected_fields); oneshot_check(config, t, meter); } ); setup_meters(config, meter_manager_.get()); bus_manager_->detectAndConfigureWmbusDevices(config, DetectionType::STDIN_FILE_SIMULATION); serial_manager_->startEventLoop(); bus_manager_->detectAndConfigureWmbusDevices(config, DetectionType::ALL_BUT_SFS); if (bus_manager_->numBusDevices() == 0) { if (config->nodeviceexit) { notice("No wmbus device detected. Exiting!\n"); serial_manager_->stop(); } else { notice("No wmbus device detected, waiting for a device to be plugged in.\n"); checkIfMultipleWmbusMetersRunning(); } } // Every 2 seconds detect any plugged in or removed wmbus devices. serial_manager_->startRegularCallback("HOT_PLUG_DETECTOR", 2, [&](){ regular_checkup(config); }); if (config->daemon) { notice("(wmbusmeters) waiting for telegrams\n"); } bool stop_after_send = false; if ((config->logsummary || !meter_manager_->hasMeters()) && serial_manager_->isRunning()) { if (config->send_bus_content.size() != 0) { stop_after_send = true; } else if (!config->analyze) { if (!config->logsummary) notice("No meters configured. Printing id:s of all telegrams heard!\n"); meter_manager_->onTelegram([](AboutTelegram &about, vector frame) { Telegram t; t.about = about; MeterKeys mk; t.parse(frame, &mk, false); // Try a best effort parse, do not print any warnings. t.print(); string info = string("(")+toString(about.type)+")"; t.explainParse(info.c_str(), 0); logTelegram(t.original, t.frame, 0, 0); return true; }); } } bus_manager_->runAnySimulations(); // Queue any command line send bus contents. for (SendBusContent &sbc : config->send_bus_content) { bus_manager_->queueSendBusContent(sbc); } // Flush the initial queue provided on the command line. bus_manager_->sendQueue(); if (stop_after_send) { serial_manager_->stop(); } // This main thread now sleeps and waits for the serial communication manager to stop. // The manager has already started one thread that performs select and then callbacks // to decoding the telegrams, finally invoking the printer. // The regular callback invoked to detect changes in the wmbus devices and perform the alarm checks, // is started in a separate thread. // // Totalling 3 threads: main (sleeping here), serial manager (telegram handling), regular checks (check lost devices and alarms) serial_manager_->waitForStop(); if (config->daemon) { notice("(wmbusmeters) shutting down\n"); } bus_manager_->removeAllBusDevices(); meter_manager_->removeAllMeters(); printer_.reset(); serial_manager_.reset(); restoreSignalHandlers(); return gotHupped(); } void start_daemon(string pid_file, ConfigOverrides overrides) { setlogmask(LOG_UPTO (LOG_INFO)); openlog("wmbusmetersd", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1); enableSyslog(); // Pre check that the pid file can be writte to. // Exit before fork, if it fails. write_pid(pid_file, 0); pid_t pid = fork(); if (pid < 0) { error("Could not fork.\n"); } if (pid > 0) { // Success! The parent stores the pid and exits. write_pid(pid_file, pid); return; } // Change the file mode mask umask(0); // Create a new SID for the daemon pid_t sid = setsid(); if (sid < 0) { // log exit(-1); } if ((chdir("/")) < 0) { error("Could not change to root as current working directory."); } close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); if (open("/dev/null", O_RDONLY) == -1) { error("Failed to reopen stdin while daemonising (errno=%d)",errno); } if (open("/dev/null", O_WRONLY) == -1) { error("Failed to reopen stdout while daemonising (errno=%d)",errno); } if (open("/dev/null", O_RDWR) == -1) { error("Failed to reopen stderr while daemonising (errno=%d)",errno); } start_using_config_files("", true, overrides); } void start_using_config_files(string root, bool is_daemon, ConfigOverrides overrides) { bool restart = false; do { shared_ptr config = loadConfiguration(root, overrides); config->daemon = is_daemon; restart = start(config.get()); if (restart) { notice("(wmbusmeters) HUP received, restarting and reloading config files.\n"); } } while (restart); } void write_pid(string pid_file, int pid) { FILE *pidf = fopen(pid_file.c_str(), "w"); if (!pidf) { error("Could not open pid file \"%s\" for writing!\n", pid_file.c_str()); } if (pid > 0) { int n = fprintf(pidf, "%d\n", pid); if (!n) { error("Could not write pid (%d) to file \"%s\"!\n", pid, pid_file.c_str()); } notice("(wmbusmeters) started %s\n", pid_file.c_str()); } fclose(pidf); return; }