diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | README.md | 18 | ||||
| -rw-r--r-- | docker-compose.yml | 25 | ||||
| -rw-r--r-- | mqtt-client/Dockerfile | 2 | ||||
| m--------- | mqtt-client/TasmotaCLI | 0 | ||||
| -rw-r--r-- | mqtt-client/mqtt-client.py | 107 | ||||
| -rw-r--r-- | mqtt-client/requirements.txt | 1 | ||||
| -rw-r--r-- | prometheus/prometheus.yml | 16 | ||||
| -rw-r--r-- | switch-snmp/.dockerignore | 5 | ||||
| -rw-r--r-- | switch-snmp/Dockerfile | 6 | ||||
| -rw-r--r-- | switch-snmp/mikrotik-switches.conf | 9 | ||||
| -rw-r--r-- | switch-snmp/mikrotik.py | 134 | ||||
| -rw-r--r-- | switch-snmp/omada-switches.conf | 21 | ||||
| -rw-r--r-- | switch-snmp/port-names.conf | 10 | ||||
| -rw-r--r-- | switch-snmp/requirements.txt | 4 | ||||
| -rw-r--r-- | switch-snmp/snmpOmada.py (renamed from switch-snmp/snmp-omada.py) | 58 | ||||
| -rw-r--r-- | switch-snmp/switches.py | 62 | 
18 files changed, 423 insertions, 61 deletions
| @@ -6,6 +6,9 @@ db.env  config.env  influxdb-config/  influxdb-data/ +node-red-data/ +prometheus/web.yml +prometheus/web.yml  # Byte-compiled / optimized / DLL files  __pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6933443 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mqtt-client/TasmotaCLI"] +	path = mqtt-client/TasmotaCLI +	url = git@github.com:jwansek/TasmotaCLI.git @@ -1,13 +1,11 @@ -# power.eda.gay +# SNMP and MQTT Power Logger & Visualizer -Logs Tasmota-flashed power usage monitors, and TP-Link Omada POE switches, to InfluxDB and Grafana using MQTT and SNMP. +Logs Tasmota-flashed power usage monitors, and TP-Link Omada/Mikrotik POE switches, to InfluxDB and Grafana using MQTT, SNMP and prometheus.  Also logs Zigbee informtion with a Tasmota-flashed Zigbee bridge. -Looking for the Mikrotik POE usage monitor/exporter? That's been moved to [MikrotikPOEPowerExporter](https://github.com/jwansek/MikrotikPOEPowerExporter) +Looking for the Mikrotik POE usage monitor/exporter? That's in [`mikrotik.py`](/switch-snmp/mikrotik.py) - - - +  ## Setup @@ -29,3 +27,11 @@ Looking for the Mikrotik POE usage monitor/exporter? That's been moved to [Mikro  You must enable SNMP in the Omada controller with the community string `tplink`:   + +Moreover mikrotik switches must be set up with an appropriate SSH key pair so they can be polled through SSH + +## MQTT setup + +We are using a [Tasmota-flashed zigbee coordinator](https://www.aliexpress.com/item/1005005254486268.html) to transmit zigbee messages to our MQTT castor, and [Tasmota-flashed plugs](https://www.aliexpress.com/item/1005008427641332.htm) for logging power from the wall over MQTT. Both must be configured with an appropriate friendlyname and told access the MQTT castor. + +  diff --git a/docker-compose.yml b/docker-compose.yml index 4100d7f..fe8fd2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services:              - ./config.env          depends_on:              - influxdb +            - pushgateway          restart: unless-stopped      snmp_client: @@ -46,20 +47,42 @@ services:              - ./config.env          depends_on:              - influxdb +            - pushgateway          restart: unless-stopped          volumes: -            - ./switch-snmp/port-names.conf:/app/port-names.conf +            - ./switch-snmp/omada-switches.conf:/app/omada-switches.conf +            - ./switch-snmp/mikrotik-switches.conf:/app/mikrotik-switches.conf +            - ./switch-snmp/mikrotik.pem:/app/mikrotik.pem      grafana:          image: grafana/grafana:8.5.27          ports:              - 3000:3000 +        environment: +            - GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-sankey-panel          depends_on:              - influxdb +            - prometheus          restart: unless-stopped          volumes:              - grafana:/var/lib/grafana +    prometheus: +        image: prom/prometheus +        volumes: +            - ./prometheus/:/etc/prometheus/ +        command: +            - --config.file=/etc/prometheus/prometheus.yml +            - --web.config.file=/etc/prometheus/web.yml +        ports: +            - 9090:9090 +        restart: unless-stopped + +    pushgateway: +        image: prom/pushgateway +        restart: unless-stopped + +  volumes:      mosquitto-data:      mosquitto-logs: diff --git a/mqtt-client/Dockerfile b/mqtt-client/Dockerfile index ad59f9b..12c8a53 100644 --- a/mqtt-client/Dockerfile +++ b/mqtt-client/Dockerfile @@ -6,5 +6,7 @@ RUN apt-get install -y python3-pip iputils-ping  COPY . /app  WORKDIR /app  RUN pip3 install -r requirements.txt +RUN pip3 install docker +RUN pip3 install -r TasmotaCLI/requirements.txt   ENTRYPOINT ["python3"]  CMD ["mqtt-client.py"] diff --git a/mqtt-client/TasmotaCLI b/mqtt-client/TasmotaCLI new file mode 160000 +Subproject dd7790dab8d3fbea8f2b58eb4d5aaffc36b3cb0 diff --git a/mqtt-client/mqtt-client.py b/mqtt-client/mqtt-client.py index a441e6d..d4279e9 100644 --- a/mqtt-client/mqtt-client.py +++ b/mqtt-client/mqtt-client.py @@ -1,25 +1,62 @@  import paho.mqtt.client as paho  from influxdb_client import InfluxDBClient, Point, WritePrecision  from influxdb_client.client.write_api import SYNCHRONOUS +import prometheus_client +import threading +import asyncio +import time  import json +import sys  import os +sys.path.insert(1, os.path.join(os.path.dirname(__file__), "TasmotaCLI")) +import tasmotaMQTTClient +import tasmotaHTTPClient +  class MQTTClient: -    def __init__(self): +    def __init__(self, mqtt_client_name = "reg.reaweb.uk/mqtt-client", loop_forever = True):          self.influxc = InfluxDBClient(              url = "http://%s:8086" % INFLUXDB_HOST,              token = os.environ["DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"],              org = os.environ["DOCKER_INFLUXDB_INIT_ORG"]           )          self.influxc.ping() +        self.tasmota_power_prom = prometheus_client.Gauge( +            "tasmota_power",  +            "Power metrics as reported by Tasmota-flashed plugs",  +            labelnames = ["plug", "field"] +        ) +        self.humidity_prom = prometheus_client.Gauge( +            "humidity", +            "Humidity as reported by a zigbee device over MQTT", +            labelnames = ["location"] +        ) +        self.temperature_prom = prometheus_client.Gauge( +            "temperature", +            "Temperature as reported by a zigbee device over MQTT", +            labelnames = ["location"] +        ) +        self.doorsensor_prom = prometheus_client.Enum( +            "door_sensor", +            "Door sensor state change as reported by zigbee door sensor over MQTT", +            states = ["opened", "closed"], +            labelnames = ["location"] +        ) +        self.door_opened_counter = prometheus_client.Counter( +            "door_opened", +            "Door sensor opened as reported by zigbee door sensor over MQTT", +            labelnames = ["location"] +        ) -        self.mqttc = paho.Client('power-listener', clean_session = True) -        self.mqttc.on_connect = self._on_connect_cb -        self.mqttc.on_message = self._on_message_cb +        self.mqttc = paho.Client(mqtt_client_name, clean_session = True) +        if loop_forever: +            self.mqttc.on_connect = self._on_connect_cb +            self.mqttc.on_message = self._on_message_cb          self.mqttc.username_pw_set(os.environ["MQTT_USER"], password = os.environ["MQTT_PASSWD"])          self.mqttc.connect(MQTT_HOST, 1883, 60) -        self.mqttc.loop_forever() +        if loop_forever: +            self.mqttc.loop_forever()      def _on_connect_cb(self, mqtt, userdata, flags, rc):          #print("Connected to broker") @@ -41,18 +78,69 @@ class MQTTClient:          elif type_ == "TasmotaZigbee":              self.handle_zigbee(msg_j) +      def handle_plug(self, msg_j, location):          print("'%s' is using %.1fw @ %s. %.1fkWh so far today, %.1fkWh yesterday" % (location, msg_j["ENERGY"]["Power"], msg_j["Time"],  msg_j["ENERGY"]["Today"], msg_j["ENERGY"]["Yesterday"]))          fields = {k: v for k, v in msg_j["ENERGY"].items() if k not in {"TotalStartTime"}}          self.append_influxdb(fields, "tasmota_power", {"plug": location}) +        for k, v in fields.items(): +            self.tasmota_power_prom.labels(plug = location, field = k).set(v) +      def handle_zigbee(self, msg_j): +        def toggle_firestick(): +            print("Starting thread...") +            tasmotaMQTTClient.MQTTClient(MQTT_HOST, "TasmotaFirestick", os.environ["MQTT_USER"], os.environ["MQTT_PASSWD"], "TOGGLE") +            #print("Waiting...") +            #time.sleep(8) +            #tasmotaMQTTClient.MQTTClient(MQTT_HOST, "TasmotaFirestick", os.environ["MQTT_USER"], os.environ["MQTT_PASSWD"], "ON") +            print("Toggled firestick.") +          zigbee_id = list(msg_j["ZbReceived"].keys())[0]          fields = msg_j["ZbReceived"][zigbee_id]          friendlyname = fields.pop("Name")          del fields["Device"]          print("Zigbee device '%s' reported: %s" % (friendlyname, str(fields))) -        self.append_influxdb(fields, "zigbee", {"friendlyname": friendlyname, "id": zigbee_id}) + +        if zigbee_id == "0x7327" and friendlyname == "TVButton" and "Power" in fields.keys(): +            if fields["Power"] == 2: +                print("TV Zigbee button pressed, toggling TasmotaTV Tasmota Plug") +                self.toggle_plug("TasmotaTV") +                threading.Thread(target = toggle_firestick, args = ()).start() +                #loop = asyncio.get_event_loop() +                #loop.run_until_complete(tasmotaHTTPClient.main(host = "geoffery.plug", username = "admin", password = os.environ["MQTT_PASSWD"], toggle = True)) +                #time.sleep(8) +                #loop.run_until_complete(tasmotaHTTPClient.main(host = "geoffery.plug", username = "admin", password = os.environ["MQTT_PASSWD"], toggle = True)) + + +        if zigbee_id == "0x74B3" and friendlyname == "HarveyButton" and "Power" in fields.keys(): +            if fields["Power"] == 2: +                print("Harvey's button pressed, toggling TasmotaHarveyPC Plug") +                self.toggle_plug("TasmotaHarveyPC") + +        if "Humidity" in fields.keys(): +            fields["Humidity"] = float(fields["Humidity"]) +            self.humidity_prom.labels(location = friendlyname).set(fields["Humidity"]) +        elif "Temperature" in fields.keys(): +            fields["Temperature"] = float(fields["Temperature"]) +            self.temperature_prom.labels(location = friendlyname).set(fields["Temperature"])  +        elif "ZoneStatus" in fields.keys() and "Contact" in fields.keys(): +            if fields["ZoneStatus"] == 1 and fields["Contact"] == 1: +                self.doorsensor_prom.labels(location = friendlyname).state("opened") +                self.door_opened_counter.labels(location = friendlyname).inc() +            elif fields["ZoneStatus"] == 0 and fields["Contact"] == 0: +                self.doorsensor_prom.labels(location = friendlyname).state("closed") + +        if "Read" not in fields.keys(): +            self.append_influxdb(fields, "zigbee", {"friendlyname": friendlyname, "id": zigbee_id}) + +    def set_plug(self, friendlyname, payload): +        t = "cmnd/TasmotaPlug/%s/Power" % friendlyname +        self.mqttc.publish(t, payload = payload) +        print("Send payload '%s' to %s" % (payload, t)) + +    def toggle_plug(self, friendlyname): +        self.set_plug(friendlyname, "TOGGLE")      def append_influxdb(self, fields, measurement_name, tags):          points = [{"measurement": measurement_name, "tags": tags, "fields": fields}] @@ -69,10 +157,13 @@ if __name__ == "__main__":      if os.path.exists(env_path):          import dotenv          dotenv.load_dotenv(dotenv_path = env_path) -        INFLUXDB_HOST = "localhost" -        MQTT_HOST = "localhost" +        INFLUXDB_HOST = "dns.athome" +        MQTT_HOST = "dns.athome" +        PROM_HOST = "dns.athome"      else:          INFLUXDB_HOST = "influxdb"          MQTT_HOST = "mqtt" +        PROM_HOST = "prometheus" +    prometheus_client.start_http_server(8000)      mqtt_client = MQTTClient() diff --git a/mqtt-client/requirements.txt b/mqtt-client/requirements.txt index ac151c7..3e6f8db 100644 --- a/mqtt-client/requirements.txt +++ b/mqtt-client/requirements.txt @@ -1,3 +1,4 @@  paho-mqtt==1.6.1  python-dotenv  influxdb-client +prometheus-client diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..48bd2f6 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,16 @@ +global: +  scrape_interval: 10s +scrape_configs: + - job_name: prometheus +   static_configs: +    - targets: +       - prometheus:9090 + - job_name: pushgateway +   static_configs: +    - targets: +       - pushgateway:9091 + - job_name: mqtt +   static_configs: +    - targets: +       - mqtt_client:8000 + diff --git a/switch-snmp/.dockerignore b/switch-snmp/.dockerignore index ea6cd72..662471f 100644 --- a/switch-snmp/.dockerignore +++ b/switch-snmp/.dockerignore @@ -1 +1,4 @@ -port-names.conf
\ No newline at end of file +port-names.conf +*.pem +*.pub + diff --git a/switch-snmp/Dockerfile b/switch-snmp/Dockerfile index a056214..4369d61 100644 --- a/switch-snmp/Dockerfile +++ b/switch-snmp/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM reg.reaweb.uk/cron  ENV TZ=Europe/London  RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone  RUN apt-get update -y @@ -11,7 +11,7 @@ RUN unzip -j "privateMibs(20220831).zip" -d  /usr/share/snmp/mibs  RUN pip3 install -r requirements.txt  RUN rm "privateMibs(20220831).zip" -RUN echo "*/1 * * * * root python3 /app/snmp-omada.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab -RUN echo "*/1 * * * * root sh -c 'sleep 30 && python3 /app/snmp-omada.py' > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab +RUN echo "*/1 * * * * root python3 /app/switches.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab +# RUN echo "*/1 * * * * root sh -c 'sleep 30 && python3 /app/switches.py' > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab  ENTRYPOINT ["bash"]  CMD ["entrypoint.sh"] diff --git a/switch-snmp/mikrotik-switches.conf b/switch-snmp/mikrotik-switches.conf new file mode 100644 index 0000000..c1a344c --- /dev/null +++ b/switch-snmp/mikrotik-switches.conf @@ -0,0 +1,9 @@ +[192.168.69.22] +ether2 = MikroTik CSS610-8G-2S+IN +ether1 = TP-RP108GE +ether3 = 3 +ether4 = 4 +ether5 = 5 +ether6 = 6 +ether7 = 7 +ether8 = 8 diff --git a/switch-snmp/mikrotik.py b/switch-snmp/mikrotik.py new file mode 100644 index 0000000..5c3a88a --- /dev/null +++ b/switch-snmp/mikrotik.py @@ -0,0 +1,134 @@ +from dataclasses import dataclass +from paramiko.ssh_exception import NoValidConnectionsError +import configparser +import threading +import fabric +import logging +import socket +import time +import os +import re + +logging.basicConfig( +    format = "%(levelname)s\t[%(asctime)s]\t%(message)s", +    level = logging.INFO, +    handlers=[ +        logging.StreamHandler() +    ] +) + +INFLUXDB_MAPPINGS = { +    "poe-out-voltage": "tpPoeVoltage", +    "poe-out-current": "tpPoeCurrent", +    "poe-out-power": "tpPoePower", +} + +@dataclass +class MikroTikSSHDevice: + +    host: str +    ssh_key_path: str +    user: str = "admin" + +    def __post_init__(self): +        self.is_being_polled = threading.Event() + +    def _get_conn(self): +        return fabric.Connection( +            user = self.user, +            host = self.host, +            connect_kwargs = {"key_filename": self.ssh_key_path}, +            connect_timeout = 5 +        ) + +    def _poll_four_interfaces(self, four_interfaces): +        # only poll four interfaces at the same time since we can only get a certain amount of information through SSH at the same time +        self.is_being_polled.set() +        result = self._get_conn().run("/interface/ethernet/poe/monitor %s once" % ",".join(four_interfaces), hide = True) +        self.is_being_polled.clear() +        return self._parse_result(result) + +    def _parse_result(self, result): +        r = result.stdout +        # print(r) +        s = [re.split(r" +", row.rstrip())[1:] for row in r.split("\r\n")][:-2] +        out = {i: {} for i in s[0][1:]} +        off_interfaces = set() +        for row in s[1:]: +            column_decrimator = 0 +            output_name = row[0][:-1] +            # print(output_name) + +            for i, interface_name in enumerate(out.keys(), 0): +                # print("off_interfaces:", off_interfaces) +                # print(i, interface_name, row[1:][i]) +                if interface_name in off_interfaces: +                    # print("Skipping '%s' for %s..." % (output_name, interface_name)) +                    column_decrimator += 1 +                else: +                    out[interface_name][output_name] = row[1:][i - column_decrimator] + +                if output_name == "poe-out-status": +                    if row[1:][i] != "powered-on": +                        # print("Adding %s to off interfaces" % interface_name) +                        off_interfaces.add(interface_name) +        return out + +    def get_poe_interfaces(self, interface_names): +        out = {} +        for four_interfaces in [interface_names[i:i + 4] for i in range(0, len(interface_names), 4)]: +            out = {**out, **self._poll_four_interfaces(four_interfaces)} +        return out + +def remove_measurement_type(type_str): +    if str(type_str).endswith(".0"): +        return float(type_str) + +    type_str = "".join([s for s in type_str if s.isdigit() or s == "."]) +    if "." in type_str: +        return float(type_str) +    else: +        return int(type_str) + +def fields_to_points(fields, switch_host, config): +    return [{ +        "measurement": "switch_status", +        "tags": {"port": port, "port_name": config.get(switch_host, port), "switch_host": switch_host, "type": "MikroTik"}, +        "fields": {INFLUXDB_MAPPINGS[k]: remove_measurement_type(v) for k, v in values.items() if k in INFLUXDB_MAPPINGS} +    } for port, values in fields.items()] + +def get_points(): +    mikrotik_switches = configparser.ConfigParser() +    mikrotik_switches.read(os.path.join(os.path.dirname(__file__), "mikrotik-switches.conf")) +    points = [] +    for mikrotik_switch in mikrotik_switches.sections(): +        mikrotik_device = MikroTikSSHDevice(mikrotik_switch, os.path.join(os.path.dirname(__file__), "mikrotik.pem")) +        try: +            points += fields_to_points(mikrotik_device.get_poe_interfaces(list(mikrotik_switches[mikrotik_switch].keys())), mikrotik_switch, mikrotik_switches) +        except (NoValidConnectionsError, TimeoutError, socket.timeout) as e: +            logging.error("Could not connect to mikrotik switch @ %s" % mikrotik_switch) +    return points + +def print_points(points): +    for measurement in points: +        if set(INFLUXDB_MAPPINGS.values()) <= set(measurement["fields"].keys()): +            if measurement["fields"]["tpPoePower"] > 0: +                logging.info("Port %s (%s) of %s switch %s is currently using %.1fW (%imA / %.1fV)" % ( +                    str(measurement["tags"]["port"]), +                    measurement["tags"]["port_name"], +                    measurement["tags"]["type"], +                    measurement["tags"]["switch_host"], +                    measurement["fields"]["tpPoePower"], +                    measurement["fields"]["tpPoeCurrent"], +                    measurement["fields"]["tpPoeVoltage"], +                )) + +if __name__ == "__main__": +    if not os.path.exists(os.path.join(os.path.dirname(__file__), "mikrotik-switches.conf")): +        raise FileNotFoundError("Couldn't find mikrotik config file") +    if not os.path.exists(os.path.join(os.path.dirname(__file__), "mikrotik.pem")): +        raise FileNotFoundError("Couldn't find mikrotik public key file") + +    import json +    points = get_points() +    print(json.dumps(points, indent = 4)) diff --git a/switch-snmp/omada-switches.conf b/switch-snmp/omada-switches.conf new file mode 100644 index 0000000..a76ff82 --- /dev/null +++ b/switch-snmp/omada-switches.conf @@ -0,0 +1,21 @@ +[192.168.69.112] +1 = EAP225 Wifi +2 = Tasmota Zigbee +4 = Mikrotik CRS310-8G+2S+ +6 = Routerbox +16 = Intel Compute Stick +24 = Frigate Pi +23 = Modem & ES205G +8 = PiKVM +10 = TL-RP108GE & EAP110 +11 = Type-C POE Charger +22 = Cluster Pi 9 +19 = Cluster Pi 5 +20 = Cluster Pi 7 +21 = Cluster Pi 8 +18 = Cluster Pi 6 +17 = Cluster Pi 4 +9 = Jetson Orin Nano +12 = Netgate SG-1100 + + diff --git a/switch-snmp/port-names.conf b/switch-snmp/port-names.conf deleted file mode 100644 index 8f8d089..0000000 --- a/switch-snmp/port-names.conf +++ /dev/null @@ -1,10 +0,0 @@ -1 = EAP225 Wifi -2 = Tasmota Zigbee -4 = 2.5Gb Switch -6 = Routerbox -13 = Intel Compute Stick -24 = Frigate Pi -23 = Modem & ES205G -8 = PiKVM -10 = TL-RP108GE & EAP110 - diff --git a/switch-snmp/requirements.txt b/switch-snmp/requirements.txt index e7e63fd..aeb5843 100644 --- a/switch-snmp/requirements.txt +++ b/switch-snmp/requirements.txt @@ -1,3 +1,5 @@  python-dotenv  influxdb-client -pandas
\ No newline at end of file +pandas +fabric +prometheus-client
\ No newline at end of file diff --git a/switch-snmp/snmp-omada.py b/switch-snmp/snmpOmada.py index aaa340e..f1546a7 100644 --- a/switch-snmp/snmp-omada.py +++ b/switch-snmp/snmpOmada.py @@ -8,15 +8,13 @@ from dataclasses import dataclass  import dotenv  import os  import pandas +import configparser  from influxdb_client import InfluxDBClient, Point, WritePrecision  from influxdb_client.client.write_api import SYNCHRONOUS  DIVIDE_BY_10_ENDPOINTS = ["tpPoePower", "tpPoeVoltage"] -PORT_NAMES = dotenv.dotenv_values(os.path.join(os.path.dirname(__file__), "port-names.conf")) -PORT_NAMES = {int(k): v for k, v in PORT_NAMES.items()} -  @dataclass  class SNMPReading:      endpoint: str @@ -38,9 +36,13 @@ class SNMPReading:          return cls(endpoint, int(port), reading) -def get_alternate_name(port): +def get_alternate_name(port, host): +    port_names = configparser.ConfigParser() +    port_names.read(os.path.join(os.path.dirname(__file__), "omada-switches.conf")) +    port_names = {int(k): v for k, v in port_names[host].items()} +      try: -        return PORT_NAMES[port] +        return port_names[port]      except KeyError:          return port @@ -64,37 +66,31 @@ def snmp_walk(host):  def readings_to_points(readings, switch_host):      points = []      df = pandas.DataFrame(readings) -    df["port_name"] = df["port"].apply(get_alternate_name) +    df["port_name"] = df["port"].apply(get_alternate_name, args = (switch_host, ))      for p, group_df in df.groupby(["port", "port_name"]):          port, port_name = p          fields = dict(zip(group_df['endpoint'], group_df['reading'])) -        points.append({"measurement": "switch_status", "tags": {"port": port, "port_name": port_name, "switch_host": switch_host}, "fields": fields}) +        points.append({ +            "measurement": "switch_status",  +            "tags": {"port": port, "port_name": port_name, "switch_host": switch_host, "type": "Omada"},  +            "fields": fields +        })      return points -if __name__ == "__main__": -    env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.env") -    if os.path.exists(env_path): -        import dotenv -        dotenv.load_dotenv(dotenv_path = env_path) -        INFLUXDB_HOST = "dns.athome" -    else: -        INFLUXDB_HOST = "influxdb" - -    influxc = InfluxDBClient( -        url = "http://%s:8086" % INFLUXDB_HOST, -        token = os.environ["DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"], -        org = os.environ["DOCKER_INFLUXDB_INIT_ORG"]  -    ) -    influxc.ping() +def get_points(): +    if not os.path.exists(os.path.join(os.path.dirname(__file__), "omada-switches.conf")): +        raise FileNotFoundError("Couldn't find config file") +    switches = configparser.ConfigParser() +    switches.read(os.path.join(os.path.dirname(__file__), "omada-switches.conf")) +    points = [] +    for switch_host in switches.sections(): +        points += readings_to_points(snmp_walk(switch_host), switch_host) +    return points -    for switch_host in os.environ["OMADA_SWITCHES"].split(","): -        points = readings_to_points(snmp_walk(switch_host), switch_host) -        write_api = influxc.write_api(write_options = SYNCHRONOUS) -        write_api.write( -            os.environ["DOCKER_INFLUXDB_INIT_BUCKET"], -            os.environ["DOCKER_INFLUXDB_INIT_ORG"], -            points, -            write_precision = WritePrecision.S -        )
\ No newline at end of file +if __name__ == "__main__": +    import mikrotik +    points = get_points() +    print(points) +    mikrotik.append(points)
\ No newline at end of file diff --git a/switch-snmp/switches.py b/switch-snmp/switches.py new file mode 100644 index 0000000..1bc43c2 --- /dev/null +++ b/switch-snmp/switches.py @@ -0,0 +1,62 @@ +import prometheus_client +import snmpOmada +import mikrotik +import os + +from influxdb_client import InfluxDBClient, Point, WritePrecision +from influxdb_client.client.write_api import SYNCHRONOUS + +def append(points): +    influxc = InfluxDBClient( +        url = "http://%s:8086" % INFLUXDB_HOST, +        token = os.environ["DOCKER_INFLUXDB_INIT_ADMIN_TOKEN"], +        org = os.environ["DOCKER_INFLUXDB_INIT_ORG"]  +    ) +    influxc.ping() + +    for measurement in points: +        for field in measurement["fields"].keys(): +            try: +                float(measurement["fields"][field]) +            except ValueError: +                continue +            else: +                switch_power.labels( +                    field = field, +                    type = measurement["tags"]["type"], +                    port = str(measurement["tags"]["port"]), +                    port_name = measurement["tags"]["port_name"], +                    host = measurement["tags"]["switch_host"] +                ).set(float(measurement["fields"][field])) +                prometheus_client.push_to_gateway("%s:9091" % PUSHGATEWAY_HOST, job = "switchSNMP", registry = registry) + +    write_api = influxc.write_api(write_options = SYNCHRONOUS) +    write_api.write( +        os.environ["DOCKER_INFLUXDB_INIT_BUCKET"], +        os.environ["DOCKER_INFLUXDB_INIT_ORG"], +        points, +        write_precision = WritePrecision.S +    ) + +if __name__ == "__main__": +    env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "config.env") +    if os.path.exists(env_path): +        import dotenv +        dotenv.load_dotenv(dotenv_path = env_path) +        INFLUXDB_HOST = "dns.athome" +        PUSHGATEWAY_HOST = "dns.athome" +    else: +        INFLUXDB_HOST = "influxdb" +        PUSHGATEWAY_HOST = "pushgateway" + +    registry = prometheus_client.CollectorRegistry() +    switch_power = prometheus_client.Gauge( +        "switch_power", +        "POE switch power usage metrics from Omada and Mikrotik switches, using Omada SNMP names", +        labelnames = ["field", "type", "port", "port_name", "host"], +        registry = registry +    ) + +    points = snmpOmada.get_points() + mikrotik.get_points() +    mikrotik.print_points(points) +    append(points) | 
