Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / luftdaten / feinstaubsensor @ b02548cc

Historique | Voir | Annoter | Télécharger (8,31 ko)

1
#!/usr/bin/env python3
2
"""
3

    
4
=head1 NAME
5

    
6
feinstaubsensor - Plugin to monitor one or more environmental sensors
7

    
8

    
9
=head1 APPLICABLE SYSTEMS
10

    
11
The "Feinstaubsensor" was developed by the OK Lab Stuttgart and is part of the
12
Citizen Science Project "luftdaten.info" (http://luftdaten.info).
13

    
14
Data is retrieved via HTTP requests from the sensors itself.
15

    
16

    
17
=head1 CONFIGURATION
18

    
19
Place a configuration entry somewhere below /etc/munin/plugin-conf.d/:
20

    
21
      [feinstaubsensor]
22
      env.sensor_hosts foo=192.168.1.4 [fe80::1:2:3:4%eth0] bar=sensor2.lan
23

    
24
The <sensor_hosts> environment variable is a space separated list of <token>.
25
Each <token> can be either a <host> or a combination of label and <host> (separated by the
26
character "=").
27
A <host> may be an IPv4 address, an IPv6 address (enclosed in square brackets) or a name to be
28
resolved via DNS.
29

    
30
Examples for <token>:
31

    
32
=over 4
33

    
34
=item 192.168.1.4
35

    
36
=item foo=192.168.1.4
37

    
38
=item [fe80::1a:2b:3c:cafe]
39

    
40
=item bar=[fe80::1a:2b:3c:cafe]
41

    
42
=item feinstaubsensor-12345.local
43

    
44
=item baz=feinstaubsensor-12345.local
45

    
46
=back
47

    
48

    
49
=head1 AUTHOR
50

    
51
Lars Kruse <devel@sumpfralle.de>
52

    
53

    
54
=head1 LICENSE
55

    
56
GNU General Public License v3.0 or later
57

    
58
SPDX-License-Identifier: GPL-3.0-or-later
59

    
60

    
61
=head1 MAGIC MARKERS
62

    
63
  #%# family=manual
64

    
65
=cut
66
"""
67

    
68
import collections
69
import functools
70
import json
71
import os
72
import re
73
import sys
74
import urllib.request
75

    
76

    
77
graphs = [
78
    {
79
        "name": "wireless_signal",
80
        "graph_title": "Feinstaub Wifi Signal",
81
        "graph_vlabel": "%",
82
        "graph_args": "-l 0",
83
        "graph_info": "Wifi signal strength",
84
        "fields": [
85
            {"api_key": "signal"},
86
        ],
87
        "value_type": "GAUGE",
88
    }, {
89
        "name": "feinstaub_samples",
90
        "graph_title": "Feinstaub Sample Count",
91
        "graph_vlabel": "#",
92
        "graph_info": "Number of samples since bootup",
93
        "fields": [
94
            {"api_key": "samples"},
95
        ],
96
        "value_type": "DERIVE",
97
    }, {
98
        "name": "feinstaub_humidity",
99
        "graph_title": "Feinstaub Humidity",
100
        "graph_vlabel": "% humidity",
101
        "graph_info": "Weather information: air humidity",
102
        "fields": [
103
            {"api_key": "humidity"},
104
            {"api_key": "BME280_humidity", "info": "BME280", "field_suffix": "bme280"},
105
        ],
106
        "value_type": "GAUGE",
107
    }, {
108
        "name": "feinstaub_temperature",
109
        "graph_title": "Feinstaub Temperature",
110
        "graph_vlabel": "°C",
111
        "graph_info": "Weather information: temperature",
112
        "fields": [
113
            {"api_key": "temperature"},
114
            {"api_key": "BME280_temperature", "info": "BME280", "field_suffix": "bme280"},
115
        ],
116
        "value_type": "GAUGE",
117
    }, {
118
        "name": "feinstaub_pressure",
119
        "graph_title": "Feinstaub Atmospheric Pressure",
120
        "graph_vlabel": "Pascal",
121
        "graph_info": "Weather information: atmospheric pressure",
122
        "fields": [
123
            {"api_key": "BME280_pressure"},
124
        ],
125
        "value_type": "GAUGE",
126
    }, {
127
        "name": "feinstaub_particles_pm10",
128
        "graph_title": "Feinstaub Particle Measurement P10",
129
        "graph_vlabel": "µg / m³",
130
        "graph_info": "Concentration of particles with a size between 2.5µm and 10µm",
131
        "fields": [
132
            {"api_key": "SDS_P1"},
133
        ],
134
        "value_type": "GAUGE",
135
    }, {
136
        "name": "feinstaub_particles_pm2_5",
137
        "graph_title": "Feinstaub Particle Measurement P2.5",
138
        "graph_vlabel": "µg / m³",
139
        "graph_info": "Concentration of particles with a size up to 2.5µm",
140
        "fields": [
141
            {"api_key": "SDS_P2"},
142
        ],
143
        "value_type": "GAUGE",
144
    }]
145

    
146

    
147
SensorHost = collections.namedtuple("SensorHost", ("host", "label", "fieldname"))
148

    
149

    
150
def clean_fieldname(text):
151
    if text == "root":
152
        # "root" is a magic (forbidden) word
153
        return "_root"
154
    else:
155
        return re.sub(r"(^[^A-Za-z_]|[^A-Za-z0-9_])", "_", text)
156

    
157

    
158
def parse_sensor_hosts_from_description(hosts_description):
159
    """ parse sensor list from the environment variable 'sensor_hosts' and retrieve their data """
160
    sensors = []
161
    for token in hosts_description.split():
162
        if "=" in token:
163
            label, host = token.strip().split("=", 1)
164
        else:
165
            host = token.strip()
166
            label = host
167
        fieldname = clean_fieldname("value_" + host)
168
        sensors.append(SensorHost(host, label, fieldname))
169
    sensors.sort(key=lambda item: item.fieldname)
170
    return sensors
171

    
172

    
173
@functools.lru_cache()
174
def get_sensor_data(host):
175
    """ request the data from a sensor and return a dict (value_type -> value)
176

    
177
    The result is cached - thus we do not need to take care for efficiency.
178

    
179
    Example dataset returned by the sensor:
180
        {"software_version": "NRZ-2017-099", "age":"88", "sensordatavalues":[
181
        {"value_type":"SDS_P1","value":"27.37"},{"value_type":"SDS_P2","value":"13.53"},
182
        {"value_type":"temperature","value":"23.70"},{"value_type":"humidity","value":"69.20"},
183
        {"value_type":"BME280_temperature","value":"9.76"},
184
        {"value_type":"BME280_humidity","value":"79.29"},
185
        {"value_type":"BME280_pressure","value":"100781.03"},
186
        {"value_type":"samples","value":"626964"},{"value_type":"min_micro","value":"225"},
187
        {"value_type":"max_micro","value":"887641"},{"value_type":"signal","value":"-47"}]}
188

    
189
    """
190
    try:
191
        with urllib.request.urlopen("http://{}/data.json".format(host)) as request:
192
            body = request.read()
193
    except IOError as exc:
194
        print("Failed to retrieve data from '{}': {}".format(host, exc), file=sys.stderr)
195
        return None
196
    try:
197
        data = json.loads(body.decode("utf-8"))
198
    except ValueError as exc:
199
        print("Failed to parse data from '{}': {}".format(host, exc), file=sys.stderr)
200
        return None
201
    return {item["value_type"]: item["value"] for item in data["sensordatavalues"]}
202

    
203

    
204
def print_graph_section(graph_description, hosts, include_config, include_values):
205
    # retrieve the data and omit ony output, if the relevant key is missing
206
    results = []
207
    for host_info in hosts:
208
        data = get_sensor_data(host_info.host)
209
        if data:
210
            for data_field in graph_description["fields"]:
211
                for dataset in data["sensordatavalues"]:
212
                    if dataset["value_type"] == data_field["api_key"]:
213
                        results.append((host_info, data_field, dataset["value"]))
214
                        break
215
                else:
216
                    # We cannot distinguish between fields that are not supported by the sensor
217
                    # (most are optional) and missing data. Thus we cannot handle online/offline
218
                    # sensor data fields, too.
219
                    pass
220
    # skip this multigraph, if no host contained this data field
221
    if results:
222
        print("multigraph {}".format(graph_description["name"]))
223
        if include_config:
224
            # graph configuration
225
            print("graph_category sensors")
226
            for key in ("graph_title", "graph_vlabel", "graph_args", "graph_info"):
227
                if key in graph_description:
228
                    print("{} {}".format(key, graph_description[key]))
229
            for host_info, data_field, value in results:
230
                try:
231
                    label = "{} ({})".format(host_info.label, data_field["info"])
232
                except KeyError:
233
                    label = host_info.label
234
                print("{}.label {}".format(host_info.fieldname, label))
235
                print("{}.type {}".format(host_info.fieldname, graph_description["value_type"]))
236
        if include_values:
237
            for host_info, data_field, value in results:
238
                print("{}.value {}".format(host_info.fieldname, value))
239
        print()
240

    
241

    
242
action = sys.argv[1] if (len(sys.argv) > 1) else ""
243
sensor_hosts = parse_sensor_hosts_from_description(os.getenv("sensor_hosts", ""))
244
if not sensor_hosts:
245
    print("ERROR: undefined or empty environment variable 'sensor_hosts'.", file=sys.stderr)
246
    sys.exit(1)
247

    
248

    
249
if action == "config":
250
    is_dirty_config = (os.getenv("MUNIN_CAP_DIRTYCONFIG") == "1")
251
    for graph in graphs:
252
        print_graph_section(graph, sensor_hosts, True, is_dirty_config)
253
elif action == "":
254
    for graph in graphs:
255
        print_graph_section(graph, sensor_hosts, False, True)
256
else:
257
    print("ERROR: unsupported action requested ('{}')".format(action), file=sys.stderr)
258
    sys.exit(2)