From 30c11db741c03b6983cf092221acdc1c7e4fad02 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sat, 4 Apr 2020 19:42:05 +0200 Subject: [PATCH 01/20] Add ability to track accounting data from Mikrotik. --- .../mikrotik_router/.translations/en.json | 6 +- .../mikrotik_router/.translations/ru.json | 6 +- custom_components/mikrotik_router/__init__.py | 8 +- .../mikrotik_router/config_flow.py | 11 +- custom_components/mikrotik_router/const.py | 2 + .../mikrotik_router/mikrotik_controller.py | 200 ++++++++++++++++++ .../mikrotik_router/mikrotikapi.py | 93 +++++++- custom_components/mikrotik_router/sensor.py | 130 +++++++++++- .../mikrotik_router/strings.json | 6 +- 9 files changed, 449 insertions(+), 13 deletions(-) diff --git a/custom_components/mikrotik_router/.translations/en.json b/custom_components/mikrotik_router/.translations/en.json index ec26cf2..913af8a 100644 --- a/custom_components/mikrotik_router/.translations/en.json +++ b/custom_components/mikrotik_router/.translations/en.json @@ -12,7 +12,8 @@ "username": "Username", "password": "Password", "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.", "ssl_handshake_failure": "SSL handshake failure", "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": { diff --git a/custom_components/mikrotik_router/.translations/ru.json b/custom_components/mikrotik_router/.translations/ru.json index 2cda4b9..b8216d0 100644 --- a/custom_components/mikrotik_router/.translations/ru.json +++ b/custom_components/mikrotik_router/.translations/ru.json @@ -12,7 +12,8 @@ "username": "Имя пользователя", "password": "Пароль", "ssl": "Использовать SSL", - "unit_of_measurement": "Единицы измерения" + "unit_of_measurement": "Единицы измерения", + "track_accounting": "Отслеживание учета" } } }, @@ -21,7 +22,8 @@ "cannot_connect": "Нет связи с Mikrotik.", "ssl_handshake_failure": "Ошибка SSL-соединения", "connection_timeout": "Таймаут подключения к Mikrotik.", - "wrong_login": "Неверные имя пользователя или пароль." + "wrong_login": "Неверные имя пользователя или пароль.", + "accounting_disabled": "Учетная запись отключена в Mikrotik, не может отслеживать." } }, "options": { diff --git a/custom_components/mikrotik_router/__init__.py b/custom_components/mikrotik_router/__init__.py index 61d051b..b9ae7f3 100644 --- a/custom_components/mikrotik_router/__init__.py +++ b/custom_components/mikrotik_router/__init__.py @@ -17,6 +17,7 @@ from .const import ( DOMAIN, DATA_CLIENT, DEFAULT_TRAFFIC_TYPE, + CONF_TRACK_ACCOUNTING, ) 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] else: traffic_type = DEFAULT_TRAFFIC_TYPE + track_accounting = config_entry.data[CONF_TRACK_ACCOUNTING] mikrotik_controller = MikrotikControllerData( hass, config_entry, name, host, port, username, password, use_ssl, - traffic_type + traffic_type, track_accounting ) await mikrotik_controller.hwinfo_update() + + if track_accounting: + await mikrotik_controller.async_accounting_hosts_update() + await mikrotik_controller.async_update() if not mikrotik_controller.data: diff --git a/custom_components/mikrotik_router/config_flow.py b/custom_components/mikrotik_router/config_flow.py index 3e9b3be..fdc6285 100644 --- a/custom_components/mikrotik_router/config_flow.py +++ b/custom_components/mikrotik_router/config_flow.py @@ -27,6 +27,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_TRAFFIC_TYPE, TRAFFIC_TYPES, + CONF_TRACK_ACCOUNTING, ) from .mikrotikapi import MikrotikAPI @@ -51,7 +52,7 @@ def configured_instances(hass): class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): """MikrotikControllerConfigFlow class""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL def __init__(self): @@ -81,10 +82,13 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): username=user_input["username"], password=user_input["password"], port=user_input["port"], - use_ssl=user_input["ssl"], + use_ssl=user_input["ssl"] ) if not api.connect(): 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 if not errors: @@ -99,6 +103,7 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): port=user_input["port"], name=user_input["name"], use_ssl=user_input["ssl"], + track_accounting=user_input["track_accounting"], errors=errors, ) @@ -115,6 +120,7 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): port=0, name="Mikrotik", use_ssl=False, + track_accounting=False, errors=None, ): """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_NAME, default=name): str, vol.Optional(CONF_SSL, default=use_ssl): bool, + vol.Optional(CONF_TRACK_ACCOUNTING, default=track_accounting): bool, } ), errors=errors, diff --git a/custom_components/mikrotik_router/const.py b/custom_components/mikrotik_router/const.py index 0472cca..c9d4a11 100644 --- a/custom_components/mikrotik_router/const.py +++ b/custom_components/mikrotik_router/const.py @@ -16,3 +16,5 @@ DEFAULT_LOGIN_METHOD = "plain" DEFAULT_TRAFFIC_TYPE = "Kbps" TRAFFIC_TYPES = ["bps", "Kbps", "Mbps", "B/s", "KB/s", "MB/s"] + +CONF_TRACK_ACCOUNTING = "track_accounting" diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 42a5326..3377253 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -3,6 +3,7 @@ import asyncio import logging from datetime import timedelta +import ipaddress from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -41,12 +42,14 @@ class MikrotikControllerData: password, use_ssl, traffic_type, + track_accounting, ): """Initialize MikrotikController.""" self.name = name self.hass = hass self.config_entry = config_entry self.traffic_type = traffic_type + self.track_accounting = track_accounting self.data = { "routerboard": {}, @@ -57,8 +60,11 @@ class MikrotikControllerData: "fw-update": {}, "script": {}, "queue": {}, + "accounting": {} } + self.local_dhcp_networks = [] + self.listeners = [] self.lock = asyncio.Lock() @@ -70,6 +76,10 @@ class MikrotikControllerData: async_track_time_interval( 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): traffic_type = self.option_traffic_type @@ -96,6 +106,14 @@ class MikrotikControllerData: """Trigger update by timer""" 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 # --------------------------- @@ -162,6 +180,19 @@ class MikrotikControllerData: 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() + # --------------------------- # async_fwupdate_check # --------------------------- @@ -190,6 +221,8 @@ class MikrotikControllerData: 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) async_dispatcher_send(self.hass, self.signal_update) self.lock.release() @@ -629,3 +662,170 @@ class MikrotikControllerData: 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 static DNS entries + dns_data = parse_api( + data={}, + source=self.api.path("/ip/dns/static", return_list=True), + key="address", + vals=[ + {"name": "address"}, + {"name": "name"}, + ], + ) + + # 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 + 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 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']: + accounting_values[addr] = { + "wan-tx": 0, + "wan-rx": 0, + "lan-tx": 0, + "lan-rx": 0 + } + self.data['accounting'][addr]["lan-wan-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]['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) + + 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) diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index 883e865..b8497fb 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -60,6 +60,7 @@ class MikrotikAPI: self._connection_retry_sec = 58 self.error = None self.connection_error_reported = False + self.accounting_last_run = None # Default ports if not self._port: @@ -396,7 +397,6 @@ class MikrotikAPI: # --------------------------- def get_traffic(self, interfaces) -> Optional(list): """Get traffic stats""" - traffic = None if not self._connected or not self._connection: if self._connection_epoch > time.time() - self._connection_retry_sec: return None @@ -482,3 +482,94 @@ class MikrotikAPI: self.lock.release() return traffic if traffic else None + + @staticmethod + def _current_milliseconds(): + from time import time + return int(round(time() * 1000)) + + def is_accounting_enabled(self): + 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 + + # --------------------------- + # take_accounting_snapshot + # Returns float -> seconds period between last run 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 + except ( + librouteros_custom.exceptions.TrapError, + librouteros_custom.exceptions.MultiTrapError, + librouteros_custom.exceptions.ProtocolError, + librouteros_custom.exceptions.FatalError, + ssl.SSLError, + BrokenPipeError, + OSError, + ValueError, + ) as api_error: + 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 diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index 5a883a2..9746f14 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -12,6 +12,19 @@ from .const import (DOMAIN, DATA_CLIENT, ATTRIBUTION) _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_LABEL = "label" ATTR_UNIT = "unit" @@ -66,8 +79,49 @@ SENSOR_TYPES = { ATTR_PATH: "interface", 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: "lan-wan-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: "lan-wan-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: "lan-wan-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: "lan-wan-tx-rx-attr", + ATTR_PATH: "accounting", + ATTR_ATTR: "wan-rx", + }, } +DEVICE_ATTRIBUTES_ACCOUNTING = [ + "address", + "mac-address", +] + # --------------------------- # async_setup_entry @@ -101,7 +155,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): new_sensors = [] for sensor in SENSOR_TYPES: - if "traffic_" not in sensor: + if "system_" in sensor: item_id = f"{inst}-{sensor}" if item_id in sensors: if sensors[item_id].enabled: @@ -116,8 +170,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): if "traffic_" in sensor: for uid in mikrotik_controller.data["interface"]: - if mikrotik_controller.data["interface"][uid][ - "type"] == "ether": + if mikrotik_controller.data["interface"][uid]["type"] == "ether": item_id = f"{inst}-{sensor}-{mikrotik_controller.data['interface'][uid]['default-name']}" if item_id in sensors: if sensors[item_id].enabled: @@ -132,6 +185,23 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): ) 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 + + sensors[item_id] = MikrotikAccountingSensor( + mikrotik_controller=mikrotik_controller, + inst=inst, + sensor=sensor, + uid=uid, + ) + new_sensors.append(sensors[item_id]) + if new_sensors: async_add_entities(new_sensors, True) @@ -276,3 +346,57 @@ class MikrotikControllerTrafficSensor(MikrotikControllerSensor): self._data["default-name"], 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, + ) diff --git a/custom_components/mikrotik_router/strings.json b/custom_components/mikrotik_router/strings.json index ec26cf2..913af8a 100644 --- a/custom_components/mikrotik_router/strings.json +++ b/custom_components/mikrotik_router/strings.json @@ -12,7 +12,8 @@ "username": "Username", "password": "Password", "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.", "ssl_handshake_failure": "SSL handshake failure", "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": { From f9a458bbfbf28995f01088a73cbcdc85a2aade3e Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 15:03:17 +0200 Subject: [PATCH 02/20] Add ability to automatically determine if the LAN accounting sensor should be created depending on account-local-traffic in Mikrotik API --- README.md | 1 + .../mikrotik_router/mikrotik_controller.py | 29 ++++++++++--------- .../mikrotik_router/mikrotikapi.py | 14 ++++++++- custom_components/mikrotik_router/sensor.py | 26 +++++++++-------- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index ab7f942..a2ccf8c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Features: * System sensors (CPU, Memory, HDD) * Firmware update binary sensor * Switches to run scripts + * RX/TX traffic sensors per hosts from Mikrotik Accounting # Integration preview ![Tracker and sensors](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/device_tracker.png) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 3377253..23fffb8 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -76,10 +76,12 @@ class MikrotikControllerData: 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 @@ -769,13 +771,14 @@ class MikrotikControllerData: # Also set traffic type for each item accounting_values = {} for addr in self.data['accounting']: - accounting_values[addr] = { - "wan-tx": 0, - "wan-rx": 0, - "lan-tx": 0, - "lan-rx": 0 - } - self.data['accounting'][addr]["lan-wan-tx-rx-attr"] = traffic_type + accounting_values[addr] = {} + accounting_values[addr]["wan-tx"] = 0 + accounting_values[addr]["wan-rx"] = 0 + if self.account_local_traffic: + accounting_values[addr]["lan-tx"] = 0 + accounting_values[addr]["lan-rx"] = 0 + + self.data['accounting'][addr]["tx-rx-attr"] = traffic_type time_diff = self.api.take_accounting_snapshot() if time_diff: @@ -819,13 +822,13 @@ class MikrotikControllerData: # 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]['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) - 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) diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index b8497fb..dd69141 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -488,7 +488,7 @@ class MikrotikAPI: from time import time return int(round(time() * 1000)) - def is_accounting_enabled(self): + def is_accounting_enabled(self) -> bool: accounting = self.path("/ip/accounting", return_list=True) if accounting is None: return False @@ -500,6 +500,18 @@ class MikrotikAPI: 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 -> seconds period between last run and current run diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index 9746f14..3ee1376 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -84,7 +84,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:download-network", ATTR_LABEL: "LAN TX", ATTR_UNIT: "ps", - ATTR_UNIT_ATTR: "lan-wan-tx-rx-attr", + ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", ATTR_ATTR: "lan-tx", }, @@ -93,7 +93,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "LAN RX", ATTR_UNIT: "ps", - ATTR_UNIT_ATTR: "lan-wan-tx-rx-attr", + ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", ATTR_ATTR: "lan-rx", }, @@ -102,7 +102,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:download-network", ATTR_LABEL: "WAN TX", ATTR_UNIT: "ps", - ATTR_UNIT_ATTR: "lan-wan-tx-rx-attr", + ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", ATTR_ATTR: "wan-tx", }, @@ -111,7 +111,7 @@ SENSOR_TYPES = { ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "WAN RX", ATTR_UNIT: "ps", - ATTR_UNIT_ATTR: "lan-wan-tx-rx-attr", + ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", ATTR_ATTR: "wan-rx", }, @@ -194,14 +194,16 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): sensors[item_id].async_schedule_update_ha_state() continue - sensors[item_id] = MikrotikAccountingSensor( - mikrotik_controller=mikrotik_controller, - inst=inst, - sensor=sensor, - uid=uid, - ) - new_sensors.append(sensors[item_id]) - + 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]) + else: + _LOGGER.info(f"WONT CREATE {SENSOR_TYPES[sensor][ATTR_ATTR]} for {item_id}") if new_sensors: async_add_entities(new_sensors, True) From a1eee6f69848195bdbb725d4f330c9284f03c61d Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 16:00:30 +0200 Subject: [PATCH 03/20] Initialize wan/lan accounting data on startup --- .../mikrotik_router/mikrotik_controller.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 23fffb8..d2a6ce7 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -688,16 +688,7 @@ class MikrotikControllerData: ] ) - # Also retrieve static DNS entries - dns_data = parse_api( - data={}, - source=self.api.path("/ip/dns/static", return_list=True), - key="address", - vals=[ - {"name": "address"}, - {"name": "name"}, - ], - ) + # Also retrieve all entries in ARP table. If some hosts are missing, build it here arp_hosts = parse_api( @@ -729,6 +720,17 @@ class MikrotikControllerData: # 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'] @@ -739,6 +741,13 @@ class MikrotikControllerData: 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 From 1c0b3018ed59b50cae3f7177e8409a05c79c8563 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 16:04:17 +0200 Subject: [PATCH 04/20] Fix --- .../mikrotik_router/mikrotik_controller.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index d2a6ce7..0a72ebf 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -780,13 +780,12 @@ class MikrotikControllerData: # Also set traffic type for each item accounting_values = {} for addr in self.data['accounting']: - accounting_values[addr] = {} - accounting_values[addr]["wan-tx"] = 0 - accounting_values[addr]["wan-rx"] = 0 - if self.account_local_traffic: - accounting_values[addr]["lan-tx"] = 0 - accounting_values[addr]["lan-rx"] = 0 - + 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() From d5ddbc7772fdeae95a262d66e0db0001ee1de46b Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 18:08:34 +0200 Subject: [PATCH 05/20] Allow for dynamic check of local_traffic enabled in Mikrotik. --- .../mikrotik_router/mikrotik_controller.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 0a72ebf..7b78774 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -76,12 +76,10 @@ class MikrotikControllerData: 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.hass, self.force_accounting_hosts_update, timedelta(minutes=5) ) - self.account_local_traffic = self.api.is_accounting_local_traffic_enabled() def _get_traffic_type_and_div(self): traffic_type = self.option_traffic_type @@ -667,7 +665,6 @@ class MikrotikControllerData: 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), @@ -688,8 +685,6 @@ class MikrotikControllerData: ] ) - - # Also retrieve all entries in ARP table. If some hosts are missing, build it here arp_hosts = parse_api( data={}, @@ -718,9 +713,8 @@ class MikrotikControllerData: "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 - + # 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), @@ -744,13 +738,13 @@ class MikrotikControllerData: # Initialize data self.data["accounting"][addr]["wan-tx"] = 0 self.data["accounting"][addr]["wan-rx"] = 0 - if self.account_local_traffic: + if self.api.is_accounting_local_traffic_enabled(): self.data["accounting"][addr]["lan-tx"] = 0 self.data["accounting"][addr]["lan-rx"] = 0 - _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting hosts") + _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") - # Build local networks + # Build list of local networks dhcp_networks = parse_api( data={}, source=self.api.path("/ip/dhcp-server/network", return_list=True), @@ -835,8 +829,9 @@ class MikrotikControllerData: self.data['accounting'][addr]['wan-rx'] = round( accounting_values[addr]['wan-rx'] / time_diff * traffic_div, 2) - if self.account_local_traffic: + if 'lan-tx' in self.data['accounting'][addr]: self.data['accounting'][addr]['lan-tx'] = round( accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2) + if 'lan-rx' in self.data['accounting'][addr]: self.data['accounting'][addr]['lan-rx'] = round( accounting_values[addr]['lan-rx'] / time_diff * traffic_div, 2) From 887e645553ecf436fa6073ef89e667f9cb864aac Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 18:29:51 +0200 Subject: [PATCH 06/20] Fix dynamic build of local tx/rx sensors. --- .../mikrotik_router/mikrotik_controller.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 7b78774..d58ce66 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -78,7 +78,7 @@ class MikrotikControllerData: ) if self.track_accounting: async_track_time_interval( - self.hass, self.force_accounting_hosts_update, timedelta(minutes=5) + self.hass, self.force_accounting_hosts_update, timedelta(minutes=15) ) def _get_traffic_type_and_div(self): @@ -735,13 +735,6 @@ class MikrotikControllerData: 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.api.is_accounting_local_traffic_enabled(): - self.data["accounting"][addr]["lan-tx"] = 0 - self.data["accounting"][addr]["lan-rx"] = 0 - _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") # Build list of local networks @@ -829,9 +822,17 @@ class MikrotikControllerData: self.data['accounting'][addr]['wan-rx'] = round( accounting_values[addr]['wan-rx'] / time_diff * traffic_div, 2) - if 'lan-tx' in self.data['accounting'][addr]: + if self.api.is_accounting_local_traffic_enabled(): self.data['accounting'][addr]['lan-tx'] = round( accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2) - if 'lan-rx' in self.data['accounting'][addr]: self.data['accounting'][addr]['lan-rx'] = round( accounting_values[addr]['lan-rx'] / time_diff * traffic_div, 2) + else: + # No time diff, just initialize/return counters to 0 for all + for addr in 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 From 03e70e521cc961711b91625cd2d8b7c0bbb143fc Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 18:32:48 +0200 Subject: [PATCH 07/20] Accounting initialize fix --- .../mikrotik_router/mikrotik_controller.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index d58ce66..ca63ce7 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -765,9 +765,9 @@ class MikrotikControllerData: # Build temp accounting values dict with all known addresses # Also set traffic type for each item - accounting_values = {} + tmp_accounting_values = {} for addr in self.data['accounting']: - accounting_values[addr] = { + tmp_accounting_values[addr] = { "wan-tx": 0, "wan-rx": 0, "lan-tx": 0, @@ -796,40 +796,46 @@ class MikrotikControllerData: 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 + 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 accounting_values: - accounting_values[source_ip]['wan-tx'] += bits_count + 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 accounting_values: - accounting_values[destination_ip]['wan-rx'] += bits_count + if destination_ip in tmp_accounting_values: + tmp_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: + for addr in tmp_accounting_values: self.data['accounting'][addr]['wan-tx'] = round( - accounting_values[addr]['wan-tx'] / time_diff * traffic_div, 2) + tmp_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) + 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( - accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2) + tmp_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) + 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 accounting_values: + for addr in tmp_accounting_values: self.data['accounting'][addr]['wan-tx'] = 0.0 self.data['accounting'][addr]['wan-rx'] = 0.0 From ca39617aaf034fbaae882ddd083bb2da90ac659c Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 23:15:53 +0200 Subject: [PATCH 08/20] Update docs and comments --- README.md | 13 ++++++++++++- .../mikrotik_router/mikrotikapi.py | 2 +- docs/assets/images/ui/accounting_sensor.jpg | Bin 0 -> 15030 bytes 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 docs/assets/images/ui/accounting_sensor.jpg diff --git a/README.md b/README.md index a2ccf8c..af9edfc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Features: * System sensors (CPU, Memory, HDD) * Firmware update binary sensor * Switches to run scripts - * RX/TX traffic sensors per hosts from Mikrotik Accounting + * RX/TX WAN/LAN traffic sensors per hosts from Mikrotik Accounting feature # Integration preview ![Tracker and sensors](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/device_tracker.png) @@ -32,6 +32,8 @@ Features: ![NAT switch](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/nat.png) ![Queue switch](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/queue_switch.png) +![Accounting sensor](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/accounting_sensor.png) + # Setup integration Setup this integration for your Mikrotik device in Home Assistant via `Configuration -> Integrations -> Add -> Mikrotik Router`. You can add this integration several times for different devices. @@ -50,3 +52,12 @@ You can add this integration several times for different devices. ## List of detected devices ![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). \ No newline at end of file diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index dd69141..bcf1c83 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -514,7 +514,7 @@ class MikrotikAPI: # --------------------------- # take_accounting_snapshot - # Returns float -> seconds period between last run and current run + # Returns float -> period in seconds between last and current run # --------------------------- def take_accounting_snapshot(self) -> float: """Get accounting data""" diff --git a/docs/assets/images/ui/accounting_sensor.jpg b/docs/assets/images/ui/accounting_sensor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10583ed1646ce7f3cb101663e922aaba1193668e GIT binary patch literal 15030 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 literal 0 HcmV?d00001 From 30bf7be151908363e610c3514258aca2b3905418 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 23:17:36 +0200 Subject: [PATCH 09/20] Update docs --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af9edfc..8ce71c5 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,9 @@ For per-IP throughput tracking Mikrotik's accounting feature is used. 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). \ No newline at end of file +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 From e8c3b3750d3c588743ce0b6e3ca936fb4fd08d6e Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 23:18:04 +0200 Subject: [PATCH 10/20] Update docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8ce71c5..593e635 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ For per-IP throughput tracking Mikrotik's accounting feature is used. 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 From 8767557e8413fa21c4c1d1e99f9e78df9cb5ee02 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 23:21:23 +0200 Subject: [PATCH 11/20] Update docs --- README.md | 1 + docs/assets/images/ui/setup_integration.PNG | Bin 0 -> 21576 bytes docs/assets/images/ui/setup_integration.png | Bin 17785 -> 0 bytes 3 files changed, 1 insertion(+) create mode 100644 docs/assets/images/ui/setup_integration.PNG delete mode 100644 docs/assets/images/ui/setup_integration.png diff --git a/README.md b/README.md index 593e635..321448f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ You can add this integration several times for different devices. * "Port" - Leave at 0 for defaults * "Name of the integration" - Friendy name for this router * "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 ![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_options.png) diff --git a/docs/assets/images/ui/setup_integration.PNG b/docs/assets/images/ui/setup_integration.PNG new file mode 100644 index 0000000000000000000000000000000000000000..42958f953ba350c13ad9d7b5ad9ed4354c8fd3ce GIT binary patch literal 21576 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 UtMY0R@bpgxPgg&ebxsLQ0LPhTLjV8( literal 0 HcmV?d00001 diff --git a/docs/assets/images/ui/setup_integration.png b/docs/assets/images/ui/setup_integration.png deleted file mode 100644 index 2eada0de0d01f7a1d5a10220e5626a7f95582244..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17785 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 From 2c577116f9d6d8d6a69d1d82e46e42b6cd56b66e Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Sun, 5 Apr 2020 23:23:07 +0200 Subject: [PATCH 12/20] Removed unnecessary logging --- custom_components/mikrotik_router/mikrotik_controller.py | 3 --- custom_components/mikrotik_router/sensor.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index ca63ce7..63fad91 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -810,9 +810,6 @@ class MikrotikControllerData: # WAN RX if destination_ip in tmp_accounting_values: tmp_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 diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index 3ee1376..5880425 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -202,8 +202,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): uid=uid, ) new_sensors.append(sensors[item_id]) - else: - _LOGGER.info(f"WONT CREATE {SENSOR_TYPES[sensor][ATTR_ATTR]} for {item_id}") + if new_sensors: async_add_entities(new_sensors, True) From e51304a5e3ec90cde864cc2f348f74775e0c51c7 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 11:32:38 +0200 Subject: [PATCH 13/20] Fixed imports, changed primary key for accounting hosts from ip address to mac address --- .../mikrotik_router/mikrotik_controller.py | 136 +++++++++++------- .../mikrotik_router/mikrotikapi.py | 71 ++++----- 2 files changed, 118 insertions(+), 89 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 091f310..2654716 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -3,7 +3,7 @@ import asyncio import logging from datetime import timedelta -import ipaddress +from ipaddress import ip_address, IPv4Network from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -719,6 +719,7 @@ class MikrotikControllerData: {"name": "status", "default": "unknown"}, {"name": "last-seen", "default": "unknown"}, {"name": "server", "default": "unknown"}, + {"name": "comment"}, ], ensure_vals=[ {"name": "interface"}, @@ -732,33 +733,38 @@ class MikrotikControllerData: self.data["dhcp"][uid]['available'] = \ 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"}, - ] - ) + # Build hosts from already retrieved DHCP Server leases + for mac in self.data["dhcp"]: + if mac not in self.data["accounting"]: + self.data["accounting"][mac] = self.data["dhcp"][mac] + + # 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", + key="mac-address", vals=[ {"name": "address"}, {"name": "mac-address"}, @@ -775,15 +781,14 @@ class MikrotikControllerData: ] ) - 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'] + for mac in arp_hosts: + if mac not in self.data["accounting"]: + self.data["accounting"][mac] = { + "address": arp_hosts[mac]['address'], + "mac-address": arp_hosts[mac]['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 + # Build name for host. dns_data = parse_api( data={}, source=self.api.path("/ip/dns/static", return_list=True), @@ -794,17 +799,22 @@ class MikrotikControllerData: ], ) - 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'] + for mac, vals in self.data["accounting"].items(): + # First try getting DHCP lease comment + if str(vals.get('comment', '').strip()): + self.data["accounting"][mac]['name'] = vals['comment'] + # Then entry in static DNS entry + elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): + self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] + # And then DHCP lease host-name + elif str(vals.get('host-name', '').strip()): + self.data["accounting"][mac]['name'] = vals['host-name'] + # If everything fails use hosts IP address as name else: - self.data["accounting"][addr]['name'] = self.data["accounting"][addr]['address'] + self.data["accounting"][mac]['name'] = vals['address'] _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") + _LOGGER.debug(self.data['accounting']) # Build list of local networks dhcp_networks = parse_api( @@ -819,30 +829,36 @@ class MikrotikControllerData: ] ) - self.local_dhcp_networks = [ipaddress.IPv4Network(network) for network in dhcp_networks] + self.local_dhcp_networks = [IPv4Network(network) for network in dhcp_networks] def _address_part_of_local_network(self, address): - address = ipaddress.ip_address(address) + address = ip_address(address) for network in self.local_dhcp_networks: if address in network: return True return False + def _get_accounting_mac_by_ip(self, requested_ip): + for mac, vals in self.data['accounting'].items(): + if vals.get('address') is requested_ip: + return mac + return None + 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 + # Build temp accounting values dict with ip address as key # Also set traffic type for each item tmp_accounting_values = {} - for addr in self.data['accounting']: - tmp_accounting_values[addr] = { + for mac, vals in self.data['accounting'].items(): + tmp_accounting_values[vals['address']] = { "wan-tx": 0, "wan-rx": 0, "lan-tx": 0, "lan-rx": 0 } - self.data['accounting'][addr]["tx-rx-attr"] = traffic_type + self.data['accounting'][mac]["tx-rx-attr"] = traffic_type time_diff = self.api.take_accounting_snapshot() if time_diff: @@ -883,28 +899,38 @@ class MikrotikControllerData: # 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( + mac = self._get_accounting_mac_by_ip(addr) + if not mac: + _LOGGER.debug(f"Address {addr} not found in accounting data, skipping update") + continue + + self.data['accounting'][mac]['wan-tx'] = round( tmp_accounting_values[addr]['wan-tx'] / time_diff * traffic_div, 2) - self.data['accounting'][addr]['wan-rx'] = round( + self.data['accounting'][mac]['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( + self.data['accounting'][mac]['lan-tx'] = round( tmp_accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2) - self.data['accounting'][addr]['lan-rx'] = round( + self.data['accounting'][mac]['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 + if 'lan-tx' in self.data['accounting'][mac]: + self.data['accounting'][mac]['lan-tx'] = 0.0 + if 'lan-rx' in self.data['accounting'][mac]: + self.data['accounting'][mac]['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 + mac = self._get_accounting_mac_by_ip(addr) + if not mac: + _LOGGER.debug(f"Address {addr} not found in accounting data, skipping update") + continue + + self.data['accounting'][mac]['wan-tx'] = 0.0 + self.data['accounting'][mac]['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 + self.data['accounting'][mac]['lan-tx'] = 0.0 + self.data['accounting'][mac]['lan-rx'] = 0.0 diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index 8e36ee5..9a71ee8 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -466,26 +466,32 @@ class MikrotikAPI: 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: + if not self.connection_check(): return False - for item in accounting: + response = self.path("/ip/accounting") + if response is None: + return False + + for item in response: 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 not self.connection_check(): + return False + + accounting = self.path("/ip/accounting") if accounting is None: return False @@ -502,48 +508,45 @@ class MikrotikAPI: # --------------------------- 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.connection_check(): + return 0 - if not self.connect(): - return 0 - - accounting = self.path("/ip/accounting") + accounting = self.path("/ip/accounting", return_list=False) 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() + except ( + librouteros_custom.exceptions.TrapError, + librouteros_custom.exceptions.MultiTrapError, + librouteros_custom.exceptions.ProtocolError, + librouteros_custom.exceptions.FatalError, + ssl.SSLError, + BrokenPipeError, + OSError, + ValueError, + ) as api_error: + self.disconnect("accounting_snapshot", api_error) + self.lock.release() + return 0 + except: + self.disconnect("accounting_snapshot") 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() + try: + list(take) + except librouteros_custom.exceptions.ConnectionClosed as api_error: + self.disconnect("accounting_snapshot", api_error) + self.lock.release() + return 0 + except: + self.disconnect("accounting_snapshot") self.lock.release() return 0 From 8244a146de03b921fe5c2ad6de912ae0a9fc3e27 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 13:02:01 +0200 Subject: [PATCH 14/20] Build hosts dynamically on every update --- custom_components/mikrotik_router/__init__.py | 4 +- .../mikrotik_router/mikrotik_controller.py | 128 +++++++++++++----- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/custom_components/mikrotik_router/__init__.py b/custom_components/mikrotik_router/__init__.py index b9ae7f3..200cd2c 100644 --- a/custom_components/mikrotik_router/__init__.py +++ b/custom_components/mikrotik_router/__init__.py @@ -57,8 +57,8 @@ async def async_setup_entry(hass, config_entry): ) await mikrotik_controller.hwinfo_update() - if track_accounting: - await mikrotik_controller.async_accounting_hosts_update() + #if track_accounting: + # await mikrotik_controller.async_accounting_hosts_update() await mikrotik_controller.async_update() diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 2654716..83d37f3 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -81,10 +81,10 @@ class MikrotikControllerData: async_track_time_interval( 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) - ) + # 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): traffic_type = self.option_traffic_type @@ -114,10 +114,10 @@ class MikrotikControllerData: # --------------------------- # force_accounting_hosts_update # --------------------------- - @callback - async def force_accounting_hosts_update(self, _now=None): - """Trigger update by timer""" - await self.async_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 @@ -188,15 +188,15 @@ class MikrotikControllerData: # --------------------------- # 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 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 @@ -708,6 +708,20 @@ class MikrotikControllerData: ] ) + # Build list of local DHCP networks + dhcp_networks = parse_api( + data={}, + source=self.api.path("/ip/dhcp-server/network"), + key="address", + vals=[ + {"name": "address"}, + ], + ensure_vals=[ + {"name": "address"}, + ] + ) + self.local_dhcp_networks = [IPv4Network(network) for network in dhcp_networks] + self.data["dhcp"] = parse_api( data=self.data["dhcp"], source=self.api.path("/ip/dhcp-server/lease"), @@ -760,7 +774,7 @@ class MikrotikControllerData: # ] # ) - # Also retrieve all entries in ARP table. If some hosts are missing, build it here + # Also add hosts not found in DHCP Leases from ARP table arp_hosts = parse_api( data={}, source=self.api.path("/ip/arp", return_list=True), @@ -788,7 +802,7 @@ class MikrotikControllerData: "mac-address": arp_hosts[mac]['address'] } - # Build name for host. + # Build name for host dns_data = parse_api( data={}, source=self.api.path("/ip/dns/static", return_list=True), @@ -816,20 +830,6 @@ class MikrotikControllerData: _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") _LOGGER.debug(self.data['accounting']) - # 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 = [IPv4Network(network) for network in dhcp_networks] def _address_part_of_local_network(self, address): address = ip_address(address) @@ -846,6 +846,68 @@ class MikrotikControllerData: def get_accounting(self): """Get Accounting data from Mikrotik""" + + # Build missing hosts from already retrieved DHCP Server leases + for mac in self.data["dhcp"]: + if mac not in self.data["accounting"]: + self.data["accounting"][mac] = self.data["dhcp"][mac] + + # Also add hosts not found in DHCP Leases from ARP table + arp_hosts = parse_api( + data={}, + source=self.api.path("/ip/arp"), + key="mac-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 mac in arp_hosts: + if mac not in self.data["accounting"]: + self.data["accounting"][mac] = { + "address": arp_hosts[mac]['address'], + "mac-address": arp_hosts[mac]['mac-address'] + } + + # Build name for host + dns_data = parse_api( + data={}, + source=self.api.path("/ip/dns/static"), + key="address", + vals=[ + {"name": "address"}, + {"name": "name"}, + ], + ) + + for mac, vals in self.data["accounting"].items(): + # First try getting DHCP lease comment + if str(vals.get('comment', '').strip()): + self.data["accounting"][mac]['name'] = vals['comment'] + # Then entry in static DNS entry + elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): + self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] + # And then DHCP lease host-name + elif str(vals.get('host-name', '').strip()): + self.data["accounting"][mac]['name'] = vals['host-name'] + # If everything fails use hosts IP address as name + else: + self.data["accounting"][mac]['name'] = vals['address'] + + _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") + _LOGGER.debug(self.data['accounting']) + traffic_type, traffic_div = self._get_traffic_type_and_div() # Build temp accounting values dict with ip address as key From aee33b8c136e5dd75a8d2051fe9ba343db5d0c96 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 13:33:48 +0200 Subject: [PATCH 15/20] Removed building hosts from ARP table for accounting devices - will use only DHCP leases. Removed unnecessary code, put all accounting sensors in the same group, set accounting sensors key to mac-address --- custom_components/mikrotik_router/__init__.py | 3 - .../mikrotik_router/mikrotik_controller.py | 130 +----------------- custom_components/mikrotik_router/sensor.py | 8 +- 3 files changed, 13 insertions(+), 128 deletions(-) diff --git a/custom_components/mikrotik_router/__init__.py b/custom_components/mikrotik_router/__init__.py index 200cd2c..c052bff 100644 --- a/custom_components/mikrotik_router/__init__.py +++ b/custom_components/mikrotik_router/__init__.py @@ -57,9 +57,6 @@ async def async_setup_entry(hass, config_entry): ) await mikrotik_controller.hwinfo_update() - #if track_accounting: - # await mikrotik_controller.async_accounting_hosts_update() - await mikrotik_controller.async_update() if not mikrotik_controller.data: diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 83d37f3..5b9a5b4 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -585,7 +585,7 @@ class MikrotikControllerData: self.data["resource"]["hdd-usage"] = "unknown" # --------------------------- - # get_system_routerboard + # get_firmware_update # --------------------------- def get_firmware_update(self): """Check for firmware update on Mikrotik""" @@ -748,89 +748,6 @@ class MikrotikControllerData: self.data["dhcp"][uid]['available'] = \ self.api.arp_ping(self.data["dhcp"][uid]['address'], self.data["dhcp"][uid]['interface']) - def build_accounting_hosts(self): - # Build hosts from already retrieved DHCP Server leases - for mac in self.data["dhcp"]: - if mac not in self.data["accounting"]: - self.data["accounting"][mac] = self.data["dhcp"][mac] - - # 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 add hosts not found in DHCP Leases from ARP table - arp_hosts = parse_api( - data={}, - source=self.api.path("/ip/arp", return_list=True), - key="mac-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 mac in arp_hosts: - if mac not in self.data["accounting"]: - self.data["accounting"][mac] = { - "address": arp_hosts[mac]['address'], - "mac-address": arp_hosts[mac]['address'] - } - - # Build name for host - dns_data = parse_api( - data={}, - source=self.api.path("/ip/dns/static", return_list=True), - key="address", - vals=[ - {"name": "address"}, - {"name": "name"}, - ], - ) - - for mac, vals in self.data["accounting"].items(): - # First try getting DHCP lease comment - if str(vals.get('comment', '').strip()): - self.data["accounting"][mac]['name'] = vals['comment'] - # Then entry in static DNS entry - elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): - self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] - # And then DHCP lease host-name - elif str(vals.get('host-name', '').strip()): - self.data["accounting"][mac]['name'] = vals['host-name'] - # If everything fails use hosts IP address as name - else: - self.data["accounting"][mac]['name'] = vals['address'] - - _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") - _LOGGER.debug(self.data['accounting']) - - def _address_part_of_local_network(self, address): address = ip_address(address) for network in self.local_dhcp_networks: @@ -846,39 +763,10 @@ class MikrotikControllerData: def get_accounting(self): """Get Accounting data from Mikrotik""" - # Build missing hosts from already retrieved DHCP Server leases - for mac in self.data["dhcp"]: + for mac, vals in self.data["dhcp"].items(): if mac not in self.data["accounting"]: - self.data["accounting"][mac] = self.data["dhcp"][mac] - - # Also add hosts not found in DHCP Leases from ARP table - arp_hosts = parse_api( - data={}, - source=self.api.path("/ip/arp"), - key="mac-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 mac in arp_hosts: - if mac not in self.data["accounting"]: - self.data["accounting"][mac] = { - "address": arp_hosts[mac]['address'], - "mac-address": arp_hosts[mac]['mac-address'] - } + self.data["accounting"][mac] = vals # Build name for host dns_data = parse_api( @@ -895,18 +783,14 @@ class MikrotikControllerData: # First try getting DHCP lease comment if str(vals.get('comment', '').strip()): self.data["accounting"][mac]['name'] = vals['comment'] - # Then entry in static DNS entry + # Then static DNS entry elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] - # And then DHCP lease host-name - elif str(vals.get('host-name', '').strip()): - self.data["accounting"][mac]['name'] = vals['host-name'] - # If everything fails use hosts IP address as name + # If everything fails use hosts DHCP lease host-name else: - self.data["accounting"][mac]['name'] = vals['address'] + self.data["accounting"][mac]['name'] = vals['host-name'] _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") - _LOGGER.debug(self.data['accounting']) traffic_type, traffic_div = self._get_traffic_type_and_div() @@ -926,7 +810,7 @@ class MikrotikControllerData: if time_diff: accounting_data = parse_api( data={}, - source=self.api.path("/ip/accounting/snapshot", return_list=True), + source=self.api.path("/ip/accounting/snapshot"), key=".id", vals=[ {"name": ".id"}, diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index a79e986..4c469a1 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -83,6 +83,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:download-network", ATTR_LABEL: "LAN TX", + ATTR_GROUP: "Accounting", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", @@ -92,6 +93,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "LAN RX", + ATTR_GROUP: "Accounting", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", @@ -101,6 +103,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:download-network", ATTR_LABEL: "WAN TX", + ATTR_GROUP: "Accounting", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", @@ -110,6 +113,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "WAN RX", + ATTR_GROUP: "Accounting", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_PATH: "accounting", @@ -371,7 +375,7 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self._inst.lower()}-{self._sensor.lower()}-{self._data['address'].lower()}" + return f"{self._inst.lower()}-{self._sensor.lower()}-{self._data['mac-address'].lower()}" @property def device_info(self): @@ -381,7 +385,7 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): (CONNECTION_NETWORK_MAC, self._data["mac-address"])}, "manufacturer": self._ctrl.data["resource"]["platform"], "model": self._ctrl.data["resource"]["board-name"], - "name": self._data["name"], + "name": SENSOR_TYPES[ATTR_GROUP], } return info From 39c31cb84a2636e1010058f561eed466e8a0c34e Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 14:19:22 +0200 Subject: [PATCH 16/20] Fixed grouping of accounting entities --- custom_components/mikrotik_router/sensor.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index 4c469a1..a95f936 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -379,13 +379,20 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): @property def device_info(self): - """Return a port description for device registry.""" + """Return a accounting description for device registry.""" info = { - "connections": { - (CONNECTION_NETWORK_MAC, self._data["mac-address"])}, + "identifiers": { + ( + DOMAIN, + "serial-number", + self._ctrl.data["routerboard"]["serial-number"], + "sensor", + "Accounting" + ) + }, "manufacturer": self._ctrl.data["resource"]["platform"], "model": self._ctrl.data["resource"]["board-name"], - "name": SENSOR_TYPES[ATTR_GROUP], + "name": self._type[ATTR_GROUP], } return info From 069897c32c81e5841c5f7c5841a660e1e778558b Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 14:50:26 +0200 Subject: [PATCH 17/20] Removed manual accounting enable/disable checkbox, now it dinamically creates entites depending on state in Mikrotik. Reverted config flow to version 1. Reused ARP call in accounting hosts building. --- .../mikrotik_router/.translations/en.json | 2 - .../mikrotik_router/.translations/ru.json | 2 - custom_components/mikrotik_router/__init__.py | 4 +- .../mikrotik_router/config_flow.py | 9 +-- custom_components/mikrotik_router/const.py | 2 - .../mikrotik_router/mikrotik_controller.py | 66 ++++++++++--------- .../mikrotik_router/mikrotikapi.py | 33 ++++------ .../mikrotik_router/strings.json | 2 - 8 files changed, 50 insertions(+), 70 deletions(-) diff --git a/custom_components/mikrotik_router/.translations/en.json b/custom_components/mikrotik_router/.translations/en.json index ca60582..8e4a33d 100644 --- a/custom_components/mikrotik_router/.translations/en.json +++ b/custom_components/mikrotik_router/.translations/en.json @@ -13,7 +13,6 @@ "password": "Password", "ssl": "Use SSL", "unit_of_measurement": "Unit of measurement", - "track_accounting": "Track accounting" } } }, @@ -23,7 +22,6 @@ "ssl_handshake_failure": "SSL handshake failure.", "connection_timeout": "Mikrotik connection timeout.", "wrong_login": "Invalid username or password.", - "accounting_disabled": "Accounting disabled in Mikrotik, cannot track." } }, "options": { diff --git a/custom_components/mikrotik_router/.translations/ru.json b/custom_components/mikrotik_router/.translations/ru.json index b8216d0..3a2f5fe 100644 --- a/custom_components/mikrotik_router/.translations/ru.json +++ b/custom_components/mikrotik_router/.translations/ru.json @@ -13,7 +13,6 @@ "password": "Пароль", "ssl": "Использовать SSL", "unit_of_measurement": "Единицы измерения", - "track_accounting": "Отслеживание учета" } } }, @@ -23,7 +22,6 @@ "ssl_handshake_failure": "Ошибка SSL-соединения", "connection_timeout": "Таймаут подключения к Mikrotik.", "wrong_login": "Неверные имя пользователя или пароль.", - "accounting_disabled": "Учетная запись отключена в Mikrotik, не может отслеживать." } }, "options": { diff --git a/custom_components/mikrotik_router/__init__.py b/custom_components/mikrotik_router/__init__.py index c052bff..b84d19d 100644 --- a/custom_components/mikrotik_router/__init__.py +++ b/custom_components/mikrotik_router/__init__.py @@ -17,7 +17,6 @@ from .const import ( DOMAIN, DATA_CLIENT, DEFAULT_TRAFFIC_TYPE, - CONF_TRACK_ACCOUNTING, ) from .mikrotik_controller import MikrotikControllerData @@ -49,11 +48,10 @@ async def async_setup_entry(hass, config_entry): traffic_type = config_entry.data[CONF_UNIT_OF_MEASUREMENT] else: traffic_type = DEFAULT_TRAFFIC_TYPE - track_accounting = config_entry.data[CONF_TRACK_ACCOUNTING] mikrotik_controller = MikrotikControllerData( hass, config_entry, name, host, port, username, password, use_ssl, - traffic_type, track_accounting + traffic_type ) await mikrotik_controller.hwinfo_update() diff --git a/custom_components/mikrotik_router/config_flow.py b/custom_components/mikrotik_router/config_flow.py index fdc6285..ec574aa 100644 --- a/custom_components/mikrotik_router/config_flow.py +++ b/custom_components/mikrotik_router/config_flow.py @@ -27,7 +27,6 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_TRAFFIC_TYPE, TRAFFIC_TYPES, - CONF_TRACK_ACCOUNTING, ) from .mikrotikapi import MikrotikAPI @@ -52,7 +51,7 @@ def configured_instances(hass): class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): """MikrotikControllerConfigFlow class""" - VERSION = 2 + VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL def __init__(self): @@ -86,9 +85,6 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): ) if not api.connect(): 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 if not errors: @@ -103,7 +99,6 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): port=user_input["port"], name=user_input["name"], use_ssl=user_input["ssl"], - track_accounting=user_input["track_accounting"], errors=errors, ) @@ -120,7 +115,6 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): port=0, name="Mikrotik", use_ssl=False, - track_accounting=False, errors=None, ): """Show the configuration form to edit data.""" @@ -137,7 +131,6 @@ class MikrotikControllerConfigFlow(ConfigFlow, domain=DOMAIN): vol.Optional(CONF_PORT, default=port): int, vol.Optional(CONF_NAME, default=name): str, vol.Optional(CONF_SSL, default=use_ssl): bool, - vol.Optional(CONF_TRACK_ACCOUNTING, default=track_accounting): bool, } ), errors=errors, diff --git a/custom_components/mikrotik_router/const.py b/custom_components/mikrotik_router/const.py index c9d4a11..0472cca 100644 --- a/custom_components/mikrotik_router/const.py +++ b/custom_components/mikrotik_router/const.py @@ -16,5 +16,3 @@ DEFAULT_LOGIN_METHOD = "plain" DEFAULT_TRAFFIC_TYPE = "Kbps" TRAFFIC_TYPES = ["bps", "Kbps", "Mbps", "B/s", "KB/s", "MB/s"] - -CONF_TRACK_ACCOUNTING = "track_accounting" diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 5b9a5b4..9776030 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -42,7 +42,6 @@ class MikrotikControllerData: password, use_ssl, traffic_type, - track_accounting, ): """Initialize MikrotikController.""" self.name = name @@ -50,7 +49,6 @@ class MikrotikControllerData: self.host = host self.config_entry = config_entry self.traffic_type = traffic_type - self.track_accounting = track_accounting self.data = { "routerboard": {}, @@ -73,6 +71,7 @@ class MikrotikControllerData: self.api = MikrotikAPI(host, username, password, port, use_ssl) + self.raw_arp_entries = [] self.nat_removed = {} async_track_time_interval( @@ -81,10 +80,6 @@ class MikrotikControllerData: async_track_time_interval( 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): traffic_type = self.option_traffic_type @@ -111,14 +106,6 @@ class MikrotikControllerData: """Trigger update by timer""" 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 # --------------------------- @@ -185,19 +172,6 @@ class MikrotikControllerData: 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() - # --------------------------- # async_fwupdate_check # --------------------------- @@ -227,8 +201,7 @@ class MikrotikControllerData: 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_dhcp) - if self.track_accounting: - await self.hass.async_add_executor_job(self.get_accounting) + await self.hass.async_add_executor_job(self.get_accounting) async_dispatcher_send(self.hass, self.signal_update) self.lock.release() @@ -371,6 +344,7 @@ class MikrotikControllerData: def update_arp(self, mac2ip, bridge_used): """Get list of hosts in ARP for interface client data from Mikrotik""" data = self.api.path("/ip/arp") + self.raw_arp_entries = data if not data: return mac2ip, bridge_used @@ -763,10 +737,38 @@ class MikrotikControllerData: def get_accounting(self): """Get Accounting data from Mikrotik""" + # Check if accounting and account-local-traffic is enabled + accounting_enabled, local_traffic_enabled = self.api.is_accounting_and_local_traffic_enabled() + + if not accounting_enabled: + # If any hosts were created return counters to 0 so sensors wont get stuck on last value + for mac, vals in self.data["accounting"].items(): + if 'wan-tx' in vals: + self.data["accounting"]['wan-tx'] = 0.0 + if 'wan-rx' in vals: + self.data["accounting"]['wan-rx'] = 0.0 + if 'lan-tx' in vals: + self.data["accounting"]['lan-tx'] = 0.0 + if 'lan-rx' in vals: + self.data["accounting"]['lan-rx'] = 0.0 + return + # Build missing hosts from already retrieved DHCP Server leases for mac, vals in self.data["dhcp"].items(): if mac not in self.data["accounting"]: - self.data["accounting"][mac] = vals + self.data["accounting"][mac] = { + 'address': vals['address'], + 'mac-address': vals['mac-address'], + 'host-name': vals['host-name'], + } + + # Build missing hosts from already retrieved ARP list + for entry in self.raw_arp_entries: + if entry['mac-address'] not in self.data["accounting"]: + self.data["accounting"][entry['mac-address']] = { + 'address': entry['address'], + 'mac-address': entry['mac-address'], + } # Build name for host dns_data = parse_api( @@ -855,7 +857,7 @@ class MikrotikControllerData: self.data['accounting'][mac]['wan-rx'] = round( tmp_accounting_values[addr]['wan-rx'] / time_diff * traffic_div, 2) - if self.api.is_accounting_local_traffic_enabled(): + if local_traffic_enabled: self.data['accounting'][mac]['lan-tx'] = round( tmp_accounting_values[addr]['lan-tx'] / time_diff * traffic_div, 2) self.data['accounting'][mac]['lan-rx'] = round( @@ -877,6 +879,6 @@ class MikrotikControllerData: self.data['accounting'][mac]['wan-tx'] = 0.0 self.data['accounting'][mac]['wan-rx'] = 0.0 - if self.api.is_accounting_local_traffic_enabled(): + if local_traffic_enabled: self.data['accounting'][mac]['lan-tx'] = 0.0 self.data['accounting'][mac]['lan-rx'] = 0.0 diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index 9a71ee8..66f1bbc 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -471,36 +471,31 @@ class MikrotikAPI: from time import time return int(round(time() * 1000)) - def is_accounting_enabled(self) -> bool: + def is_accounting_and_local_traffic_enabled(self) -> (bool, bool): + # Returns: + # 1st bool: Is accounting enabled + # 2nd bool: Is account-local-traffic enabled + if not self.connection_check(): - return False + return False, False response = self.path("/ip/accounting") if response is None: - return False + return False, False for item in response: if 'enabled' not in item: continue - if item['enabled']: - return True + if not item['enabled']: + return False, False - return False - - def is_accounting_local_traffic_enabled(self) -> bool: - if not self.connection_check(): - return False - - accounting = self.path("/ip/accounting") - if accounting is None: - return False - - for item in accounting: + for item in response: if 'account-local-traffic' not in item: continue - if item['account-local-traffic']: - return True - return False + if not item['account-local-traffic']: + return True, False + + return True, True # --------------------------- # take_accounting_snapshot diff --git a/custom_components/mikrotik_router/strings.json b/custom_components/mikrotik_router/strings.json index 913af8a..8ff7b12 100644 --- a/custom_components/mikrotik_router/strings.json +++ b/custom_components/mikrotik_router/strings.json @@ -13,7 +13,6 @@ "password": "Password", "ssl": "Use SSL", "unit_of_measurement": "Unit of measurement", - "track_accounting": "Track accounting" } } }, @@ -23,7 +22,6 @@ "ssl_handshake_failure": "SSL handshake failure", "connection_timeout": "Mikrotik connection timeout.", "wrong_login": "Invalid user name or password.", - "accounting_disabled": "Accounting disabled in Mikrotik, cannot track." } }, "options": { From 3c44445cd14d2a4581a203684b606a0c7d3f5b8a Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 14:57:29 +0200 Subject: [PATCH 18/20] Fix for naming of static DNS entries without host-name and static DNS entry. Fallback value is not mac-address --- README.md | 5 ++--- custom_components/mikrotik_router/mikrotik_controller.py | 7 +++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e0da443..08f44c5 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,6 @@ You can add this integration several times for different devices. * "Port" - Leave at 0 for defaults * "Name of the integration" - Friendy name for this router * "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 ![Integration options](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/integration_options.png) @@ -67,7 +66,7 @@ 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. +Feature will be automaticaly used if accounting is enabled in Mikrotik. Feature is present in Winbox IP-Accounting. 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). @@ -75,4 +74,4 @@ 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 +4. Device's MAC address diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 9776030..a65cfc7 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -788,9 +788,12 @@ class MikrotikControllerData: # Then static DNS entry elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] - # If everything fails use hosts DHCP lease host-name - else: + # Then DHCP lease host-name + elif str(vals.get('host-name', '').strip()): self.data["accounting"][mac]['name'] = vals['host-name'] + # If everything fails fallback to device's MAC address + else: + self.data["accounting"][mac]['name'] = vals['mac-address'] _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") From a9b72434e2fb635000c5542aa3916704e92aee70 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 15:07:23 +0200 Subject: [PATCH 19/20] Remove trailing commas --- custom_components/mikrotik_router/.translations/en.json | 4 ++-- custom_components/mikrotik_router/.translations/ru.json | 4 ++-- custom_components/mikrotik_router/strings.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/mikrotik_router/.translations/en.json b/custom_components/mikrotik_router/.translations/en.json index 8e4a33d..65057da 100644 --- a/custom_components/mikrotik_router/.translations/en.json +++ b/custom_components/mikrotik_router/.translations/en.json @@ -12,7 +12,7 @@ "username": "Username", "password": "Password", "ssl": "Use SSL", - "unit_of_measurement": "Unit of measurement", + "unit_of_measurement": "Unit of measurement" } } }, @@ -21,7 +21,7 @@ "cannot_connect": "Cannot connect to Mikrotik.", "ssl_handshake_failure": "SSL handshake failure.", "connection_timeout": "Mikrotik connection timeout.", - "wrong_login": "Invalid username or password.", + "wrong_login": "Invalid username or password." } }, "options": { diff --git a/custom_components/mikrotik_router/.translations/ru.json b/custom_components/mikrotik_router/.translations/ru.json index 3a2f5fe..2cda4b9 100644 --- a/custom_components/mikrotik_router/.translations/ru.json +++ b/custom_components/mikrotik_router/.translations/ru.json @@ -12,7 +12,7 @@ "username": "Имя пользователя", "password": "Пароль", "ssl": "Использовать SSL", - "unit_of_measurement": "Единицы измерения", + "unit_of_measurement": "Единицы измерения" } } }, @@ -21,7 +21,7 @@ "cannot_connect": "Нет связи с Mikrotik.", "ssl_handshake_failure": "Ошибка SSL-соединения", "connection_timeout": "Таймаут подключения к Mikrotik.", - "wrong_login": "Неверные имя пользователя или пароль.", + "wrong_login": "Неверные имя пользователя или пароль." } }, "options": { diff --git a/custom_components/mikrotik_router/strings.json b/custom_components/mikrotik_router/strings.json index 8ff7b12..ec26cf2 100644 --- a/custom_components/mikrotik_router/strings.json +++ b/custom_components/mikrotik_router/strings.json @@ -12,7 +12,7 @@ "username": "Username", "password": "Password", "ssl": "Use SSL", - "unit_of_measurement": "Unit of measurement", + "unit_of_measurement": "Unit of measurement" } } }, @@ -21,7 +21,7 @@ "cannot_connect": "Cannot connect to Mikrotik.", "ssl_handshake_failure": "SSL handshake failure", "connection_timeout": "Mikrotik connection timeout.", - "wrong_login": "Invalid user name or password.", + "wrong_login": "Invalid user name or password." } }, "options": { From cd9e30682ccd22afd779ae46eda5073624bd9332 Mon Sep 17 00:00:00 2001 From: Ivan Pavlina Date: Tue, 7 Apr 2020 20:31:16 +0200 Subject: [PATCH 20/20] Add comment as accounting sensor attribute --- .../mikrotik_router/mikrotik_controller.py | 64 ++++++++++--------- custom_components/mikrotik_router/sensor.py | 10 +-- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index a65cfc7..1e752f6 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -739,18 +739,20 @@ class MikrotikControllerData: """Get Accounting data from Mikrotik""" # Check if accounting and account-local-traffic is enabled accounting_enabled, local_traffic_enabled = self.api.is_accounting_and_local_traffic_enabled() + traffic_type, traffic_div = self._get_traffic_type_and_div() if not accounting_enabled: # If any hosts were created return counters to 0 so sensors wont get stuck on last value for mac, vals in self.data["accounting"].items(): + self.data['accounting'][mac]["tx-rx-attr"] = traffic_type if 'wan-tx' in vals: - self.data["accounting"]['wan-tx'] = 0.0 + self.data["accounting"][mac]['wan-tx'] = 0.0 if 'wan-rx' in vals: - self.data["accounting"]['wan-rx'] = 0.0 + self.data["accounting"][mac]['wan-rx'] = 0.0 if 'lan-tx' in vals: - self.data["accounting"]['lan-tx'] = 0.0 + self.data["accounting"][mac]['lan-tx'] = 0.0 if 'lan-rx' in vals: - self.data["accounting"]['lan-rx'] = 0.0 + self.data["accounting"][mac]['lan-rx'] = 0.0 return # Build missing hosts from already retrieved DHCP Server leases @@ -760,44 +762,48 @@ class MikrotikControllerData: 'address': vals['address'], 'mac-address': vals['mac-address'], 'host-name': vals['host-name'], + 'comment': vals['comment'] } # Build missing hosts from already retrieved ARP list + host_update_from_arp = False for entry in self.raw_arp_entries: if entry['mac-address'] not in self.data["accounting"]: self.data["accounting"][entry['mac-address']] = { 'address': entry['address'], 'mac-address': entry['mac-address'], + 'host-name': '', + 'comment': '' } + host_update_from_arp = True - # Build name for host - dns_data = parse_api( - data={}, - source=self.api.path("/ip/dns/static"), - key="address", - vals=[ - {"name": "address"}, - {"name": "name"}, - ], - ) + # If some host was added from ARP table build new host-name for it from static DNS entry. Fallback to MAC + if host_update_from_arp: + dns_data = parse_api( + data={}, + source=self.api.path("/ip/dns/static"), + key="address", + vals=[ + {"name": "address"}, + {"name": "name"}, + ], + ) + # Try to build hostname from DNS static entry + for mac, vals in self.data["accounting"].items(): + if not str(vals.get('host-name', '')).strip() or vals['host-name'] is 'unknown': + if vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '')).strip(): + self.data["accounting"][mac]['host-name'] = dns_data[vals['address']]['name'] + + # Check if any host still have empty 'host-name'. Default it to MAC. + # Same check for 'comment' (pretty name) for mac, vals in self.data["accounting"].items(): - # First try getting DHCP lease comment - if str(vals.get('comment', '').strip()): - self.data["accounting"][mac]['name'] = vals['comment'] - # Then static DNS entry - elif vals['address'] in dns_data and str(dns_data[vals['address']].get('name', '').strip()): - self.data["accounting"][mac]['name'] = dns_data[vals['address']]['name'] - # Then DHCP lease host-name - elif str(vals.get('host-name', '').strip()): - self.data["accounting"][mac]['name'] = vals['host-name'] - # If everything fails fallback to device's MAC address - else: - self.data["accounting"][mac]['name'] = vals['mac-address'] + if not str(vals.get('host-name', '')).strip() or vals['host-name'] is 'unknown': + self.data["accounting"][mac]['host-name'] = mac + if not str(vals.get('comment', '')).strip() or vals['host-name'] is 'comment': + self.data["accounting"][mac]['comment'] = mac - _LOGGER.debug(f"Generated {len(self.data['accounting'])} accounting devices") - - traffic_type, traffic_div = self._get_traffic_type_and_div() + _LOGGER.debug(f"Working with {len(self.data['accounting'])} accounting devices") # Build temp accounting values dict with ip address as key # Also set traffic type for each item diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index a95f936..1ddf128 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -124,6 +124,7 @@ SENSOR_TYPES = { DEVICE_ATTRIBUTES_ACCOUNTING = [ "address", "mac-address", + "comment" ] @@ -194,7 +195,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, sensors): if "accounting_" in sensor: for uid in mikrotik_controller.data["accounting"]: - item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['name']}" + item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['mac-address']}" if item_id in sensors: if sensors[item_id].enabled: sensors[item_id].async_schedule_update_ha_state() @@ -370,7 +371,7 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): @property def name(self): """Return the name.""" - return f"{self._inst} {self._data['name']} {self._type[ATTR_LABEL]}" + return f"{self._inst} {self._data['host-name']} {self._type[ATTR_LABEL]} " @property def unique_id(self): @@ -409,8 +410,9 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): async def async_added_to_hass(self): """Port entity created.""" _LOGGER.debug( - "New sensor %s (%s %s)", + "New sensor %s (%s [%s] %s)", self._inst, - self._data["name"], + self._data["host-name"], + self._data["mac-address"], self._sensor, )