Merge branch 'master' into pr/1

This commit is contained in:
Ivan Pavlina 2020-04-07 10:54:19 +02:00
commit aa8f41f6fe
13 changed files with 481 additions and 11 deletions

View file

@ -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
![Tracker and sensors](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/device_tracker.png) ![Tracker and sensors](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/device_tracker.png)
@ -39,6 +40,7 @@ Features:
![Queue switch](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/queue_switch.png) ![Queue switch](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/queue_switch.png)
![Host tracker](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/host_tracker.png) ![Host tracker](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/host_tracker.png)
![Accounting sensor](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/accounting_sensor.png)
# 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
![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_options.png) ![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_options.png)
@ -58,3 +61,18 @@ You can add this integration several times for different devices.
## List of detected devices ## List of detected devices
![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_devices.png) ![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_devices.png)
## 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

View file

@ -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": {

View file

@ -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": {

View file

@ -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:

View file

@ -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,

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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,
)

View file

@ -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": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB