aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--.gitmodules3
-rw-r--r--README.md18
-rw-r--r--docker-compose.yml25
-rw-r--r--mqtt-client/Dockerfile2
m---------mqtt-client/TasmotaCLI0
-rw-r--r--mqtt-client/mqtt-client.py107
-rw-r--r--mqtt-client/requirements.txt1
-rw-r--r--prometheus/prometheus.yml16
-rw-r--r--switch-snmp/Dockerfile4
-rw-r--r--switch-snmp/mikrotik-switches.conf9
-rw-r--r--switch-snmp/mikrotik.py130
-rw-r--r--switch-snmp/omada-switches.conf18
-rw-r--r--switch-snmp/port-names.conf10
-rw-r--r--switch-snmp/requirements.txt4
-rw-r--r--switch-snmp/snmpOmada.py (renamed from switch-snmp/snmp-omada.py)58
-rw-r--r--switch-snmp/switches.py62
17 files changed, 411 insertions, 59 deletions
diff --git a/.gitignore b/.gitignore
index 7de9703..69b3cef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index 331eb2d..ed2d5ea 100644
--- a/README.md
+++ b/README.md
@@ -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)
-![InfluxDB screenshot](https://pbs.twimg.com/media/F_U75tVXwAA5QfG?format=jpg&name=medium)
-
-![InfluxDB screenshot 2](https://i.imgur.com/B1J0j4O.png)
+![Grafana screenshot](https://i.imgur.com/YcAmIf5.png)
## 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`:
![SNMP](https://i.imgur.com/bWUGPQO.png)
+
+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.
+
+![Zigbee coordinator](https://i.imgur.com/GSyKSgS.png) ![MQTT Configuration](https://i.imgur.com/96q7nmo.png)
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..37ed8cb 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_geoffery():
+ print("Starting thread...")
+ tasmotaMQTTClient.MQTTClient(MQTT_HOST, "TasmotaGeoffery", os.environ["MQTT_USER"], os.environ["MQTT_PASSWD"], "OFF")
+ print("Waiting...")
+ time.sleep(8)
+ tasmotaMQTTClient.MQTTClient(MQTT_HOST, "TasmotaGeoffery", os.environ["MQTT_USER"], os.environ["MQTT_PASSWD"], "ON")
+ print("Toggled again.")
+
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_geoffery, 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/Dockerfile b/switch-snmp/Dockerfile
index a056214..9e12d5e 100644
--- a/switch-snmp/Dockerfile
+++ b/switch-snmp/Dockerfile
@@ -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..b777b53
--- /dev/null
+++ b/switch-snmp/mikrotik-switches.conf
@@ -0,0 +1,9 @@
+[192.168.69.22]
+ether1 = Modem
+ether2 = 2
+ether3 = EAP110 Wifi
+ether4 = Amazon Firestick
+ether5 = 5
+ether6 = 6
+ether7 = 7
+ether8 = 8 \ No newline at end of file
diff --git a/switch-snmp/mikrotik.py b/switch-snmp/mikrotik.py
new file mode 100644
index 0000000..8493675
--- /dev/null
+++ b/switch-snmp/mikrotik.py
@@ -0,0 +1,130 @@
+from dataclasses import dataclass
+from paramiko.ssh_exception import NoValidConnectionsError
+import configparser
+import threading
+import fabric
+import logging
+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}
+ )
+
+ 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):
+ 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 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..3f3f3f0
--- /dev/null
+++ b/switch-snmp/omada-switches.conf
@@ -0,0 +1,18 @@
+[192.168.69.26]
+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
+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
+
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)