tomaae.homeassistant-mikrot.../custom_components/mikrotik_router/mikrotik_controller.py

843 lines
31 KiB
Python
Raw Normal View History

"""Mikrotik Controller for Mikrotik Router."""
2019-12-12 23:01:57 +01:00
import asyncio
import logging
2020-03-21 19:02:28 +03:00
from datetime import timedelta
import ipaddress
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from .const import (
2019-12-02 18:13:55 +01:00
DOMAIN,
CONF_TRACK_ARP,
DEFAULT_TRACK_ARP,
CONF_SCAN_INTERVAL,
CONF_UNIT_OF_MEASUREMENT,
2019-12-02 18:13:55 +01:00
DEFAULT_SCAN_INTERVAL,
DEFAULT_TRAFFIC_TYPE,
)
2019-12-12 23:01:57 +01:00
from .exceptions import ApiEntryNotFound
2020-03-21 19:02:28 +03:00
from .helper import from_entry, parse_api
from .mikrotikapi import MikrotikAPI
2019-12-02 17:59:49 +01:00
_LOGGER = logging.getLogger(__name__)
2019-12-02 17:59:49 +01:00
# ---------------------------
# MikrotikControllerData
2019-12-02 17:59:49 +01:00
# ---------------------------
2020-03-16 04:51:41 +01:00
class MikrotikControllerData:
"""MikrotikController Class"""
2020-03-16 04:51:41 +01:00
def __init__(
self,
hass,
config_entry,
name,
host,
port,
username,
password,
use_ssl,
traffic_type,
track_accounting,
2020-03-16 04:51:41 +01:00
):
"""Initialize MikrotikController."""
2019-12-02 18:13:55 +01:00
self.name = name
self.hass = hass
self.config_entry = config_entry
self.traffic_type = traffic_type
self.track_accounting = track_accounting
2020-03-16 04:51:41 +01:00
self.data = {
"routerboard": {},
"resource": {},
"interface": {},
"arp": {},
"nat": {},
"fw-update": {},
"script": {},
"queue": {},
"accounting": {}
2020-03-16 04:51:41 +01:00
}
self.local_dhcp_networks = []
2019-12-02 18:13:55 +01:00
self.listeners = []
2019-12-12 23:01:57 +01:00
self.lock = asyncio.Lock()
2019-12-02 18:13:55 +01:00
self.api = MikrotikAPI(host, username, password, port, use_ssl)
2020-03-16 04:51:41 +01:00
async_track_time_interval(
self.hass, self.force_update, self.option_scan_interval
)
async_track_time_interval(
self.hass, self.force_fwupdate_check, timedelta(hours=1)
)
self.account_local_traffic = False
if self.track_accounting:
async_track_time_interval(
self.hass, self.force_accounting_hosts_update, timedelta(minutes=15)
)
self.account_local_traffic = self.api.is_accounting_local_traffic_enabled()
def _get_traffic_type_and_div(self):
traffic_type = self.option_traffic_type
if traffic_type == "Kbps":
traffic_div = 0.001
elif traffic_type == "Mbps":
traffic_div = 0.000001
elif traffic_type == "B/s":
traffic_div = 0.125
elif traffic_type == "KB/s":
traffic_div = 0.000125
elif traffic_type == "MB/s":
traffic_div = 0.000000125
else:
traffic_type = "bps"
traffic_div = 1
return traffic_type, traffic_div
2019-12-02 19:23:09 +01:00
# ---------------------------
# force_update
# ---------------------------
@callback
2019-12-05 22:10:42 +01:00
async def force_update(self, _now=None):
"""Trigger update by timer"""
2019-12-02 18:13:55 +01:00
await self.async_update()
# ---------------------------
# force_accounting_hosts_update
# ---------------------------
@callback
async def force_accounting_hosts_update(self, _now=None):
"""Trigger update by timer"""
await self.async_accounting_hosts_update()
# ---------------------------
# force_fwupdate_check
# ---------------------------
@callback
2019-12-05 22:10:42 +01:00
async def force_fwupdate_check(self, _now=None):
"""Trigger hourly update by timer"""
await self.async_fwupdate_check()
2019-12-02 18:13:55 +01:00
# ---------------------------
# option_track_arp
# ---------------------------
@property
def option_track_arp(self):
"""Config entry option to not track ARP."""
return self.config_entry.options.get(CONF_TRACK_ARP, DEFAULT_TRACK_ARP)
2019-12-02 18:13:55 +01:00
# ---------------------------
# option_scan_interval
# ---------------------------
@property
def option_scan_interval(self):
"""Config entry option scan interval."""
2020-03-16 04:51:41 +01:00
scan_interval = self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
2019-12-02 18:13:55 +01:00
return timedelta(seconds=scan_interval)
# ---------------------------
# option_traffic_type
# ---------------------------
@property
def option_traffic_type(self):
"""Config entry option to not track ARP."""
2020-03-16 04:51:41 +01:00
return self.config_entry.options.get(
CONF_UNIT_OF_MEASUREMENT, DEFAULT_TRAFFIC_TYPE
)
2019-12-02 18:13:55 +01:00
# ---------------------------
# signal_update
# ---------------------------
@property
def signal_update(self):
"""Event to signal new data."""
return f"{DOMAIN}-update-{self.name}"
2019-12-02 18:13:55 +01:00
# ---------------------------
# connected
# ---------------------------
def connected(self):
"""Return connected state"""
2019-12-02 18:13:55 +01:00
return self.api.connected()
2019-12-02 18:13:55 +01:00
# ---------------------------
# hwinfo_update
# ---------------------------
async def hwinfo_update(self):
"""Update Mikrotik hardware info"""
2019-12-12 23:01:57 +01:00
try:
await asyncio.wait_for(self.lock.acquire(), timeout=10)
except:
return
await self.hass.async_add_executor_job(self.get_system_routerboard)
await self.hass.async_add_executor_job(self.get_system_resource)
self.lock.release()
# ---------------------------
# async_accounting_hosts_update
# ---------------------------
async def async_accounting_hosts_update(self):
"""Update Mikrotik accounting hosts"""
try:
await asyncio.wait_for(self.lock.acquire(), timeout=10)
except:
return
await self.hass.async_add_executor_job(self.build_accounting_hosts)
self.lock.release()
2019-12-04 16:09:30 +01:00
# ---------------------------
# async_fwupdate_check
# ---------------------------
async def async_fwupdate_check(self):
"""Update Mikrotik data"""
2019-12-12 23:01:57 +01:00
await self.hass.async_add_executor_job(self.get_firmware_update)
2019-12-04 16:09:30 +01:00
async_dispatcher_send(self.hass, self.signal_update)
2019-12-02 18:13:55 +01:00
# ---------------------------
# async_update
# ---------------------------
async def async_update(self):
"""Update Mikrotik data"""
2019-12-12 23:01:57 +01:00
try:
await asyncio.wait_for(self.lock.acquire(), timeout=10)
except:
return
2020-03-16 04:51:41 +01:00
if "available" not in self.data["fw-update"]:
2019-12-04 16:09:30 +01:00
await self.async_fwupdate_check()
2019-12-12 23:01:57 +01:00
await self.hass.async_add_executor_job(self.get_interface)
await self.hass.async_add_executor_job(self.get_interface_traffic)
await self.hass.async_add_executor_job(self.get_interface_client)
await self.hass.async_add_executor_job(self.get_nat)
await self.hass.async_add_executor_job(self.get_system_resource)
await self.hass.async_add_executor_job(self.get_script)
await self.hass.async_add_executor_job(self.get_queue)
if self.track_accounting:
await self.hass.async_add_executor_job(self.get_accounting)
2019-12-02 18:13:55 +01:00
async_dispatcher_send(self.hass, self.signal_update)
2019-12-12 23:01:57 +01:00
self.lock.release()
2019-12-02 18:13:55 +01:00
# ---------------------------
# async_reset
# ---------------------------
async def async_reset(self):
"""Reset dispatchers"""
2019-12-02 18:13:55 +01:00
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
2019-12-02 18:13:55 +01:00
self.listeners = []
return True
2019-12-03 01:47:38 +01:00
# ---------------------------
2019-12-04 20:13:11 +01:00
# set_value
2019-12-03 01:47:38 +01:00
# ---------------------------
def set_value(self, path, param, value, mod_param, mod_value):
"""Change value using Mikrotik API"""
2019-12-03 01:47:38 +01:00
return self.api.update(path, param, value, mod_param, mod_value)
2019-12-04 20:13:11 +01:00
# ---------------------------
# run_script
# ---------------------------
def run_script(self, name):
"""Run script using Mikrotik API"""
2019-12-12 23:01:57 +01:00
try:
self.api.run_script(name)
except ApiEntryNotFound as error:
_LOGGER.error("Failed to run script: %s", error)
2019-12-02 18:13:55 +01:00
# ---------------------------
# get_interface
2019-12-02 18:13:55 +01:00
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_interface(self):
"""Get all interfaces data from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["interface"] = parse_api(
data=self.data["interface"],
source=self.api.path("/interface", return_list=True),
2020-03-16 04:51:41 +01:00
key="default-name",
vals=[
2020-03-16 04:51:41 +01:00
{"name": "default-name"},
{"name": "name", "default_val": "default-name"},
{"name": "type", "default": "unknown"},
{"name": "running", "type": "bool"},
{
"name": "enabled",
"source": "disabled",
"type": "bool",
"reverse": True,
},
{"name": "port-mac-address", "source": "mac-address"},
{"name": "comment"},
{"name": "last-link-down-time"},
{"name": "last-link-up-time"},
{"name": "link-downs"},
{"name": "tx-queue-drop"},
{"name": "actual-mtu"},
],
ensure_vals=[
2020-03-16 04:51:41 +01:00
{"name": "client-ip-address"},
{"name": "client-mac-address"},
{"name": "rx-bits-per-second", "default": 0},
{"name": "tx-bits-per-second", "default": 0},
],
)
# ---------------------------
# get_interface_traffic
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_interface_traffic(self):
"""Get traffic for all interfaces from Mikrotik"""
interface_list = ""
2020-03-16 04:51:41 +01:00
for uid in self.data["interface"]:
interface_list += self.data["interface"][uid]["name"] + ","
2019-12-11 15:34:35 +01:00
interface_list = interface_list[:-1]
2020-03-16 04:51:41 +01:00
self.data["interface"] = parse_api(
data=self.data["interface"],
2019-12-12 23:01:57 +01:00
source=self.api.get_traffic(interface_list),
2020-03-16 04:51:41 +01:00
key_search="name",
vals=[
2020-03-16 04:51:41 +01:00
{"name": "rx-bits-per-second", "default": 0},
{"name": "tx-bits-per-second", "default": 0},
],
)
traffic_type, traffic_div = self._get_traffic_type_and_div()
2020-03-16 04:51:41 +01:00
for uid in self.data["interface"]:
2020-03-21 19:02:28 +03:00
self.data["interface"][uid][
"rx-bits-per-second-attr"] = traffic_type
self.data["interface"][uid][
"tx-bits-per-second-attr"] = traffic_type
2020-03-16 04:51:41 +01:00
self.data["interface"][uid]["rx-bits-per-second"] = round(
self.data["interface"][uid]["rx-bits-per-second"] * traffic_div
2020-03-16 04:51:41 +01:00
)
self.data["interface"][uid]["tx-bits-per-second"] = round(
self.data["interface"][uid]["tx-bits-per-second"] * traffic_div
2020-03-16 04:51:41 +01:00
)
2020-03-11 23:25:51 +01:00
2019-12-02 18:13:55 +01:00
# ---------------------------
2019-12-05 22:29:25 +01:00
# get_interface_client
2019-12-02 18:13:55 +01:00
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_interface_client(self):
"""Get ARP data from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["arp"] = {}
2019-12-05 22:29:25 +01:00
# Remove data if disabled
2019-12-02 18:13:55 +01:00
if not self.option_track_arp:
2020-03-16 04:51:41 +01:00
for uid in self.data["interface"]:
self.data["interface"][uid]["client-ip-address"] = "disabled"
self.data["interface"][uid]["client-mac-address"] = "disabled"
2019-12-12 09:06:15 +01:00
return
2019-12-02 18:13:55 +01:00
mac2ip = {}
bridge_used = False
2019-12-12 23:01:57 +01:00
mac2ip, bridge_used = self.update_arp(mac2ip, bridge_used)
2019-12-05 22:29:25 +01:00
if bridge_used:
2019-12-12 23:01:57 +01:00
self.update_bridge_hosts(mac2ip)
2019-12-05 22:29:25 +01:00
# Map ARP to ifaces
2020-03-16 04:51:41 +01:00
for uid in self.data["interface"]:
if uid not in self.data["arp"]:
continue
2020-03-16 04:51:41 +01:00
self.data["interface"][uid]["client-ip-address"] = from_entry(
self.data["arp"][uid], "address"
)
self.data["interface"][uid]["client-mac-address"] = from_entry(
self.data["arp"][uid], "mac-address"
)
2019-12-05 22:29:25 +01:00
# ---------------------------
# update_arp
# ---------------------------
2019-12-12 23:01:57 +01:00
def update_arp(self, mac2ip, bridge_used):
2019-12-05 22:29:25 +01:00
"""Get list of hosts in ARP for interface client data from Mikrotik"""
data = self.api.path("/ip/arp", return_list=True)
if not data:
return mac2ip, bridge_used
2019-12-02 18:13:55 +01:00
for entry in data:
# Ignore invalid entries
2020-03-16 04:51:41 +01:00
if entry["invalid"]:
2019-12-02 18:13:55 +01:00
continue
2020-03-16 04:51:41 +01:00
if "interface" not in entry:
2020-03-11 23:39:43 +01:00
continue
2019-12-02 18:13:55 +01:00
# Do not add ARP detected on bridge
2020-03-16 04:51:41 +01:00
if entry["interface"] == "bridge":
2019-12-02 18:13:55 +01:00
bridge_used = True
# Build address table on bridge
2020-03-16 04:51:41 +01:00
if "mac-address" in entry and "address" in entry:
mac2ip[entry["mac-address"]] = entry["address"]
2019-12-02 18:13:55 +01:00
continue
2019-12-02 18:13:55 +01:00
# Get iface default-name from custom name
2019-12-12 23:01:57 +01:00
uid = self.get_iface_from_entry(entry)
2019-12-02 18:13:55 +01:00
if not uid:
continue
2019-12-21 08:21:17 +01:00
_LOGGER.debug("Processing entry %s, entry %s", "/ip/arp", entry)
2019-12-02 18:13:55 +01:00
# Create uid arp dict
2020-03-16 04:51:41 +01:00
if uid not in self.data["arp"]:
self.data["arp"][uid] = {}
2019-12-02 18:13:55 +01:00
# Add data
2020-03-16 04:51:41 +01:00
self.data["arp"][uid]["interface"] = uid
self.data["arp"][uid]["mac-address"] = (
from_entry(entry, "mac-address")
if "mac-address" not in self.data["arp"][uid]
else "multiple"
)
self.data["arp"][uid]["address"] = (
from_entry(entry, "address")
if "address" not in self.data["arp"][uid]
else "multiple"
)
2019-12-05 22:29:25 +01:00
return mac2ip, bridge_used
2019-12-02 18:13:55 +01:00
# ---------------------------
# update_bridge_hosts
# ---------------------------
2019-12-12 23:01:57 +01:00
def update_bridge_hosts(self, mac2ip):
2019-12-05 22:29:25 +01:00
"""Get list of hosts in bridge for interface client data from Mikrotik"""
data = self.api.path("/interface/bridge/host", return_list=True)
if not data:
return
2019-12-02 18:13:55 +01:00
for entry in data:
# Ignore port MAC
2020-03-16 04:51:41 +01:00
if entry["local"]:
2019-12-02 18:13:55 +01:00
continue
2019-12-02 18:13:55 +01:00
# Get iface default-name from custom name
2019-12-12 23:01:57 +01:00
uid = self.get_iface_from_entry(entry)
2019-12-02 18:13:55 +01:00
if not uid:
continue
2020-03-16 04:51:41 +01:00
_LOGGER.debug(
"Processing entry %s, entry %s", "/interface/bridge/host", entry
)
2019-12-02 18:13:55 +01:00
# Create uid arp dict
2020-03-16 04:51:41 +01:00
if uid not in self.data["arp"]:
self.data["arp"][uid] = {}
2019-12-02 18:13:55 +01:00
# Add data
2020-03-16 04:51:41 +01:00
self.data["arp"][uid]["interface"] = uid
if "mac-address" in self.data["arp"][uid]:
self.data["arp"][uid]["mac-address"] = "multiple"
self.data["arp"][uid]["address"] = "multiple"
2019-12-02 18:13:55 +01:00
else:
2020-03-21 19:02:28 +03:00
self.data["arp"][uid]["mac-address"] = from_entry(entry,
"mac-address")
2020-03-16 04:51:41 +01:00
self.data["arp"][uid]["address"] = (
mac2ip[self.data["arp"][uid]["mac-address"]]
if self.data["arp"][uid]["mac-address"] in mac2ip
else ""
)
2019-12-02 19:23:09 +01:00
# ---------------------------
# get_iface_from_entry
2019-12-02 19:23:09 +01:00
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_iface_from_entry(self, entry):
"""Get interface default-name using name from interface dict"""
2019-12-02 19:23:09 +01:00
uid = None
2020-03-16 04:51:41 +01:00
for ifacename in self.data["interface"]:
if self.data["interface"][ifacename]["name"] == entry["interface"]:
2019-12-11 15:34:35 +01:00
uid = ifacename
2019-12-02 19:23:09 +01:00
break
2019-12-02 19:23:09 +01:00
return uid
2019-12-03 18:29:05 +01:00
# ---------------------------
# get_nat
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_nat(self):
"""Get NAT data from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["nat"] = parse_api(
data=self.data["nat"],
2020-04-04 18:01:26 +02:00
source=self.api.path("/ip/firewall/nat", return_list=True),
2020-03-16 04:51:41 +01:00
key=".id",
2019-12-12 09:06:15 +01:00
vals=[
2020-03-16 04:51:41 +01:00
{"name": ".id"},
{"name": "protocol", "default": "any"},
{"name": "dst-port", "default": "any"},
{"name": "in-interface", "default": "any"},
{"name": "to-addresses"},
{"name": "to-ports"},
{"name": "comment"},
{
"name": "enabled",
"source": "disabled",
"type": "bool",
"reverse": True,
},
2019-12-12 09:06:15 +01:00
],
val_proc=[
[
2020-03-16 04:51:41 +01:00
{"name": "name"},
{"action": "combine"},
{"key": "protocol"},
{"text": ":"},
{"key": "dst-port"},
2019-12-12 09:06:15 +01:00
]
2019-12-12 13:02:11 +01:00
],
2020-03-16 04:51:41 +01:00
only=[{"key": "action", "value": "dst-nat"}],
2019-12-12 09:06:15 +01:00
)
2019-12-02 18:13:55 +01:00
# ---------------------------
# get_system_routerboard
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_system_routerboard(self):
"""Get routerboard data from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["routerboard"] = parse_api(
data=self.data["routerboard"],
source=self.api.path("/system/routerboard", return_list=True),
2019-12-11 15:34:35 +01:00
vals=[
2020-03-16 04:51:41 +01:00
{"name": "routerboard", "type": "bool"},
{"name": "model", "default": "unknown"},
{"name": "serial-number", "default": "unknown"},
{"name": "firmware", "default": "unknown"},
],
2019-12-11 15:34:35 +01:00
)
2019-12-02 18:13:55 +01:00
# ---------------------------
# get_system_resource
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_system_resource(self):
"""Get system resources data from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["resource"] = parse_api(
data=self.data["resource"],
source=self.api.path("/system/resource", return_list=True),
2019-12-11 15:34:35 +01:00
vals=[
2020-03-16 04:51:41 +01:00
{"name": "platform", "default": "unknown"},
{"name": "board-name", "default": "unknown"},
{"name": "version", "default": "unknown"},
{"name": "uptime", "default": "unknown"},
{"name": "cpu-load", "default": "unknown"},
{"name": "free-memory", "default": 0},
{"name": "total-memory", "default": 0},
{"name": "free-hdd-space", "default": 0},
{"name": "total-hdd-space", "default": 0},
],
2019-12-11 15:34:35 +01:00
)
2020-03-16 04:51:41 +01:00
if self.data["resource"]["total-memory"] > 0:
self.data["resource"]["memory-usage"] = round(
(
(
self.data["resource"]["total-memory"]
- self.data["resource"]["free-memory"]
)
/ self.data["resource"]["total-memory"]
)
* 100
)
else:
2020-03-16 04:51:41 +01:00
self.data["resource"]["memory-usage"] = "unknown"
if self.data["resource"]["total-hdd-space"] > 0:
self.data["resource"]["hdd-usage"] = round(
(
(
self.data["resource"]["total-hdd-space"]
- self.data["resource"]["free-hdd-space"]
)
/ self.data["resource"]["total-hdd-space"]
)
* 100
)
else:
2020-03-16 04:51:41 +01:00
self.data["resource"]["hdd-usage"] = "unknown"
2019-12-04 16:09:30 +01:00
# ---------------------------
# get_system_routerboard
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_firmware_update(self):
"""Check for firmware update on Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["fw-update"] = parse_api(
data=self.data["fw-update"],
source=self.api.path("/system/package/update", return_list=True),
2019-12-12 13:02:11 +01:00
vals=[
2020-03-16 04:51:41 +01:00
{"name": "status"},
{"name": "channel", "default": "unknown"},
{"name": "installed-version", "default": "unknown"},
{"name": "latest-version", "default": "unknown"},
],
2019-12-12 13:02:11 +01:00
)
2020-03-16 04:51:41 +01:00
if "status" in self.data["fw-update"]:
self.data["fw-update"]["available"] = (
2020-03-21 19:02:28 +03:00
True if self.data["fw-update"][
"status"] == "New version is available"
2020-03-16 04:51:41 +01:00
else False
)
2019-12-12 13:02:11 +01:00
else:
2020-03-16 04:51:41 +01:00
self.data["fw-update"]["available"] = False
2019-12-12 13:02:11 +01:00
2019-12-04 20:13:11 +01:00
# ---------------------------
# get_script
# ---------------------------
2019-12-12 23:01:57 +01:00
def get_script(self):
"""Get list of all scripts from Mikrotik"""
2020-03-16 04:51:41 +01:00
self.data["script"] = parse_api(
data=self.data["script"],
source=self.api.path("/system/script", return_list=True),
2020-03-16 04:51:41 +01:00
key="name",
2019-12-11 15:34:35 +01:00
vals=[
2020-03-16 04:51:41 +01:00
{"name": "name"},
{"name": "last-started", "default": "unknown"},
{"name": "run-count", "default": "unknown"},
],
2019-12-11 15:34:35 +01:00
)
# ---------------------------
# get_queue
# ---------------------------
def get_queue(self):
"""Get Queue data from Mikrotik"""
self.data["queue"] = parse_api(
data=self.data["queue"],
source=self.api.path("/queue/simple", return_list=True),
key="name",
vals=[
{"name": ".id"},
{"name": "name", "default": "unknown"},
{"name": "target", "default": "unknown"},
{"name": "max-limit", "default": "0/0"},
{"name": "limit-at", "default": "0/0"},
{"name": "burst-limit", "default": "0/0"},
{"name": "burst-threshold", "default": "0/0"},
{"name": "burst-time", "default": "0s/0s"},
{"name": "packet-marks", "default": "none"},
{"name": "parent", "default": "none"},
{"name": "comment"},
{
"name": "enabled",
"source": "disabled",
"type": "bool",
"reverse": True,
},
]
)
traffic_type, traffic_div = self._get_traffic_type_and_div()
for uid in self.data["queue"]:
upload_max_limit_bps, download_max_limit_bps = [int(x) for x in
self.data["queue"][uid]["max-limit"].split('/')]
self.data["queue"][uid]["upload-max-limit"] = f"{round(upload_max_limit_bps * traffic_div)} {traffic_type}"
self.data["queue"][uid]["download-max-limit"] = f"{round(download_max_limit_bps * traffic_div)} {traffic_type}"
upload_limit_at_bps, download_limit_at_bps = [int(x) for x in
self.data["queue"][uid]["limit-at"].split('/')]
self.data["queue"][uid]["upload-limit-at"] = f"{round(upload_limit_at_bps * traffic_div)} {traffic_type}"
self.data["queue"][uid]["download-limit-at"] = f"{round(download_limit_at_bps * traffic_div)} {traffic_type}"
upload_burst_limit_bps, download_burst_limit_bps = [int(x) for x in
self.data["queue"][uid]["burst-limit"].split('/')]
self.data["queue"][uid]["upload-burst-limit"] = f"{round(upload_burst_limit_bps * traffic_div)} {traffic_type}"
self.data["queue"][uid]["download-burst-limit"] = f"{round(download_burst_limit_bps * traffic_div)} {traffic_type}"
upload_burst_threshold_bps, download_burst_threshold_bps = [int(x) for x in
self.data["queue"][uid]["burst-threshold"].split('/')]
self.data["queue"][uid]["upload-burst-threshold"] = f"{round(upload_burst_threshold_bps * traffic_div)} {traffic_type}"
self.data["queue"][uid]["download-burst-threshold"] = f"{round(download_burst_threshold_bps * traffic_div)} {traffic_type}"
upload_burst_time, download_burst_time = self.data["queue"][uid]["burst-time"].split('/')
self.data["queue"][uid]["upload-burst-time"] = upload_burst_time
self.data["queue"][uid]["download-burst-time"] = download_burst_time
def build_accounting_hosts(self):
# Build hosts from DHCP Server leases and ARP list
self.data["accounting"] = parse_api(
data=self.data["accounting"],
source=self.api.path("/ip/dhcp-server/lease", return_list=True),
key="address",
vals=[
{"name": "address"},
{"name": "mac-address"},
{"name": "host-name"},
{"name": "comment"},
{"name": "disabled", "default": True},
],
only=[
{"key": "disabled", "value": False},
],
ensure_vals=[
{"name": "address"},
{"name": "mac-address"},
]
)
# Also retrieve all entries in ARP table. If some hosts are missing, build it here
arp_hosts = parse_api(
data={},
source=self.api.path("/ip/arp", return_list=True),
key="address",
vals=[
{"name": "address"},
{"name": "mac-address"},
{"name": "disabled", "default": True},
{"name": "invalid", "default": True},
],
only=[
{"key": "disabled", "value": False},
{"key": "invalid", "value": False}
],
ensure_vals=[
{"name": "address"},
{"name": "mac-address"},
]
)
for addr in arp_hosts:
if addr not in self.data["accounting"]:
self.data["accounting"][addr] = {
"address": arp_hosts[addr]['address'],
"mac-address": arp_hosts[addr]['address']
}
# Build name for host. First try getting DHCP lease comment, then entry in DNS and then device's host-name.
# If everything fails use hosts IP address as name
dns_data = parse_api(
data={},
source=self.api.path("/ip/dns/static", return_list=True),
key="address",
vals=[
{"name": "address"},
{"name": "name"},
],
)
for addr in self.data["accounting"]:
if str(self.data["accounting"][addr].get('comment', '').strip()):
self.data["accounting"][addr]['name'] = self.data["accounting"][addr]['comment']
elif addr in dns_data and str(dns_data[addr].get('name', '').strip()):
self.data["accounting"][addr]['name'] = dns_data[addr]['name']
elif str(self.data["accounting"][addr].get('host-name', '').strip()):
self.data["accounting"][addr]['name'] = self.data["accounting"][addr]['host-name']
else:
self.data["accounting"][addr]['name'] = self.data["accounting"][addr]['address']
# Initialize data
self.data["accounting"][addr]["wan-tx"] = 0
self.data["accounting"][addr]["wan-rx"] = 0
if self.account_local_traffic:
self.data["accounting"][addr]["lan-tx"] = 0
self.data["accounting"][addr]["lan-rx"] = 0
_LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting hosts")
# Build local networks
dhcp_networks = parse_api(
data={},
source=self.api.path("/ip/dhcp-server/network", return_list=True),
key="address",
vals=[
{"name": "address"},
],
ensure_vals=[
{"name": "address"},
]
)
self.local_dhcp_networks = [ipaddress.IPv4Network(network) for network in dhcp_networks]
def _address_part_of_local_network(self, address):
address = ipaddress.ip_address(address)
for network in self.local_dhcp_networks:
if address in network:
return True
return False
def get_accounting(self):
"""Get Accounting data from Mikrotik"""
traffic_type, traffic_div = self._get_traffic_type_and_div()
# Build temp accounting values dict with all known addresses
# Also set traffic type for each item
accounting_values = {}
for addr in self.data['accounting']:
2020-04-05 16:04:17 +02:00
accounting_values[addr] = {
"wan-tx": 0,
"wan-rx": 0,
"lan-tx": 0,
"lan-rx": 0
}
self.data['accounting'][addr]["tx-rx-attr"] = traffic_type
time_diff = self.api.take_accounting_snapshot()
if time_diff:
accounting_data = parse_api(
data={},
source=self.api.path("/ip/accounting/snapshot", return_list=True),
key=".id",
vals=[
{"name": ".id"},
{"name": "src-address"},
{"name": "dst-address"},
{"name": "bytes", "default": 0},
],
)
for item in accounting_data.values():
source_ip = str(item.get('src-address')).strip()
destination_ip = str(item.get('dst-address')).strip()
bits_count = int(str(item.get('bytes')).strip()) * 8
if self._address_part_of_local_network(source_ip) and self._address_part_of_local_network(destination_ip):
# LAN TX/RX
if source_ip in accounting_values:
accounting_values[source_ip]['lan-tx'] += bits_count
if destination_ip in accounting_values:
accounting_values[destination_ip]['lan-rx'] += bits_count
elif self._address_part_of_local_network(source_ip) and \
not self._address_part_of_local_network(destination_ip):
# WAN TX
if source_ip in accounting_values:
accounting_values[source_ip]['wan-tx'] += bits_count
elif not self._address_part_of_local_network(source_ip) and \
self._address_part_of_local_network(destination_ip):
# WAN RX
if destination_ip in accounting_values:
accounting_values[destination_ip]['wan-rx'] += bits_count
else:
_LOGGER.debug(f"Skipping packet from {source_ip} to {destination_ip}")
continue
# Now that we have sum of all traffic in bytes for given period
# calculate real throughput and transform it to appropriate unit
for addr in accounting_values:
self.data['accounting'][addr]['wan-tx'] = round(
accounting_values[addr]['wan-tx'] / time_diff * traffic_div, 2)
self.data['accounting'][addr]['wan-rx'] = round(
accounting_values[addr]['wan-rx'] / time_diff * traffic_div, 2)
if self.account_local_traffic:
self.data['accounting'][addr]['lan-tx'] = round(
accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2)
self.data['accounting'][addr]['lan-rx'] = round(
accounting_values[addr]['lan-rx'] / time_diff * traffic_div, 2)