diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1200f58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/legacy + +config.ini +.ism.ini +ism.ini diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md index 1a28361..5b6e8a3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ # ism-catcher -Munin plugin and other stuff for rtl_433 +Munin plugin and other stuff for [rtl_433][1]. + + +## Hardware Requirements +* 24/7 running GNU/Linux system with USB port +* DVB-T dongle with [compatible RTL2832(U)][2] chipset +* Compatible wireless sensor (e.g. `THN128` or `THR128`) + + +## Software Requirements +* Munin master/node setup (+ Webserver for Munin output) +* [rtl_433][1] installation (and JSON-enabled device) + + +## Data Aggregation +See: `other-scripts/rtl_433.sh` + + +## Plugin Installation +```bash +# Setup INI file... +cp -vn config/sample.ini ~/.ism.ini +editor ~/.ism.ini + + +# Setup plugin configuration... +cp -vn plugin-conf.d/ism /etc/munin/plugin-conf.d/ +editor /etc/munin/plugin-conf.d/ism + +# Enable plugin... +ln -s /full/path/to/ism-catcher /etc/munin/plugins/ism + +# Test plugin... +munin-run ism config +munin-run ism + +# Restart daemon... +service munin-node restart +``` + + +[1]: https://github.com/merbanan/rtl_433 +[2]: http://amzn.to/2qIxh9n diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ce82278 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +* Include screenshots from Munin! (`example` directory) +* Duplicate data detection? (filter out double OSv1…) diff --git a/config/sample.ini b/config/sample.ini new file mode 100644 index 0000000..01b4f78 --- /dev/null +++ b/config/sample.ini @@ -0,0 +1,147 @@ +; ------------------------------------ ; +; SAMPLE CONFIGURATION FOR ISM-CATCHER ; +; ------------------------------------ ; + +;; (OPTIONAL) +;; Munin update interval. +;interval = 300 + +;; (OPTIONAL) +;; Location of binary *.db files. +;; Defaults to sys_get_temp_dir(). +;datadir = /tmp + +;; (OPTIONAL) +;; Purge old data in *.db files after so many packets. +;; At DBv1 each packet is 10 byte, excluding a 2 byte header. +;dbcleanup = 100 + +;; (OPTIONAL) +;; Custom title for graphs. +;title = 433.92 MHz + +;; (OPTIONAL) +;; Custom description for page. +;info = Powered by foo bar and bar foo. + +;; (OPTIONAL) +;; Custom vlabel for graphs. +;vlabel = degree Celsius + +;; (OPTIONAL) +;; Custom category for graphs. +;category = sensors + +;; (OPTIONAL) +;; Set upper limit for fancy graphs. +;highest = 50.0 + +;; (OPTIONAL) +;; Set lower limit for fancy graphs. +;lowest = -20.0 + +;; (OPTIONAL) +;; Draw a line at with color . +;; The third argument would print a legend too. +;; Very usefull for freezing mark or similar. +base = 0:BEBEBE + + +; Text between [] is an unique key for one sensor/measurement. +; It's impossible to combine two values from one wireless sensor +; into one definition. Each value needs its own definition... +[EXAMPLE] + +; All lower-cased settings are required conditions for this sensor. +; Just think of it as simple IF-EQUAL checks, concatenated by AND. +; The keys and values from JSON input are converted to lowercase. +; Beware of changed values in ID or SID fields on battery change! +model = OSv1 Temperature Sensor +channel = 1 +;sid = n + +;; (OPTIONAL) +;; All upper-cased settings are overriding the built-in default values. +;; Take a look at the Wiki: http://munin-monitoring.org/wiki/fieldnames +;LABEL = Visible name for this sensor +;INFO = Description for this sensor +;WARNING = []:[] +;CRITICAL = []:[] +;COLOUR = <0..28> +;LINE = <1..3> + +;; (OPTIONAL) +;; The default data source is "temperature_C". +;; You can override it with your own field names. +;SOURCE = humidity + +;; (OPTIONAL) +;; Use this line to hide the sensor in the graphs. +;; All data will be stored in the RRD, but it's hidden. +;graph = false + +;; (OPTIONAL) +;; By default processing of groups will stop after the first match. +;; You can change this behavior for sensors with multiple values. +;; (e.g. temperature, humidity, barometric pressure, ...) +;FINAL = false + + +; --------------------------------------- ; +; SOME EXAMPLES FROM MY PERSONAL SETUP... ; +; --------------------------------------- ; + +[FRIDGE] +model = OSv1 Temperature Sensor +channel = 3 + +LABEL = Fridge +INFO = Oregon Scientific THR128 (channel 3) +CRITICAL = 2:12 +COLOUR = 1 +LINE = 2 + + +[OUTDOOR] +model = OSv1 Temperature Sensor +channel = 2 +;sid = n + +LABEL = Outdoor +INFO = Oregon Scientific THN128 (channel 2) +COLOUR = 0 +LINE = 3 + + +[Acurite] +model = Acurite 606TX Sensor +;id = n + +GRAPH = false +LABEL = [!] Acurite +INFO = Acurite 606TX Sensor +COLOUR = 16 + + +[Nexus_Temperature] +model = Nexus Temperature/Humidity +channel = 1 +;id = n + +GRAPH = false +LABEL = [!] Nexus (temperature) +INFO = Nexus Temperature/Humidity +FINAL = false +COLOUR = 8 + + +[Nexus_Humidity] +model = Nexus Temperature/Humidity +channel = 1 +;id = n + +GRAPH = false +LABEL = [!] Nexus (humidity) +INFO = Nexus Temperature/Humidity +SOURCE = humidity +COLOUR = 20 diff --git a/example/OUTPUT.md b/example/OUTPUT.md new file mode 100644 index 0000000..199c075 --- /dev/null +++ b/example/OUTPUT.md @@ -0,0 +1,35 @@ +``` +$ ism-catcher --dump=outdoor | tail -n 10 +[2017-06-02T16:42:02+02:00] 30.100 (0x0000) +[2017-06-02T16:42:03+02:00] 30.100 (0x0000) +[2017-06-02T16:42:31+02:00] 29.900 (0x0000) +[2017-06-02T16:42:31+02:00] 29.900 (0x0000) +[2017-06-02T16:43:00+02:00] 29.800 (0x0000) +[2017-06-02T16:43:00+02:00] 29.800 (0x0000) +[2017-06-02T16:43:29+02:00] 29.800 (0x0000) +[2017-06-02T16:43:30+02:00] 29.800 (0x0000) +[2017-06-02T16:43:58+02:00] 29.800 (0x0000) +[2017-06-02T16:43:58+02:00] 29.800 (0x0000) + +$ ism-catcher --dump acurite | tail -n 10 +[2017-06-02T16:09:02+02:00] 32.100 (0x0001) +[2017-06-02T16:13:10+02:00] 32.200 (0x0001) +[2017-06-02T16:13:41+02:00] 32.100 (0x0001) +[2017-06-02T16:18:21+02:00] 32.000 (0x0001) +[2017-06-02T16:21:26+02:00] 31.800 (0x0001) +[2017-06-02T16:21:57+02:00] 31.700 (0x0001) +[2017-06-02T16:26:06+02:00] 31.600 (0x0001) +[2017-06-02T16:26:37+02:00] 31.500 (0x0001) +[2017-06-02T16:40:03+02:00] 30.700 (0x0001) +[2017-06-02T16:41:04+02:00] 30.700 (0x0001) + +$ ism-catcher +indoor.value 26.8 +fridge.value 7.1 +outdoor.value 29.8 +acurite.value 30.7 +nexus_temperature.value 28.2 +nexus_temperature.extinfo LOW BATTERY +nexus_humidity.value 19 +nexus_humidity.extinfo LOW BATTERY +``` diff --git a/example/TWITTER.md b/example/TWITTER.md new file mode 100644 index 0000000..c251117 --- /dev/null +++ b/example/TWITTER.md @@ -0,0 +1,3 @@ +Live demo of tweets and graphs: [@_GUMPENDORF_](https://twitter.com/_GUMPENDORF_) + +Generated on the Munin master server with simple scripts from `other-scripts` directory. diff --git a/example/example.json b/example/example.json new file mode 100644 index 0000000..58d2de6 --- /dev/null +++ b/example/example.json @@ -0,0 +1,100 @@ +{"time" : "2017-06-02 16:40:24", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:40:35", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:40:35", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:40:46", "model" : "Nexus Temperature/Humidity", "id" : 195, "battery" : "LOW", "channel" : 1, "temperature_C" : 28.300, "humidity" : 19} +{"time" : "2017-06-02 16:40:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:40:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:40:55", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:40:55", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:41:04", "model" : "Acurite 606TX Sensor", "id" : 111, "battery" : "LOW", "temperature_C" : 30.700} +{"time" : "2017-06-02 16:41:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:41:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:41:26", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:41:26", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:41:33", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:41:33", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:41:44", "model" : "Nexus Temperature/Humidity", "id" : 195, "battery" : "LOW", "channel" : 1, "temperature_C" : 28.200, "humidity" : 19} +{"time" : "2017-06-02 16:41:49", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:41:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:41:57", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:41:57", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.200} +{"time" : "2017-06-02 16:42:02", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:42:03", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.100} +{"time" : "2017-06-02 16:42:28", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:42:28", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:42:31", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.900} +{"time" : "2017-06-02 16:42:31", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.900} +{"time" : "2017-06-02 16:42:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:42:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:42:59", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:42:59", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:43:00", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:43:00", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:43:29", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:43:30", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:43:30", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:43:30", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:43:49", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:43:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:43:58", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:43:58", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.800} +{"time" : "2017-06-02 16:44:00", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:44:01", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:44:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:44:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:44:27", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.900} +{"time" : "2017-06-02 16:44:27", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 29.900} +{"time" : "2017-06-02 16:44:31", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:44:32", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.100} +{"time" : "2017-06-02 16:44:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:44:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:44:56", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.000} +{"time" : "2017-06-02 16:44:57", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.000} +{"time" : "2017-06-02 16:45:03", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:45:03", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:45:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:45:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:45:25", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.200} +{"time" : "2017-06-02 16:45:25", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.200} +{"time" : "2017-06-02 16:45:34", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:45:34", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:45:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:45:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.800} +{"time" : "2017-06-02 16:46:05", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:46:05", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:46:19", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:46:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:46:23", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.600} +{"time" : "2017-06-02 16:46:23", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.600} +{"time" : "2017-06-02 16:46:28", "model" : "Nexus Temperature/Humidity", "id" : 195, "battery" : "LOW", "channel" : 1, "temperature_C" : 28.000, "humidity" : 19} +{"time" : "2017-06-02 16:46:36", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:46:36", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:46:52", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.800} +{"time" : "2017-06-02 16:46:52", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.800} +{"time" : "2017-06-02 16:47:07", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:47:07", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:47:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:47:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:47:21", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.000} +{"time" : "2017-06-02 16:47:21", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.000} +{"time" : "2017-06-02 16:47:38", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:47:38", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:47:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:47:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:47:50", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.100} +{"time" : "2017-06-02 16:48:19", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.100} +{"time" : "2017-06-02 16:48:19", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.100} +{"time" : "2017-06-02 16:48:19", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:48:20", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:48:23", "model" : "Nexus Temperature/Humidity", "id" : 195, "battery" : "LOW", "channel" : 1, "temperature_C" : 28.100, "humidity" : 19} +{"time" : "2017-06-02 16:48:40", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:48:40", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 7.000} +{"time" : "2017-06-02 16:48:48", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.000} +{"time" : "2017-06-02 16:48:48", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 31.000} +{"time" : "2017-06-02 16:48:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:48:50", "model" : "OSv1 Temperature Sensor", "sid" : 0, "channel" : 1, "battery" : "OK", "temperature_C" : 26.900} +{"time" : "2017-06-02 16:49:11", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 6.900} +{"time" : "2017-06-02 16:49:11", "model" : "OSv1 Temperature Sensor", "sid" : 10, "channel" : 3, "battery" : "OK", "temperature_C" : 6.900} +{"time" : "2017-06-02 16:49:17", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.900} +{"time" : "2017-06-02 16:49:17", "model" : "OSv1 Temperature Sensor", "sid" : 6, "channel" : 2, "battery" : "OK", "temperature_C" : 30.900} +{"time" : "2017-06-02 16:49:20", "model" : "Nexus Temperature/Humidity", "id" : 195, "battery" : "LOW", "channel" : 1, "temperature_C" : 28.200, "humidity" : 19} diff --git a/ism-catcher b/ism-catcher new file mode 100755 index 0000000..b4621bc --- /dev/null +++ b/ism-catcher @@ -0,0 +1,869 @@ +#!/usr/bin/env php +] --live\n", $cmd); + print(" Parse JSON from STDIN, one dataset per line!\n\n"); + printf("Usage: %s [--config=] --dump=\n", $cmd); + print(" Dump packets of binary database .\n\n"); + printf("Usage: %s [--config=] [{autoconf|config}]\n", $cmd); + print(" Execute Munin plugin with optional arguments.\n\n"); + print("=========================\n"); + print(" Supported ENV variables \n"); + print("=========================\n\n"); + print(" * hostname = virtual hostname for munin\n"); + print(" * ini_file = path to configuration file\n\n"); + print("/-------------------------------------------------\\\n"); + print("| License: GNU General Public License v3.0 |\n"); + print("| Author: Christian Schroetter |\n"); + print("| Website: https://github.com/froonix/ism-catcher |\n"); + print("\\-------------------------------------------------/\n"); + exit; +} + +if(isset($opt['c'])) +{ + ISM::setINI($opt['c']); +} +else if(isset($opt['config'])) +{ + ISM::setINI($opt['config']); +} +else if(isset($_SERVER['ini_file'])) +{ + ISM::setINI($_SERVER['ini_file']); +} +else if(isset($_SERVER['HOME']) && file_exists($_SERVER['HOME'] . '/.ism.ini')) +{ + ISM::setINI($_SERVER['HOME'] . '/.ism.ini'); +} +else if(file_exists(__dir__ . '/ism.ini')) +{ + ISM::setINI(__dir__ . '/ism.ini'); +} +else if(file_exists(__dir__ . '/config.ini')) +{ + ISM::setINI(__dir__ . '/config.ini'); +} +else +{ + throw new Exception('INI file not found'); +} + +$arg = null; +if(isset($opt['dump'])) +{ + $mode = 'dump'; + $arg = $opt['dump']; +} +else if(isset($opt['live'])) +{ + $mode = 'live'; +} +else +{ + // Stupid and bad workaround for PHP 5.x... + $mode = strtolower(array_pop($_SERVER['argv'])); + + switch($mode) + { + case 'autoconf': + print("no\n"); + return 0; + break; + + case 'config': + break; + + default: + $mode = null; + break; + } +} + +$return = ISM::main($mode, $arg); +exit($return); + +function exception_error_handler($severity, $message, $file, $line) +{ + if (!(error_reporting() & $severity)) + { + return; + } + + throw new ErrorException($message, 0, $severity, $file, $line); +} + +abstract class ISM +{ + const PACKET_LIMIT = 100; + const MUNIN_INTERVAL = 300; + const BUFFER_SIZE = 1024; + const TEMPLATE_DB = '%s/ism-%s.db'; + const LOWBATT_WARNING = 'LOW BATTERY'; + + // Bit identifiers + const FLAG_NONE = 0; + const FLAG_LOWBATT = 1; + + // Default INI values + const DEFAULT_TITLE = 'ISM'; + const DEFAULT_CATEGORY = 'sensors'; + const DEFAULT_INFO = 'rtl_433'; + const DEFAULT_VLABEL = ''; + + private static $cfg = null; + private static $ini = null; + + public static function main($mode, $arg) + { + self::parseINI(); + + switch($mode) + { + case 'live': + return self::procRTL(); + break; + + case 'dump': + return self::dumpDB($arg); + break; + + case 'config': + return self::procMunin(true); + break; + + default: + return self::procMunin(); + break; + } + + return -1; + } + + public static function setINI($file) + { + self::$cfg = $file; + } + + private static function parseINI() + { + if((self::$ini = parse_ini_file(self::$cfg, true, INI_SCANNER_RAW)) === false) + { + throw new Exception(sprintf('Could not open or parse INI file %s', self::$cfg)); + } + } + + private static function openDB($id, $readonly = true, $create = false) + { + if(!isset(self::$ini['datadir'])) + { + self::$ini['datadir'] = sys_get_temp_dir(); + } + + return new ISMDB(sprintf(self::TEMPLATE_DB, self::$ini['datadir'], md5(strtolower($id))), $readonly, $create); + } + + private static function cleanField($str) + { + return preg_replace('/_{2,}/', '_', preg_replace('/[^a-z0-9_]+/', '_', strtolower($str))); + } + + private static function cleanLine($str) + { + return str_replace(["\r", "\n"], '', $str); + } + + private static function outputLine($key, $value, $_ = null) + { + $key = ($_ !== null) ? sprintf('%s.%s', self::cleanField($_), self::cleanField($key)) : $key; + $value = self::cleanLine($value); + + printf("%s %s\n", $key, $value); + } + + private static function procMunin($config = false) + { + if($config) + { + if(isset($_SERVER['hostname'])) + { + self::outputLine('host_name', $_SERVER['hostname']); + } + + if(!isset(self::$ini['title'])) + { + self::$ini['title'] = self::DEFAULT_TITLE; + } + + if(!isset(self::$ini['category'])) + { + self::$ini['category'] = self::DEFAULT_CATEGORY; + } + + if(!isset(self::$ini['vlabel'])) + { + self::$ini['vlabel'] = self::DEFAULT_VLABEL; + } + + if(!isset(self::$ini['info'])) + { + self::$ini['info'] = self::DEFAULT_INFO; + } + + self::outputLine('graph_title', self::$ini['title']); + self::outputLine('graph_category', self::$ini['category']); + self::outputLine('graph_vlabel', self::$ini['vlabel']); + self::outputLine('graph_info', self::$ini['info']); + self::outputLine('graph_scale', 'no'); + + $args = ['--base 1000']; + + if(isset(self::$ini['highest'])) + { + $args[] = sprintf('-u %f', self::$ini['highest']); + } + + if(isset(self::$ini['lowest'])) + { + $args[] = sprintf('-l %f', self::$ini['lowest']); + } + + self::outputLine('graph_args', implode(' ', $args)); + } + else if(!isset(self::$ini['interval']) || self::$ini['interval'] < 30) + { + self::$ini['interval'] = self::MUNIN_INTERVAL; + } + else + { + self::$ini['interval'] = (int) self::$ini['interval']; + } + + $base = false; + foreach(self::$ini as $section => $data) + { + if(!is_array($data)) + { + continue; + } + + if($config) + { + if(!isset($data['LABEL'])) + { + $data['LABEL'] = $section; + } + + self::outputLine('label', $data['LABEL'], $section); + + if(isset($data['COLOUR'])) + { + self::outputLine('colour', sprintf('COLOUR%d', $data['COLOUR']), $section); + } + + if(isset($data['LINE'])) + { + self::outputLine('draw', sprintf('LINE%d', $data['LINE']), $section); + } + + if(isset($data['INFO'])) + { + self::outputLine('info', $data['INFO'], $section); + } + + if(isset($data['CRITICAL'])) + { + self::outputLine('critical', $data['CRITICAL'], $section); + } + + if(isset($data['WARNING'])) + { + self::outputLine('warning', $data['WARNING'], $section); + } + + if(isset($data['GRAPH']) && in_array(strtolower($data['GRAPH']), ['0', 'false', 'off', 'no'])) + { + self::outputLine('graph', 'no', $section); + } + else if(!$base && isset(self::$ini['base'])) + { + self::outputLine('line', self::$ini['base'], $section); + $base = true; + } + } + else + { + unset($db); + $db = self::openDB($section); + + if($db->getPacketCount()) + { + $item = $db->getPacket(-1); + + if($item[ISMDB::FIELD_TIME] < (intval(time() / self::$ini['interval']) - 1) * self::$ini['interval']) + { + continue; + } + + self::outputLine('value', $item[ISMDB::FIELD_VALUE], $section); + + // TODO: Implement INI switch to output last update time? + #self::outputLine('extinfo', date('c', $item[ISMDB::FIELD_TIME]), $section); + + if($item[ISMDB::FIELD_FLAGS] & self::FLAG_LOWBATT) + { + self::outputLine('extinfo', self::LOWBATT_WARNING, $section); + } + } + } + } + unset($section, $data, $db); + + return 0; + } + + private static function procRTL($handle = STDIN) + { + if(!isset(self::$ini['dbcleanup'])) + { + self::$ini['dbcleanup'] = self::PACKET_LIMIT; + } + else + { + self::$ini['dbcleanup'] = abs((int) self::$ini['dbcleanup']); + } + + while(!feof($handle)) + { + if(($line = fgets($handle, self::BUFFER_SIZE)) === false) + { + if(feof($handle)) + { + return 0; + } + + throw new Exception('Could not read next line but EOF not reached'); + } + + $line = trim($line); + + if(empty($line)) + { + continue; + } + else if(!($json = json_decode($line, true))) + { + throw new Exception(sprintf('Could not parse JSON: %s', $line)); + } + + foreach($json as $key => $value) + { + unset($json[$key]); + $json[strtolower($key)] = (string) $value; + } + unset($key, $value); + + foreach(self::$ini as $section => $data) + { + if(!is_array($data)) + { + continue; + } + + foreach($data as $key => $value) + { + if(preg_match('/^[a-z0-9_]+$/', $key)) + { + if(!isset($json[$key]) || $value !== $json[$key]) + { + continue 2; + } + } + } + unset($key, $value); + + if(!isset($data['SOURCE'])) + { + $data['SOURCE'] = 'temperature_c'; + } + else + { + $data['SOURCE'] = strtolower($data['SOURCE']); + } + + if(!isset($json[$data['SOURCE']])) + { + continue; + } + + $item = [ + ISMDB::FIELD_TIME => strtotime($json['time']), + ISMDB::FIELD_VALUE => $json[$data['SOURCE']], + ISMDB::FIELD_FLAGS => self::FLAG_NONE, + ]; + + if(isset($json['battery']) && strtoupper($json['battery']) == 'LOW') + { + $item[ISMDB::FIELD_FLAGS] |= self::FLAG_LOWBATT; + } + + $db = self::openDB($section, false, true); + $db->cleanUp(-self::$ini['dbcleanup']); + $db->putPacket($item); + unset($db, $item); + + if(!isset($data['FINAL']) || !in_array(strtolower($data['FINAL']), ['0', 'false', 'off', 'no'])) + { + break; + } + } + unset($section, $data); + } + + return 0; + } + + private static function dumpDB($id) + { + $db = self::openDB($id); + + for($i = 0; $i < $db->getPacketCount(); $i++) + { + $data = $db->getPacketByIndex($i); + printf("[%s] %11s (0x%04X)\n", date('c', $data[ISMDB::FIELD_TIME]), number_format($data[ISMDB::FIELD_VALUE], ISMDB::DECIMALS, '.', ''), $data[ISMDB::FIELD_FLAGS]); + } + + return 0; + } +} + +# -------------------------------------------------------------------- # +# Don't leave a database open forever, because it will be locked! # +# This class exists only for short and exclusive read/write calls. # +# Just destroy it after usage to unlock the real database file... # +# # +# And yes, this dataformat is extreme overkill, but it's funny! :-) # +# -------------------------------------------------------------------- # +class ISMDB +{ + // Database format + const DBVERSION = 1; + const HEADERSIZE = 2; + const PACKETSIZE = 10; + + // Decimal precision + const DECIMALS = 3; + + // Data array keys + const FIELD_FLAGS = 0; + const FIELD_TIME = 1; + const FIELD_VALUE = 2; + + private $ro = null; + private $handle = null; + private $init = false; + private $version = self::DBVERSION; + private $flags = 0; + + public function __construct($file, $readonly = true, $create = true) + { + if(!file_exists($file) || filesize($file) < 2) + { + if(!$create && !file_exists($file)) + { + throw new Exception('Database does not exist'); + } + + file_put_contents($file, $this->buildHeader()); + clearstatcache(false, $file); + } + + if($readonly) + { + $this->ro = true; + $lock = LOCK_SH; + $mode = 'rb'; + } + else + { + $this->ro = false; + $lock = LOCK_EX; + $mode = 'r+b'; + } + + $this->handle = fopen($file, $mode); + + if(!flock($this->handle, $lock)) + { + throw new Exception(sprintf('Lock operation (%d) failed', $lock)); + } + + $this->readHeader(); + + if($this->version !== self::DBVERSION) + { + throw new Exception(sprintf('Unsupported DB version: %d', $this->version)); + } + else if($this->flags) + { + throw new Exception(sprintf('Unsupported flags: 0x%02X', $this->flags)); + } + + try + { + $this->getPacketCount(); + } + catch(Exception $e) + { + if($this->init) + { + throw $e; + } + else if(!$create) + { + throw new Exception('Database is damaged but recreation is forbidden'); + } + + $this->init = true; @unlink($file); + $this->__construct($file, $readonly, true); + } + } + + public function __destruct() + { + try + { + flock($this->handle, LOCK_UN); + fclose($this->handle); + unset($this->handle); + } + catch(Exception $e) + { + return; + } + } + + public function cleanUp($limit = 0) + { + if(($c = $this->getPacketCount()) > (abs($limit) * 1.5)) + { + if($limit < 0) + { + $this->removePackets(-($c - abs($limit))); + } + else + { + $this->removePackets($limit); + } + } + } + + public function removePacketByIndex($i) + { + return $this->removePackets(1, abs($i)); + } + + public function removePackets($num, $offset = 0) + { + $this->readOnlyCheck(); + + if(!$num) + { + throw new InvalidArgumentException('The packet selection can\'t be null'); + } + else if($offset < 0) + { + throw new InvalidArgumentException('Negative offsets not supported'); + } + else if((abs($num) + $offset) > ($c = $this->getPacketCount())) + { + throw new OutOfRangeException('Too many packets selected'); + } + + $this->seek(); + + if($num < 0) + { + if(!$offset) + { + if(!ftruncate($this->handle, self::HEADERSIZE + (self::PACKETSIZE * ($c - abs($num))))) + { + throw new Exception('Truncate operation failed'); + } + + return; + } + + $start = self::HEADERSIZE + (self::PACKETSIZE * ($c - ($offset + abs($num)))); + } + else + { + $start = self::HEADERSIZE + (self::PACKETSIZE * $offset); + } + + + $oldsize = self::HEADERSIZE + (self::PACKETSIZE * $c); + $length = self::PACKETSIZE * abs($num); + $last = ($oldsize - $start - $length); + $newsize = $start + $last; + + $tmp = fopen('php://memory', 'w+b'); + + if(($_ = stream_copy_to_stream($this->handle, $tmp, $start)) < $start) + { + throw new Exception(sprintf('Memory operation #1 failed (only %d/%d bytes copied)', $_, $start)); + } + else if(($_ = stream_copy_to_stream($this->handle, $tmp, $last, ($start + $length))) < $last) + { + throw new Exception(sprintf('Memory operation #2 failed (only %d/%d bytes copied)', $_, $last)); + } + else if(fseek($tmp, 0) < 0) + { + throw new Exception('Memory operation #3 failed'); + } + + $this->seek(); + + if(($_ = stream_copy_to_stream($tmp, $this->handle)) < $newsize) + { + throw new Exception(sprintf('Memory operation #4 failed (only %d/%d bytes copied)', $_, $newsize)); + } + else if(!ftruncate($this->handle, $newsize)) + { + throw new Exception('Memory operation #5 failed'); + } + + fclose($tmp); + } + + public function getPacketCount() + { + $this->seek(0, SEEK_END); + $l = ftell($this->handle); + + if($l === false) + { + throw new Exception('Could not get pointer'); + } + else if(($l - self::HEADERSIZE) % self::PACKETSIZE) + { + throw new Exception('File is corrupted'); + } + + return ($l - self::HEADERSIZE) / self::PACKETSIZE; + } + + public function getPacketByIndex($i) + { + return $this->getPacket(abs($i) + 1); + } + + public function getPacket($pos = -1) + { + if(!$pos) + { + throw new InvalidArgumentException('The packet position can\'t be null'); + } + else if(abs($pos) > $this->getPacketCount()) + { + throw new OutOfRangeException('Packet selection invalid'); + } + else if($pos > 0) + { + $offset = self::HEADERSIZE + (self::PACKETSIZE * ($pos - 1)); + $whence = SEEK_SET; + } + else + { + $offset = -(self::PACKETSIZE * (abs($pos) - 1)); + $whence = SEEK_END; + } + + $this->seekPacket($pos); + $data = $this->read(); + + $time = self::decodeInteger(substr($data, 2, 4)); + $value = self::decodeFloat(substr($data, 6, 4)); + + $flags = self::decodeFlags($data[0] . $data[1]); + $flags = $flags[0] + ($flags[1] << 8); + + return [ + self::FIELD_FLAGS => $flags, + self::FIELD_TIME => $time, + self::FIELD_VALUE => $value, + ]; + } + + public function putPacket($data = null, $pos = 0) + { + $this->readOnlyCheck(); + + if($pos !== 0) + { + throw new Exception('This would overwrite an existing packet! Insert operations are not implemented yet...'); + } + + $time = (isset($data[self::FIELD_TIME])) ? $data[self::FIELD_TIME] : 0; + $value = (isset($data[self::FIELD_VALUE])) ? $data[self::FIELD_VALUE] : 0.0; + + $flags = (isset($data[self::FIELD_FLAGS])) ? $data[self::FIELD_FLAGS] : 0; + $flagB = $flags >> 8; $flagA = $flags - ($flagB << 8); + + $stream = self::encodeFlags($flagA, $flagB); + $stream .= self::encodeInteger($time); + $stream .= self::encodeFloat($value); + + $this->seekPacket($pos); + $this->write($stream); + } + + private function readOnlyCheck() + { + if($this->ro) + { + throw new LogicException('Operation not supported in read-only mode'); + } + } + + private function readHeader() + { + $this->seek(); + $_ = self::decodeFlags($this->read(self::HEADERSIZE)); + $this->version = $_[0]; $this->flags = $_[1]; + } + + private function buildHeader() + { + return self::encodeFlags($this->version, $this->flags); + } + + private function seekPacket($pos = 0) + { + if(!$pos) + { + $offset = 0; + $whence = SEEK_END; + } + else if($pos > 0) + { + $offset = self::HEADERSIZE + (self::PACKETSIZE * ($pos - 1)); + $whence = SEEK_SET; + } + else + { + $offset = -(self::PACKETSIZE * abs($pos)); + $whence = SEEK_END; + } + + $this->seek($offset, $whence); + } + + private function seek($offset = 0, $whence = SEEK_SET) + { + if(fseek($this->handle, $offset, $whence) < 0) + { + throw new Exception('Seeking failed'); + } + } + + private function read($length = self::PACKETSIZE, $require = true) + { + if(($data = fread($this->handle, $length)) === false) + { + throw new Exception('Read operation failed'); + } + else if($require && ($c = strlen($data)) < $length) + { + throw new Exception(sprintf('Short read (got only %d bytes)', $c)); + } + + return $data; + } + + private function write($binary, $require = true) + { + if(($length = fwrite($this->handle, $binary)) === false) + { + throw new Exception('Write operation failed'); + } + else if($require && $length < strlen($binary)) + { + throw new Exception(sprintf('Short write (shed only %d bytes)', $length)); + } + } + + // Input: [, ] + // Output: char[4] + private static function encodeFloat($float, $dec = self::DECIMALS) + { + return self::encodeInteger($float * pow(10, $dec)); + } + + // Input: [, ] + // Output: long32_t + private static function decodeFloat($bin, $dec = self::DECIMALS) + { + return (self::decodeInteger($bin) / pow(10, $dec)); + } + + // Input: + // Output: int32_t + private static function encodeInteger($int) + { + return pack('l', $int); + } + + // Input: + // Output: char[4] + private static function decodeInteger($bin) + { + return unpack('l', $bin)[1]; + } + + // Input: [, ] + // array(, ) + // Output: char[2] + private static function encodeFlags($int, $flags = 0) + { + if(is_array($int) && count($int) > 1) + { + $flags = $int[1]; + $int = $int[0]; + } + + return chr($int) . chr($flags); + } + + // Input: + // Output: array(uint8_t, uint8_t) + private static function decodeFlags($bin) + { + $bin .= (strlen($bin) < 2) ? "\x00" : null; + $bin .= (strlen($bin) < 2) ? "\x00" : null; + + return [ord($bin[0]), ord($bin[1])]; + } +} + +?> diff --git a/other-scripts/crontab b/other-scripts/crontab new file mode 100644 index 0000000..5483c70 --- /dev/null +++ b/other-scripts/crontab @@ -0,0 +1 @@ +@reboot ~/rtl_433.sh diff --git a/other-scripts/graphs.sh b/other-scripts/graphs.sh new file mode 100755 index 0000000..11baead --- /dev/null +++ b/other-scripts/graphs.sh @@ -0,0 +1,80 @@ +#!/bin/bash +set -euf -o pipefail + +case ${1:-} in + "day") + START=`date -d '- 1 day 00:00:01' '+%s'` + END=`date -d '- 1 day 23:59:59' '+%s'` + MESSAGE="Der gestrige Tag im Überblick…" + TITLE="(letzter Tag)" + COLOR="0066B3" + ;; + "week") + START=`date -d 'last monday - 1 week 00:00:01' '+%s'` + END=`date -d 'last monday - 1 day 23:59:59' '+%s'` + MESSAGE="Die vergangene Woche im Überblick…" + TITLE="(letzte Woche)" + COLOR="00CC00" + ;; + "month") + START=`date -d "$(date -d "- 1 month" +%Y-%m-01) 00:00:01" '+%s'` + END=`date -d "- $(date +%d) days - 0 month 23:59:59" '+%s'` + MESSAGE="Der letzte Monat im Überblick…" + TITLE="(letzter Monat)" + COLOR="B30000" + ;; + "year") + START=`date -d "$(date -d "- 1 year" +%Y-01-01) 00:00:01" '+%s'` + END=`date -d "$(date -d "- 1 year" +%Y-12-31) 23:59:59" '+%s'` + MESSAGE="Das letzte Jahr im Überblick…" + TITLE="(letztes Jahr)" + COLOR="FF8000" + ;; + *) + echo "Usage: $0 {day|week|month|year}" >&2 + exit 3 + ;; +esac + +WIDTH=720; HEIGHT=409 +ACCOUNT="__EXAMPLE__" +FIELD="Name des Außensensors" +HEADER="Temperaturverlauf $TITLE" +FOOTER="Twitter @${ACCOUNT}" + +TMPFILE=`tempfile` +rrdtool graph "$TMPFILE" --imgformat PNG --width "$WIDTH" --height "$HEIGHT" \ + --start "$START" --end "$END" --title "$HEADER" -W "$FOOTER" --vertical-label '°C' --border 0 \ + --disable-rrdtool-tag --full-size-mode --slope-mode --base 1000 -l -15 -u 45 \ + --font 'DEFAULT:0:DejaVuSans,DejaVu Sans,DejaVu LGC Sans,Bitstream Vera Sans' \ + --font 'LEGEND:7:DejaVuSansMono,DejaVu Sans Mono,DejaVu LGC Sans Mono,Bitstream Vera Sans Mono,monospace' \ + --color 'BACK#F0F0F0' --color 'FRAME#F0F0F0' --color 'CANVAS#FFFFFF' --color 'FONT#666666' --color 'AXIS#CFD6F8' --color 'ARROW#CFD6F8' \ + 'COMMENT:\r' 'COMMENT: ' 'COMMENT:Minimum' 'COMMENT:Durchschnitt' 'COMMENT:Maximum\j' \ + 'DEF:max=/var/lib/munin/CATEGORY/HOSTNAME-PLUGIN-FIELD-g.rrd:42:MAX' \ + 'DEF:min=/var/lib/munin/CATEGORY/HOSTNAME-PLUGIN-FIELD-g.rrd:42:MIN' \ + 'DEF:avg=/var/lib/munin/CATEGORY/HOSTNAME-PLUGIN-FIELD-g.rrd:42:AVERAGE' \ + 'LINE1:0#BEBEBE' "LINE3:avg#${COLOR}:${FIELD}" \ + 'GPRINT:min:MIN:%.2lf%s' \ + 'GPRINT:avg:AVERAGE:%.2lf%s' \ + 'GPRINT:max:MAX:%.2lf%s\j' \ + 'COMMENT:\r' >/dev/null + +twurl set default "$ACCOUNT" +MEDIA_ID=`twurl -H "upload.twitter.com" -X POST "/1.1/media/upload.json" --file "$TMPFILE" --file-field "media" | jq -r '.media_id_string'` + +if [[ ! "$MEDIA_ID" =~ ^[0-9]+$ ]] +then + echo "Failed to upload image!" 1>&2 + exit 1 +fi + +twurl set default "$ACCOUNT" +TWEET_ID=`twurl "/1.1/statuses/update.json" -d "media_ids=${MEDIA_ID}&status=${MESSAGE}" | jq -r '.id'` + +if [[ ! "$TWEET_ID" =~ ^[0-9]+$ ]] +then + echo "Failed to send tweet!" 1>&2 + exit 1 +fi + +rm -f "$TMPFILE" diff --git a/other-scripts/rtl_433.sh b/other-scripts/rtl_433.sh new file mode 100755 index 0000000..7a53f7f --- /dev/null +++ b/other-scripts/rtl_433.sh @@ -0,0 +1,13 @@ +#!/bin/sh +while true +do + # Only an example! Adjust to your needs... + rtl_433 -R 50 -l 1 -g 50 -F json -T 900 -q \ + 2>/dev/null | ism-catcher --live; sleep 10 +done + +# Copy everything to a logfile: (don't forget to setup logrotate if required!) +# rtl_433 […] 2>/dev/null | tee -a ~/path/to/archive.json | ism-catcher --live + +# Copy each line to STDERR for debugging purposes: +# rtl_433 […] | tee >(cat >&2) | ism-catcher --live diff --git a/other-scripts/tweet.sh b/other-scripts/tweet.sh new file mode 100755 index 0000000..b386a70 --- /dev/null +++ b/other-scripts/tweet.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euf -o pipefail +LC_NUMERIC=C + +START=`date -d "$(date -d '- 1 hour' '+%Y-%m-%d %H:00:00')" '+%s'` +END=`date -d "$(date -d '- 1 hour' '+%Y-%m-%d %H:59:59')" '+%s'` + +cd /var/lib/munin/CATEGORY + +max=$(rrdtool graph /dev/null -s "$START" -e "$END" DEF:v1=HOSTNAME-PLUGIN-FIELD-g.rrd:42:MAX PRINT:v1:MAX:%lf | tail -n +2) +min=$(rrdtool graph /dev/null -s "$START" -e "$END" DEF:v1=HOSTNAME-PLUGIN-FIELD-g.rrd:42:MIN PRINT:v1:MIN:%lf | tail -n +2) +avg=$(rrdtool graph /dev/null -s "$START" -e "$END" DEF:v1=HOSTNAME-PLUGIN-FIELD-g.rrd:42:AVERAGE PRINT:v1:AVERAGE:%lf | tail -n +2) + +max=$(printf "%.2f" "$max" | sed 's/\./,/') +min=$(printf "%.2f" "$min" | sed 's/\./,/') +avg=$(printf "%.2f" "$avg" | sed 's/\./,/') + +if [[ "$max" =~ ^-?[0-9]+(,[0-9]+)?$ && "$min" =~ ^-?[0-9]+(,[0-9]+)?$ && "$avg" =~ ^-?[0-9]+(,[0-9]+)?$ ]] +then + ttytter -ssl -keyf="<...>" -status="Temperaturwerte der letzten Stunde: $min / $avg / $max °C (min/avg/max)" &> /dev/null && exit 0 +else + echo "Invalid data from RRD!"; exit 2 +fi diff --git a/plugin-conf.d/ism b/plugin-conf.d/ism new file mode 100644 index 0000000..c12ef7b --- /dev/null +++ b/plugin-conf.d/ism @@ -0,0 +1,11 @@ +[ism] + +### Local user and/or group to execute this plugin +#group +user + +### Recommended: Full path to INI file (if not in same directory) +env.ini_file + +### Optional: Virtual hostname (special Munin setup required!) +#env.hostname