diff options
author | jwansek <eddie.atten.ea29@gmail.com> | 2023-11-04 00:03:42 +0000 |
---|---|---|
committer | jwansek <eddie.atten.ea29@gmail.com> | 2023-11-04 00:03:42 +0000 |
commit | a09c97c4182885d3f62735b45ebe76f5a3a9f8dd (patch) | |
tree | 76b1330a71c3ad8237d788c2e6fc78afa29e37c9 | |
parent | 29eb6dd5be90204e9251eef942b7b994ff4d2423 (diff) | |
download | power.eda.gay-a09c97c4182885d3f62735b45ebe76f5a3a9f8dd.tar.gz power.eda.gay-a09c97c4182885d3f62735b45ebe76f5a3a9f8dd.zip |
Changed mikrotik interface to use SSH instead, worked on client side
-rw-r--r-- | .dockerignore | 2 | ||||
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | app.py | 6 | ||||
-rw-r--r-- | app_requirements.txt | 4 | ||||
-rw-r--r-- | cron_Dockerfile | 1 | ||||
-rw-r--r-- | database.py | 5 | ||||
-rw-r--r-- | devices.py | 6 | ||||
-rw-r--r-- | mikrotik.py | 159 | ||||
-rw-r--r-- | static/scripts.js | 42 | ||||
-rw-r--r-- | static/style.css | 12 |
10 files changed, 144 insertions, 95 deletions
diff --git a/.dockerignore b/.dockerignore index df93d7e..9481f42 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ power.env db.env +*.pub +*.pem @@ -1,5 +1,7 @@ power.env db.env +*.pub +*.pem # Byte-compiled / optimized / DLL files __pycache__/ @@ -3,10 +3,11 @@ import mistune import mikrotik import devices import flask +import time import os app = flask.Flask(__name__) -switch = mikrotik.MikroTikSerialDevice() +switch = mikrotik.MikroTikSSHDevice() markdown_renderer = mistune.create_markdown( renderer = mistune.HTMLRenderer(), plugins = ["strikethrough", "table", "url"] @@ -26,12 +27,13 @@ def api_get_mikrotik_devices(): @app.route("/api/mikrotik_interface/<interface>") def api_poll_mikrotik_interface(interface): + # time.sleep(0.25) try: return flask.jsonify( { "interface": interface, "description": switch.interfaces[interface], - "poe_status": switch.get_poe_info(interface) + "poe_status": switch.get_interface_poe(interface) } ) except (IndexError, KeyError): diff --git a/app_requirements.txt b/app_requirements.txt index df52348..1a676fc 100644 --- a/app_requirements.txt +++ b/app_requirements.txt @@ -1,2 +1,4 @@ flask -mistune
\ No newline at end of file +mistune +fabric + diff --git a/cron_Dockerfile b/cron_Dockerfile index ad6ea6a..ed4c9d1 100644 --- a/cron_Dockerfile +++ b/cron_Dockerfile @@ -11,6 +11,7 @@ RUN touch .docker RUN pip3 install -r cron_requirements.txt
RUN echo "*/1 * * * * root python3 /app/devices.py nothourly > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab
+RUN echo "*/1 * * * * root ( sleep 30; python3 /app/devices.py nothourly > /proc/1/fd/1 2>/proc/1/fd/2 )" >> /etc/crontab
RUN echo "@daily root python3 /app/devices.py daily > /proc/1/fd/1 2>/proc/1/fd/2" >> /etc/crontab
ENTRYPOINT ["bash"]
CMD ["entrypoint.sh"]
\ No newline at end of file diff --git a/database.py b/database.py index 8b57d25..4782087 100644 --- a/database.py +++ b/database.py @@ -163,9 +163,6 @@ if __name__ == "__main__": if not os.path.exists(".docker"):
import dotenv
dotenv.load_dotenv(dotenv_path = "power.env")
- host = "srv.athome"
- else:
- host = None
- with PowerDatabase(host = host) as db:
+ with PowerDatabase() as db:
print(db.get_last_plug_readings())
@@ -9,9 +9,7 @@ import os if not os.path.exists(os.path.join("/app", ".docker")): import dotenv dotenv.load_dotenv(dotenv_path = "power.env") - HOST = "srv.athome" -else: - HOST = None +HOST = None async def get_energy_for(host, username = None, password = None): device = await tasmotadevicecontroller.TasmotaDevice().connect(host, username, password) @@ -53,7 +51,7 @@ def poll_watt_all(): def poll_kwh_all(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - with database.PowerDatabase(host = HOST) as db: + with database.PowerDatabase() as db: for host, username, password, description in db.get_tasmota_devices(): while True: try: diff --git a/mikrotik.py b/mikrotik.py index b739132..42a6eeb 100644 --- a/mikrotik.py +++ b/mikrotik.py @@ -1,98 +1,99 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass import threading -import serial -import devices -import time +import fabric import os import re @dataclass -class MikroTikSerialDevice: - """This is a horrible, horrible way of doing this - pretty much anything else would be better, for example connecting - over SSH instead of serial - - Even using a serial connection like this is an abomination - Please seriously do not do this, this is some necromancy, like it doesn't - log out of the serial connection properly so make sure nothing else is plugged - into the switch serial port - - I am doing it this way because I do not understand mikrotik scripting - """ - device: str = os.environ["MIKROTIK_DEVICE"] - user: str = os.environ["MIKROTIK_USER"] - passwd: str = os.environ["MIKROTIK_PASS"] +class MikroTikSSHDevice: def __post_init__(self): self.interfaces = {} - self.last_return = {} for i in os.environ["MIKROTIK_INTERFACES"].split(";"): self.interfaces.__setitem__(*i.split(",")) self.is_being_polled = threading.Event() - self.poe_cache = {interface: {} for interface in self.interfaces} - - def get_poe_info(self, interface): - # fetch from cache so that multiple processes don't try to access serial at the same time - # this means that the same MikroTikSerialDevice object must be used for multiple threads - # if another thread is accessing the critical region, return from cache - if self.is_being_polled.is_set(): - fetched_cache = self.poe_cache[interface] - fetched_cache["cached"] = True - return fetched_cache - + self.interface_groups_cache = {} + + self.interface_groups = [] + temp = [] + for i, interface_name in enumerate(self.interfaces.keys(), 1): + temp.append(interface_name) + if i % 4 == 0: + self.interface_groups.append(tuple(temp)) + temp = [] + + # make sure we have some cache + # also use as sanity-test + for interface_group in self.interface_groups: + self._poll_interface_group(interface_group) + + def _get_conn(self): + return fabric.Connection( + user = os.environ["MIKROTIK_USER"], + host = os.environ["MIKROTIK_DEVICE"], + connect_kwargs = {"key_filename": os.environ["MIKROTIK_KEY_PATH"]} + ) + + def _get_interfacegroup_containing(self, interface_name): + for interface_group in self.interface_groups: + if interface_name in interface_group: + return interface_group + + def _poll_interface_group(self, interface_group): self.is_being_polled.set() - self.ser = serial.Serial(self.device, int(os.environ["MIKROTIK_BAUD"]), timeout=0.25) - - if self.last_return == {}: - self._push_serial("") - self._push_serial(self.user) - self._push_serial(self.passwd) - self._push_serial("/interface/ethernet/poe/monitor %s" % interface) - time.sleep(0.05) - self.ser.write(bytes("q", 'ISO-8859-1')) - out = self._read() - self.ser.close() + result = self._get_conn().run("/interface/ethernet/poe/monitor %s once" % ",".join(interface_group), hide = True) self.is_being_polled.clear() - - return self._post_out(out, interface) - - def _push_serial(self, text): - time.sleep(0.05) - self.ser.write(bytes(text + "\r\n", 'ISO-8859-1')) - time.sleep(0.05) - - def _read(self): - return self.ser.readlines() - - def _post_out(self, out, interface, was_cached = False): - d = {} - for line in out: - line = line.decode().strip() - # print("line:", line) - if line.startswith("poe"): - d.__setitem__(*line.split(": ")) - - # also fetch from cache if it returned nothing - if d == {}: - fetched_cache = self.poe_cache[interface] - fetched_cache["cached"] = True - return fetched_cache - - self.last_return = d - self.poe_cache[interface] = d - d["cached"] = was_cached - return d - - - + parsed_result = self._parse_result(result) + self.interface_groups_cache[interface_group] = parsed_result + # print("Cached group:", interface_group) + return parsed_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 + + # i refuse to use async programming + def get_interface_poe(self, interface_name): + interface_group = self._get_interfacegroup_containing(interface_name) + if self.is_being_polled.is_set(): + result = self.interface_groups_cache[interface_group][interface_name] + result["cached"] = True + else: + result = self._poll_interface_group(interface_group)[interface_name] + result["cached"] = False + + return result if __name__ == "__main__": if not os.path.exists(os.path.join("/app", ".docker")): import dotenv dotenv.load_dotenv(dotenv_path = "power.env") - mikrotik = MikroTikSerialDevice() - for i in range(10): - for interface in mikrotik.interfaces: - print(interface, mikrotik.get_poe_info(interface)) - + import time + mikrotik = MikroTikSSHDevice() + print("Ready.") + for interface_name in mikrotik.interfaces.keys(): + threading.Thread(target = lambda i: print(i, mikrotik.get_interface_poe(i)), args = (interface_name, )).start() + time.sleep(1)
\ No newline at end of file diff --git a/static/scripts.js b/static/scripts.js index cb43003..10facc4 100644 --- a/static/scripts.js +++ b/static/scripts.js @@ -7,23 +7,51 @@ $(document).ready(function() { fetch("/api/mikrotik_devices").then((resp) => { resp.json().then((body) => { - Object.keys(body).forEach((interface, i) => { + Object.keys(body).reverse().forEach((interface, i) => { let tr_elem = document.createElement("tr"); tr_elem.classList.add("mikrotik_tr") tr_elem.id = "mikrotik_tr_" + interface; // console.log(interface, body[interface]); parent_elem.parentNode.insertBefore(tr_elem, parent_elem.nextSibling); - }) + + for (let i = 0; i <= 4; i++) { + let td_elem = document.createElement("td"); + if (i === 0) { + td_elem.innerHTML = interface; + td_elem.classList.add("mikrotik_interface_name") + } else if (i === 1) { + td_elem.innerHTML = body[interface]; + } else if (i === 2) { + td_elem.id = "mikrotik_td_" + interface + "_watts_now"; + } else if (i === 3) { + td_elem.id = "mikrotik_td_" + interface + "_watts_yesterday"; + } + tr_elem.appendChild(td_elem); + } + }); + + get_mikrotik_table(Object.keys(body)); }); }); - - // parent_elem.parentNode.insertBefore(document.createElement("tr"), parent_elem.nextSibling); }); }); get_main_table(); }) +function get_mikrotik_table(interfaces) { + interfaces.forEach((interface) => { + fetch("/api/mikrotik_interface/" + interface).then((resp) => { + resp.json().then((body) => { + console.log(body["poe_status"]); + // TODO: Add a delay if it was cached + }); + }); + }); + + setTimeout(function() {get_mikrotik_table(interfaces);}, 1000) +} + function get_main_table() { fetch("/api/plugs").then((resp) => { resp.json().then((body) => { @@ -47,5 +75,9 @@ function get_main_table() { }); }); - setTimeout(get_main_table, 30000); + setTimeout(get_main_table, 10000); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); }
\ No newline at end of file diff --git a/static/style.css b/static/style.css index 56627aa..0ab4e82 100644 --- a/static/style.css +++ b/static/style.css @@ -112,6 +112,18 @@ footer { background-color: gainsboro; } +.mikrotik_tr { + font-size: x-small; + padding-bottom: 1em; + padding-top: 1em; + border-spacing:0 5px; +} + +.mikrotik_interface_name { + text-align: right; + padding-right: 15px; +} + @media screen and (max-width: 1200px) { #multicharts ul li { width: 100%; |