aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjwansek <eddie.atten.ea29@gmail.com>2023-11-04 00:03:42 +0000
committerjwansek <eddie.atten.ea29@gmail.com>2023-11-04 00:03:42 +0000
commita09c97c4182885d3f62735b45ebe76f5a3a9f8dd (patch)
tree76b1330a71c3ad8237d788c2e6fc78afa29e37c9
parent29eb6dd5be90204e9251eef942b7b994ff4d2423 (diff)
downloadpower.eda.gay-a09c97c4182885d3f62735b45ebe76f5a3a9f8dd.tar.gz
power.eda.gay-a09c97c4182885d3f62735b45ebe76f5a3a9f8dd.zip
Changed mikrotik interface to use SSH instead, worked on client side
-rw-r--r--.dockerignore2
-rw-r--r--.gitignore2
-rw-r--r--app.py6
-rw-r--r--app_requirements.txt4
-rw-r--r--cron_Dockerfile1
-rw-r--r--database.py5
-rw-r--r--devices.py6
-rw-r--r--mikrotik.py159
-rw-r--r--static/scripts.js42
-rw-r--r--static/style.css12
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
diff --git a/.gitignore b/.gitignore
index 771dea5..3de3c7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
power.env
db.env
+*.pub
+*.pem
# Byte-compiled / optimized / DLL files
__pycache__/
diff --git a/app.py b/app.py
index c4db499..01c49da 100644
--- a/app.py
+++ b/app.py
@@ -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())
diff --git a/devices.py b/devices.py
index 4193780..8ec9261 100644
--- a/devices.py
+++ b/devices.py
@@ -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%;