mirror of
https://github.com/tomaae/homeassistant-mikrotik_router.git
synced 2025-06-22 08:53:32 +02:00
Merge branch 'master' into pr/1
This commit is contained in:
commit
aa8f41f6fe
13 changed files with 481 additions and 11 deletions
18
README.md
18
README.md
|
@ -25,6 +25,7 @@ Features:
|
||||||
* Configurable update interval
|
* Configurable update interval
|
||||||
* Configurable traffic unit (bps, Kbps, Mbps, B/s, KB/s, MB/s)
|
* Configurable traffic unit (bps, Kbps, Mbps, B/s, KB/s, MB/s)
|
||||||
* Supports monitoring of multiple mikrotik devices simultaneously
|
* Supports monitoring of multiple mikrotik devices simultaneously
|
||||||
|
* RX/TX WAN/LAN traffic sensors per hosts from Mikrotik Accounting feature
|
||||||
|
|
||||||
# Integration preview
|
# Integration preview
|
||||||

|

|
||||||
|
@ -39,6 +40,7 @@ Features:
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
# Setup integration
|
# Setup integration
|
||||||
Setup this integration for your Mikrotik device in Home Assistant via `Configuration -> Integrations -> Add -> Mikrotik Router`.
|
Setup this integration for your Mikrotik device in Home Assistant via `Configuration -> Integrations -> Add -> Mikrotik Router`.
|
||||||
|
@ -49,6 +51,7 @@ You can add this integration several times for different devices.
|
||||||
* "Port" - Leave at 0 for defaults
|
* "Port" - Leave at 0 for defaults
|
||||||
* "Name of the integration" - Friendy name for this router
|
* "Name of the integration" - Friendy name for this router
|
||||||
* "Unit of measurement" - Traffic sensor measurement (bps, Kbps, Mbps, B/s, KB/s, MB/s)
|
* "Unit of measurement" - Traffic sensor measurement (bps, Kbps, Mbps, B/s, KB/s, MB/s)
|
||||||
|
* "Track accounting" - Determines if integration will track per-host throughput. Accounting must be enabled in Mikrotik first
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||

|

|
||||||
|
@ -58,3 +61,18 @@ You can add this integration several times for different devices.
|
||||||
|
|
||||||
## List of detected devices
|
## List of detected devices
|
||||||

|

|
||||||
|
|
||||||
|
## Accounting
|
||||||
|
For per-IP throughput tracking Mikrotik's accounting feature is used.
|
||||||
|
|
||||||
|
[Mikrotik support page](https://wiki.mikrotik.com/wiki/Manual:IP/Accounting)
|
||||||
|
|
||||||
|
Before setting up integration in HA, go in Winbox IP-Accounting and setup the feature. Make sure that threshold is set to resonable value to store all connections between user defined scan interval. Max value is 8192 so for piece of mind i recommend setting that value. Web Access is not needed, integration is using API access.
|
||||||
|
|
||||||
|
Integration will scan DHCP Lease table and ARP table to generate all known hosts and create two sensors for WAN traffic (mikrotik-XXX-wan-rx and mikrotik-XXX-wan-tx). If the parameter *account-local-traffic* is set in Mikrotik's accounting configuration it will also create two sensors for LAN traffic (mikrotik-XXX-lan-rx and mikrotik-XXX-lan-tx).
|
||||||
|
|
||||||
|
Device's name will be determined by first available string this order:
|
||||||
|
1. DHCP lease comment
|
||||||
|
2. DNS static entry
|
||||||
|
3. DHCP hostname
|
||||||
|
4. Device's IP address
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"ssl": "Use SSL",
|
"ssl": "Use SSL",
|
||||||
"unit_of_measurement": "Unit of measurement"
|
"unit_of_measurement": "Unit of measurement",
|
||||||
|
"track_accounting": "Track accounting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,7 +22,8 @@
|
||||||
"cannot_connect": "Cannot connect to Mikrotik.",
|
"cannot_connect": "Cannot connect to Mikrotik.",
|
||||||
"ssl_handshake_failure": "SSL handshake failure.",
|
"ssl_handshake_failure": "SSL handshake failure.",
|
||||||
"connection_timeout": "Mikrotik connection timeout.",
|
"connection_timeout": "Mikrotik connection timeout.",
|
||||||
"wrong_login": "Invalid username or password."
|
"wrong_login": "Invalid username or password.",
|
||||||
|
"accounting_disabled": "Accounting disabled in Mikrotik, cannot track."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"username": "Имя пользователя",
|
"username": "Имя пользователя",
|
||||||
"password": "Пароль",
|
"password": "Пароль",
|
||||||
"ssl": "Использовать SSL",
|
"ssl": "Использовать SSL",
|
||||||
"unit_of_measurement": "Единицы измерения"
|
"unit_of_measurement": "Единицы измерения",
|
||||||
|
"track_accounting": "Отслеживание учета"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,7 +22,8 @@
|
||||||
"cannot_connect": "Нет связи с Mikrotik.",
|
"cannot_connect": "Нет связи с Mikrotik.",
|
||||||
"ssl_handshake_failure": "Ошибка SSL-соединения",
|
"ssl_handshake_failure": "Ошибка SSL-соединения",
|
||||||
"connection_timeout": "Таймаут подключения к Mikrotik.",
|
"connection_timeout": "Таймаут подключения к Mikrotik.",
|
||||||
"wrong_login": "Неверные имя пользователя или пароль."
|
"wrong_login": "Неверные имя пользователя или пароль.",
|
||||||
|
"accounting_disabled": "Учетная запись отключена в Mikrotik, не может отслеживать."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -17,6 +17,7 @@ from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
DATA_CLIENT,
|
DATA_CLIENT,
|
||||||
DEFAULT_TRAFFIC_TYPE,
|
DEFAULT_TRAFFIC_TYPE,
|
||||||
|
CONF_TRACK_ACCOUNTING,
|
||||||
)
|
)
|
||||||
from .mikrotik_controller import MikrotikControllerData
|
from .mikrotik_controller import MikrotikControllerData
|
||||||
|
|
||||||
|
@ -48,12 +49,17 @@ async def async_setup_entry(hass, config_entry):
|
||||||
traffic_type = config_entry.data[CONF_UNIT_OF_MEASUREMENT]
|
traffic_type = config_entry.data[CONF_UNIT_OF_MEASUREMENT]
|
||||||
else:
|
else:
|
||||||
traffic_type = DEFAULT_TRAFFIC_TYPE
|
traffic_type = DEFAULT_TRAFFIC_TYPE
|
||||||
|
track_accounting = config_entry.data[CONF_TRACK_ACCOUNTING]
|
||||||
|
|
||||||
mikrotik_controller = MikrotikControllerData(
|
mikrotik_controller = MikrotikControllerData(
|
||||||
hass, config_entry, name, host, port, username, password, use_ssl,
|
hass, config_entry, name, host, port, username, password, use_ssl,
|
||||||
traffic_type
|
traffic_type, track_accounting
|
||||||
)
|
)
|
||||||
await mikrotik_controller.hwinfo_update()
|
await mikrotik_controller.hwinfo_update()
|
||||||
|
|
||||||
|
if track_accounting:
|
||||||
|
await mikrotik_controller.async_accounting_hosts_update()
|
||||||
|
|
||||||
await mikrotik_controller.async_update()
|
await mikrotik_controller.async_update()
|
||||||
|
|
||||||
if not mikrotik_controller.data:
|
if not mikrotik_controller.data:
|
||||||
|
|
|
@ -27,6 +27,7 @@ from .const import (
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEFAULT_TRAFFIC_TYPE,
|
DEFAULT_TRAFFIC_TYPE,
|
||||||
TRAFFIC_TYPES,
|
TRAFFIC_TYPES,
|
||||||
|
CONF_TRACK_ACCOUNTING,
|
||||||
)
|
)
|
||||||
from .mikrotikapi import MikrotikAPI
|
from .mikrotikapi import MikrotikAPI
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ def configured_instances(hass):
|
||||||
class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""MikrotikControllerConfigFlow class"""
|
"""MikrotikControllerConfigFlow class"""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -81,10 +82,13 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
username=user_input["username"],
|
username=user_input["username"],
|
||||||
password=user_input["password"],
|
password=user_input["password"],
|
||||||
port=user_input["port"],
|
port=user_input["port"],
|
||||||
use_ssl=user_input["ssl"],
|
use_ssl=user_input["ssl"]
|
||||||
)
|
)
|
||||||
if not api.connect():
|
if not api.connect():
|
||||||
errors[CONF_HOST] = api.error
|
errors[CONF_HOST] = api.error
|
||||||
|
else:
|
||||||
|
if user_input[CONF_TRACK_ACCOUNTING] and not api.is_accounting_enabled():
|
||||||
|
errors[CONF_HOST] = "accounting_disabled"
|
||||||
|
|
||||||
# Save instance
|
# Save instance
|
||||||
if not errors:
|
if not errors:
|
||||||
|
@ -99,6 +103,7 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
port=user_input["port"],
|
port=user_input["port"],
|
||||||
name=user_input["name"],
|
name=user_input["name"],
|
||||||
use_ssl=user_input["ssl"],
|
use_ssl=user_input["ssl"],
|
||||||
|
track_accounting=user_input["track_accounting"],
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,6 +120,7 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
port=0,
|
port=0,
|
||||||
name="Mikrotik",
|
name="Mikrotik",
|
||||||
use_ssl=False,
|
use_ssl=False,
|
||||||
|
track_accounting=False,
|
||||||
errors=None,
|
errors=None,
|
||||||
):
|
):
|
||||||
"""Show the configuration form to edit data."""
|
"""Show the configuration form to edit data."""
|
||||||
|
@ -131,6 +137,7 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
vol.Optional(CONF_PORT, default=port): int,
|
vol.Optional(CONF_PORT, default=port): int,
|
||||||
vol.Optional(CONF_NAME, default=name): str,
|
vol.Optional(CONF_NAME, default=name): str,
|
||||||
vol.Optional(CONF_SSL, default=use_ssl): bool,
|
vol.Optional(CONF_SSL, default=use_ssl): bool,
|
||||||
|
vol.Optional(CONF_TRACK_ACCOUNTING, default=track_accounting): bool,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
|
|
|
@ -16,3 +16,5 @@ DEFAULT_LOGIN_METHOD = "plain"
|
||||||
|
|
||||||
DEFAULT_TRAFFIC_TYPE = "Kbps"
|
DEFAULT_TRAFFIC_TYPE = "Kbps"
|
||||||
TRAFFIC_TYPES = ["bps", "Kbps", "Mbps", "B/s", "KB/s", "MB/s"]
|
TRAFFIC_TYPES = ["bps", "Kbps", "Mbps", "B/s", "KB/s", "MB/s"]
|
||||||
|
|
||||||
|
CONF_TRACK_ACCOUNTING = "track_accounting"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
@ -41,6 +42,7 @@ class MikrotikControllerData:
|
||||||
password,
|
password,
|
||||||
use_ssl,
|
use_ssl,
|
||||||
traffic_type,
|
traffic_type,
|
||||||
|
track_accounting,
|
||||||
):
|
):
|
||||||
"""Initialize MikrotikController."""
|
"""Initialize MikrotikController."""
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -48,6 +50,7 @@ class MikrotikControllerData:
|
||||||
self.host = host
|
self.host = host
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
self.traffic_type = traffic_type
|
self.traffic_type = traffic_type
|
||||||
|
self.track_accounting = track_accounting
|
||||||
|
|
||||||
self.data = {
|
self.data = {
|
||||||
"routerboard": {},
|
"routerboard": {},
|
||||||
|
@ -60,8 +63,11 @@ class MikrotikControllerData:
|
||||||
"queue": {},
|
"queue": {},
|
||||||
"dhcp-server": {},
|
"dhcp-server": {},
|
||||||
"dhcp": {},
|
"dhcp": {},
|
||||||
|
"accounting": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.local_dhcp_networks = []
|
||||||
|
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
@ -75,6 +81,10 @@ class MikrotikControllerData:
|
||||||
async_track_time_interval(
|
async_track_time_interval(
|
||||||
self.hass, self.force_fwupdate_check, timedelta(hours=1)
|
self.hass, self.force_fwupdate_check, timedelta(hours=1)
|
||||||
)
|
)
|
||||||
|
if self.track_accounting:
|
||||||
|
async_track_time_interval(
|
||||||
|
self.hass, self.force_accounting_hosts_update, timedelta(minutes=15)
|
||||||
|
)
|
||||||
|
|
||||||
def _get_traffic_type_and_div(self):
|
def _get_traffic_type_and_div(self):
|
||||||
traffic_type = self.option_traffic_type
|
traffic_type = self.option_traffic_type
|
||||||
|
@ -101,6 +111,14 @@ class MikrotikControllerData:
|
||||||
"""Trigger update by timer"""
|
"""Trigger update by timer"""
|
||||||
await self.async_update()
|
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
|
# force_fwupdate_check
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
|
@ -167,6 +185,19 @@ class MikrotikControllerData:
|
||||||
await self.hass.async_add_executor_job(self.get_system_resource)
|
await self.hass.async_add_executor_job(self.get_system_resource)
|
||||||
self.lock.release()
|
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()
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# async_fwupdate_check
|
# async_fwupdate_check
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
|
@ -196,6 +227,8 @@ class MikrotikControllerData:
|
||||||
await self.hass.async_add_executor_job(self.get_script)
|
await self.hass.async_add_executor_job(self.get_script)
|
||||||
await self.hass.async_add_executor_job(self.get_queue)
|
await self.hass.async_add_executor_job(self.get_queue)
|
||||||
await self.hass.async_add_executor_job(self.get_dhcp)
|
await self.hass.async_add_executor_job(self.get_dhcp)
|
||||||
|
if self.track_accounting:
|
||||||
|
await self.hass.async_add_executor_job(self.get_accounting)
|
||||||
|
|
||||||
async_dispatcher_send(self.hass, self.signal_update)
|
async_dispatcher_send(self.hass, self.signal_update)
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
@ -699,3 +732,179 @@ class MikrotikControllerData:
|
||||||
|
|
||||||
self.data["dhcp"][uid]['available'] = \
|
self.data["dhcp"][uid]['available'] = \
|
||||||
self.api.arp_ping(self.data["dhcp"][uid]['address'], self.data["dhcp"][uid]['interface'])
|
self.api.arp_ping(self.data["dhcp"][uid]['address'], self.data["dhcp"][uid]['interface'])
|
||||||
|
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 (only static entries)
|
||||||
|
# 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']
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices")
|
||||||
|
|
||||||
|
# Build list of 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
|
||||||
|
tmp_accounting_values = {}
|
||||||
|
for addr in self.data['accounting']:
|
||||||
|
tmp_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 tmp_accounting_values:
|
||||||
|
tmp_accounting_values[source_ip]['lan-tx'] += bits_count
|
||||||
|
if destination_ip in tmp_accounting_values:
|
||||||
|
tmp_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 tmp_accounting_values:
|
||||||
|
tmp_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 tmp_accounting_values:
|
||||||
|
tmp_accounting_values[destination_ip]['wan-rx'] += bits_count
|
||||||
|
|
||||||
|
# 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 tmp_accounting_values:
|
||||||
|
self.data['accounting'][addr]['wan-tx'] = round(
|
||||||
|
tmp_accounting_values[addr]['wan-tx'] / time_diff * traffic_div, 2)
|
||||||
|
self.data['accounting'][addr]['wan-rx'] = round(
|
||||||
|
tmp_accounting_values[addr]['wan-rx'] / time_diff * traffic_div, 2)
|
||||||
|
|
||||||
|
if self.api.is_accounting_local_traffic_enabled():
|
||||||
|
self.data['accounting'][addr]['lan-tx'] = round(
|
||||||
|
tmp_accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2)
|
||||||
|
self.data['accounting'][addr]['lan-rx'] = round(
|
||||||
|
tmp_accounting_values[addr]['lan-rx'] / time_diff * traffic_div, 2)
|
||||||
|
else:
|
||||||
|
# If local traffic was enabled earlier and then disabled return counters for LAN traffic to 0
|
||||||
|
if 'lan-tx' in self.data['accounting'][addr]:
|
||||||
|
self.data['accounting'][addr]['lan-tx'] = 0.0
|
||||||
|
if 'lan-rx' in self.data['accounting'][addr]:
|
||||||
|
self.data['accounting'][addr]['lan-rx'] = 0.0
|
||||||
|
else:
|
||||||
|
# No time diff, just initialize/return counters to 0 for all
|
||||||
|
for addr in tmp_accounting_values:
|
||||||
|
self.data['accounting'][addr]['wan-tx'] = 0.0
|
||||||
|
self.data['accounting'][addr]['wan-rx'] = 0.0
|
||||||
|
|
||||||
|
if self.api.is_accounting_local_traffic_enabled():
|
||||||
|
self.data['accounting'][addr]['lan-tx'] = 0.0
|
||||||
|
self.data['accounting'][addr]['lan-rx'] = 0.0
|
||||||
|
|
|
@ -60,6 +60,7 @@ class MikrotikAPI:
|
||||||
self._connection_retry_sec = 58
|
self._connection_retry_sec = 58
|
||||||
self.error = None
|
self.error = None
|
||||||
self.connection_error_reported = False
|
self.connection_error_reported = False
|
||||||
|
self.accounting_last_run = None
|
||||||
|
|
||||||
# Default ports
|
# Default ports
|
||||||
if not self._port:
|
if not self._port:
|
||||||
|
@ -427,6 +428,7 @@ class MikrotikAPI:
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except (
|
except (
|
||||||
librouteros_custom.exceptions.TrapError,
|
librouteros_custom.exceptions.TrapError,
|
||||||
librouteros_custom.exceptions.MultiTrapError,
|
librouteros_custom.exceptions.MultiTrapError,
|
||||||
|
@ -463,3 +465,97 @@ class MikrotikAPI:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _current_milliseconds():
|
||||||
|
from time import time
|
||||||
|
return int(round(time() * 1000))
|
||||||
|
|
||||||
|
def is_accounting_enabled(self) -> bool:
|
||||||
|
accounting = self.path("/ip/accounting", return_list=True)
|
||||||
|
if accounting is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for item in accounting:
|
||||||
|
if 'enabled' not in item:
|
||||||
|
continue
|
||||||
|
if item['enabled']:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_accounting_local_traffic_enabled(self) -> bool:
|
||||||
|
accounting = self.path("/ip/accounting", return_list=True)
|
||||||
|
if accounting is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for item in accounting:
|
||||||
|
if 'account-local-traffic' not in item:
|
||||||
|
continue
|
||||||
|
if item['account-local-traffic']:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# take_accounting_snapshot
|
||||||
|
# Returns float -> period in seconds between last and current run
|
||||||
|
# ---------------------------
|
||||||
|
def take_accounting_snapshot(self) -> float:
|
||||||
|
"""Get accounting data"""
|
||||||
|
if not self._connected or not self._connection:
|
||||||
|
if self._connection_epoch > time.time() - self._connection_retry_sec:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
accounting = self.path("/ip/accounting")
|
||||||
|
|
||||||
|
self.lock.acquire()
|
||||||
|
try:
|
||||||
|
# Prepare command
|
||||||
|
take = accounting('snapshot/take')
|
||||||
|
# Run command on Mikrotik
|
||||||
|
tuple(take)
|
||||||
|
except librouteros_custom.exceptions.ConnectionClosed:
|
||||||
|
if not self.connection_error_reported:
|
||||||
|
_LOGGER.error("Mikrotik %s connection closed", self._host)
|
||||||
|
self.connection_error_reported = True
|
||||||
|
|
||||||
|
self.disconnect()
|
||||||
|
self.lock.release()
|
||||||
|
return 0
|
||||||
|
if not self.connection_error_reported:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Mikrotik %s error while take_accounting_snapshot %s -> %s - %s", self._host,
|
||||||
|
type(api_error), api_error.args
|
||||||
|
)
|
||||||
|
self.connection_error_reported = True
|
||||||
|
|
||||||
|
self.disconnect()
|
||||||
|
self.lock.release()
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
if not self.connection_error_reported:
|
||||||
|
_LOGGER.error(
|
||||||
|
"% -> %s error on %s host while take_accounting_snapshot",
|
||||||
|
type(e), e.args, self._host,
|
||||||
|
)
|
||||||
|
self.connection_error_reported = True
|
||||||
|
|
||||||
|
self.disconnect()
|
||||||
|
self.lock.release()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
# First request will be discarded because we cannot know when the last data was retrieved
|
||||||
|
# prevents spikes in data
|
||||||
|
if not self.accounting_last_run:
|
||||||
|
self.accounting_last_run = self._current_milliseconds()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Calculate time difference in seconds and return
|
||||||
|
time_diff = self._current_milliseconds() - self.accounting_last_run
|
||||||
|
self.accounting_last_run = self._current_milliseconds()
|
||||||
|
return time_diff / 1000
|
||||||
|
|
|
@ -12,6 +12,19 @@ from .const import (DOMAIN, DATA_CLIENT, ATTRIBUTION)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# format_attribute
|
||||||
|
# ---------------------------
|
||||||
|
def format_attribute(attr):
|
||||||
|
res = attr.replace("-", " ")
|
||||||
|
res = res.capitalize()
|
||||||
|
res = res.replace(" ip ", " IP ")
|
||||||
|
res = res.replace(" mac ", " MAC ")
|
||||||
|
res = res.replace(" mtu", " MTU")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
ATTR_ICON = "icon"
|
ATTR_ICON = "icon"
|
||||||
ATTR_LABEL = "label"
|
ATTR_LABEL = "label"
|
||||||
ATTR_UNIT = "unit"
|
ATTR_UNIT = "unit"
|
||||||
|
@ -66,8 +79,49 @@ SENSOR_TYPES = {
|
||||||
ATTR_PATH: "interface",
|
ATTR_PATH: "interface",
|
||||||
ATTR_ATTR: "rx-bits-per-second",
|
ATTR_ATTR: "rx-bits-per-second",
|
||||||
},
|
},
|
||||||
|
"accounting_lan_tx": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:download-network",
|
||||||
|
ATTR_LABEL: "LAN TX",
|
||||||
|
ATTR_UNIT: "ps",
|
||||||
|
ATTR_UNIT_ATTR: "tx-rx-attr",
|
||||||
|
ATTR_PATH: "accounting",
|
||||||
|
ATTR_ATTR: "lan-tx",
|
||||||
|
},
|
||||||
|
"accounting_lan_rx": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:upload-network",
|
||||||
|
ATTR_LABEL: "LAN RX",
|
||||||
|
ATTR_UNIT: "ps",
|
||||||
|
ATTR_UNIT_ATTR: "tx-rx-attr",
|
||||||
|
ATTR_PATH: "accounting",
|
||||||
|
ATTR_ATTR: "lan-rx",
|
||||||
|
},
|
||||||
|
"accounting_wan_tx": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:download-network",
|
||||||
|
ATTR_LABEL: "WAN TX",
|
||||||
|
ATTR_UNIT: "ps",
|
||||||
|
ATTR_UNIT_ATTR: "tx-rx-attr",
|
||||||
|
ATTR_PATH: "accounting",
|
||||||
|
ATTR_ATTR: "wan-tx",
|
||||||
|
},
|
||||||
|
"accounting_wan_rx": {
|
||||||
|
ATTR_DEVICE_CLASS: None,
|
||||||
|
ATTR_ICON: "mdi:upload-network",
|
||||||
|
ATTR_LABEL: "WAN RX",
|
||||||
|
ATTR_UNIT: "ps",
|
||||||
|
ATTR_UNIT_ATTR: "tx-rx-attr",
|
||||||
|
ATTR_PATH: "accounting",
|
||||||
|
ATTR_ATTR: "wan-rx",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEVICE_ATTRIBUTES_ACCOUNTING = [
|
||||||
|
"address",
|
||||||
|
"mac-address",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# async_setup_entry
|
# async_setup_entry
|
||||||
|
@ -101,7 +155,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors):
|
||||||
new_sensors = []
|
new_sensors = []
|
||||||
|
|
||||||
for sensor in SENSOR_TYPES:
|
for sensor in SENSOR_TYPES:
|
||||||
if "traffic_" not in sensor:
|
if "system_" in sensor:
|
||||||
item_id = f"{inst}-{sensor}"
|
item_id = f"{inst}-{sensor}"
|
||||||
_LOGGER.debug("Updating sensor %s", item_id)
|
_LOGGER.debug("Updating sensor %s", item_id)
|
||||||
if item_id in sensors:
|
if item_id in sensors:
|
||||||
|
@ -133,6 +187,24 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors):
|
||||||
)
|
)
|
||||||
new_sensors.append(sensors[item_id])
|
new_sensors.append(sensors[item_id])
|
||||||
|
|
||||||
|
if "accounting_" in sensor:
|
||||||
|
for uid in mikrotik_controller.data["accounting"]:
|
||||||
|
|
||||||
|
item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['name']}"
|
||||||
|
if item_id in sensors:
|
||||||
|
if sensors[item_id].enabled:
|
||||||
|
sensors[item_id].async_schedule_update_ha_state()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if SENSOR_TYPES[sensor][ATTR_ATTR] in mikrotik_controller.data['accounting'][uid].keys():
|
||||||
|
sensors[item_id] = MikrotikAccountingSensor(
|
||||||
|
mikrotik_controller=mikrotik_controller,
|
||||||
|
inst=inst,
|
||||||
|
sensor=sensor,
|
||||||
|
uid=uid,
|
||||||
|
)
|
||||||
|
new_sensors.append(sensors[item_id])
|
||||||
|
|
||||||
if new_sensors:
|
if new_sensors:
|
||||||
async_add_entities(new_sensors, True)
|
async_add_entities(new_sensors, True)
|
||||||
|
|
||||||
|
@ -277,3 +349,57 @@ class MikrotikControllerTrafficSensor(MikrotikControllerSensor):
|
||||||
self._data["default-name"],
|
self._data["default-name"],
|
||||||
self._sensor,
|
self._sensor,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# MikrotikAccountingSensor
|
||||||
|
# ---------------------------
|
||||||
|
class MikrotikAccountingSensor(MikrotikControllerSensor):
|
||||||
|
"""Define an Mikrotik Accounting sensor."""
|
||||||
|
|
||||||
|
def __init__(self, mikrotik_controller, inst, sensor, uid):
|
||||||
|
"""Initialize."""
|
||||||
|
super().__init__(mikrotik_controller, inst, sensor)
|
||||||
|
self._uid = uid
|
||||||
|
self._data = mikrotik_controller.data[SENSOR_TYPES[sensor][ATTR_PATH]][uid]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name."""
|
||||||
|
return f"{self._inst} {self._data['name']} {self._type[ATTR_LABEL]}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique_id for this entity."""
|
||||||
|
return f"{self._inst.lower()}-{self._sensor.lower()}-{self._data['address'].lower()}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return a port description for device registry."""
|
||||||
|
info = {
|
||||||
|
"connections": {
|
||||||
|
(CONNECTION_NETWORK_MAC, self._data["mac-address"])},
|
||||||
|
"manufacturer": self._ctrl.data["resource"]["platform"],
|
||||||
|
"model": self._ctrl.data["resource"]["board-name"],
|
||||||
|
"name": self._data["name"],
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
attributes = self._attrs
|
||||||
|
for variable in DEVICE_ATTRIBUTES_ACCOUNTING:
|
||||||
|
if variable in self._data:
|
||||||
|
attributes[format_attribute(variable)] = self._data[variable]
|
||||||
|
|
||||||
|
return attributes
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Port entity created."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"New sensor %s (%s %s)",
|
||||||
|
self._inst,
|
||||||
|
self._data["name"],
|
||||||
|
self._sensor,
|
||||||
|
)
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"ssl": "Use SSL",
|
"ssl": "Use SSL",
|
||||||
"unit_of_measurement": "Unit of measurement"
|
"unit_of_measurement": "Unit of measurement",
|
||||||
|
"track_accounting": "Track accounting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -21,7 +22,8 @@
|
||||||
"cannot_connect": "Cannot connect to Mikrotik.",
|
"cannot_connect": "Cannot connect to Mikrotik.",
|
||||||
"ssl_handshake_failure": "SSL handshake failure",
|
"ssl_handshake_failure": "SSL handshake failure",
|
||||||
"connection_timeout": "Mikrotik connection timeout.",
|
"connection_timeout": "Mikrotik connection timeout.",
|
||||||
"wrong_login": "Invalid user name or password."
|
"wrong_login": "Invalid user name or password.",
|
||||||
|
"accounting_disabled": "Accounting disabled in Mikrotik, cannot track."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
BIN
docs/assets/images/ui/accounting_sensor.jpg
Normal file
BIN
docs/assets/images/ui/accounting_sensor.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
docs/assets/images/ui/setup_integration.PNG
Normal file
BIN
docs/assets/images/ui/setup_integration.PNG
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
Loading…
Add table
Add a link
Reference in a new issue