Accounting feature renamed to Client traffic. In case of older firmware fallback to accounting fetature for client traffic monitoring, instead use kid-control-devices

This commit is contained in:
Ivan Pavlina 2022-01-01 17:26:40 +00:00
parent 0120409382
commit e93b02e71d
3 changed files with 225 additions and 100 deletions

View file

@ -129,8 +129,8 @@ class MikrotikControllerData:
"wireless_hosts": {}, "wireless_hosts": {},
"host": {}, "host": {},
"host_hass": {}, "host_hass": {},
"accounting": {}, "client_traffic": {},
"environment": {}, "environment": {}
} }
self.listeners = [] self.listeners = []
@ -612,12 +612,13 @@ class MikrotikControllerData:
if self.api.connected(): if self.api.connected():
await self.hass.async_add_executor_job(self.get_system_resource) await self.hass.async_add_executor_job(self.get_system_resource)
if ( if self.api.connected() and self.option_sensor_client_traffic:
self.api.connected() if 0 < self.major_fw_version < 7:
and self.option_sensor_client_traffic _LOGGER.info("Using accounting feature for client traffic processing")
and 0 < self.major_fw_version < 7 await self.hass.async_add_executor_job(self.process_accounting)
): elif 0 < self.major_fw_version >= 7:
await self.hass.async_add_executor_job(self.process_accounting) _LOGGER.info("Using accounting kid control devices for client traffic processing")
await self.hass.async_add_executor_job(self.process_kid_control_devices)
if self.api.connected() and self.option_sensor_simple_queues: if self.api.connected() and self.option_sensor_simple_queues:
await self.hass.async_add_executor_job(self.get_queue) await self.hass.async_add_executor_job(self.get_queue)
@ -1875,8 +1876,8 @@ class MikrotikControllerData:
# Build missing hosts from main hosts dict # Build missing hosts from main hosts dict
for uid, vals in self.data["host"].items(): for uid, vals in self.data["host"].items():
if uid not in self.data["accounting"]: if uid not in self.data["client_traffic"]:
self.data["accounting"][uid] = { self.data["client_traffic"][uid] = {
"address": vals["address"], "address": vals["address"],
"mac-address": vals["mac-address"], "mac-address": vals["mac-address"],
"host-name": vals["host-name"], "host-name": vals["host-name"],
@ -1885,11 +1886,11 @@ class MikrotikControllerData:
"local_accounting": False, "local_accounting": False,
} }
_LOGGER.debug(f"Working with {len(self.data['accounting'])} accounting devices") _LOGGER.debug(f"Working with {len(self.data['client_traffic'])} accounting devices")
# Build temp accounting values dict with ip address as key # Build temp accounting values dict with ip address as key
tmp_accounting_values = {} tmp_accounting_values = {}
for uid, vals in self.data["accounting"].items(): for uid, vals in self.data["client_traffic"].items():
tmp_accounting_values[vals["address"]] = { tmp_accounting_values[vals["address"]] = {
"wan-tx": 0, "wan-tx": 0,
"wan-rx": 0, "wan-rx": 0,
@ -1897,7 +1898,7 @@ class MikrotikControllerData:
"lan-rx": 0, "lan-rx": 0,
} }
time_diff = self.api.take_accounting_snapshot() time_diff = self.api.take_client_traffic_snapshot(True)
if time_diff: if time_diff:
accounting_data = parse_api( accounting_data = parse_api(
data={}, data={},
@ -1965,20 +1966,20 @@ class MikrotikControllerData:
) )
continue continue
self.data["accounting"][uid]["tx-rx-attr"] = uom_type self.data["client_traffic"][uid]["tx-rx-attr"] = uom_type
self.data["accounting"][uid]["available"] = accounting_enabled self.data["client_traffic"][uid]["available"] = accounting_enabled
self.data["accounting"][uid]["local_accounting"] = local_traffic_enabled self.data["client_traffic"][uid]["local_accounting"] = local_traffic_enabled
if not accounting_enabled: if not accounting_enabled:
# Skip calculation for WAN and LAN if accounting is disabled # Skip calculation for WAN and LAN if accounting is disabled
continue continue
self.data["accounting"][uid]["wan-tx"] = ( self.data["client_traffic"][uid]["wan-tx"] = (
round(vals["wan-tx"] / time_diff * uom_div, 2) round(vals["wan-tx"] / time_diff * uom_div, 2)
if vals["wan-tx"] if vals["wan-tx"]
else 0.0 else 0.0
) )
self.data["accounting"][uid]["wan-rx"] = ( self.data["client_traffic"][uid]["wan-rx"] = (
round(vals["wan-rx"] / time_diff * uom_div, 2) round(vals["wan-rx"] / time_diff * uom_div, 2)
if vals["wan-rx"] if vals["wan-rx"]
else 0.0 else 0.0
@ -1988,12 +1989,12 @@ class MikrotikControllerData:
# Skip calculation for LAN if LAN accounting is disabled # Skip calculation for LAN if LAN accounting is disabled
continue continue
self.data["accounting"][uid]["lan-tx"] = ( self.data["client_traffic"][uid]["lan-tx"] = (
round(vals["lan-tx"] / time_diff * uom_div, 2) round(vals["lan-tx"] / time_diff * uom_div, 2)
if vals["lan-tx"] if vals["lan-tx"]
else 0.0 else 0.0
) )
self.data["accounting"][uid]["lan-rx"] = ( self.data["client_traffic"][uid]["lan-rx"] = (
round(vals["lan-rx"] / time_diff * uom_div, 2) round(vals["lan-rx"] / time_diff * uom_div, 2)
if vals["lan-rx"] if vals["lan-rx"]
else 0.0 else 0.0
@ -2033,7 +2034,7 @@ class MikrotikControllerData:
# _get_accounting_uid_by_ip # _get_accounting_uid_by_ip
# --------------------------- # ---------------------------
def _get_accounting_uid_by_ip(self, requested_ip): def _get_accounting_uid_by_ip(self, requested_ip):
for mac, vals in self.data["accounting"].items(): for mac, vals in self.data["client_traffic"].items():
if vals.get("address") is requested_ip: if vals.get("address") is requested_ip:
return mac return mac
return None return None
@ -2050,3 +2051,125 @@ class MikrotikControllerData:
break break
return uid return uid
# ---------------------------
# process_kid_control
# ---------------------------
def process_kid_control_devices(self):
"""Get Kid Control Device data from Mikrotik"""
uom_type, uom_div = self._get_unit_of_measurement()
# Build missing hosts from main hosts dict
for uid, vals in self.data["host"].items():
if uid not in self.data["client_traffic"]:
self.data["client_traffic"][uid] = {
"address": vals["address"],
"mac-address": vals["mac-address"],
"host-name": vals["host-name"],
"previous-bytes-up": 0.0,
"previous-bytes-down": 0.0,
"wan-tx": 0.0,
"wan-rx": 0.0,
"tx-rx-attr": uom_type,
"available": False,
"local_accounting": False,
}
_LOGGER.debug(f"Working with {len(self.data['client_traffic'])} kid control devices")
time_diff = self.api.take_client_traffic_snapshot(False)
if not time_diff:
return
kid_control_devices_data = parse_api(
data={},
source=self.api.path("/ip/kid-control/device"),
key="mac-address",
vals=[
{"name": "mac-address"},
{"name": "bytes-down"},
{"name": "bytes-up"},
{
"name": "enabled",
"source": "disabled",
"type": "bool",
"reverse": True,
}
]
)
if not kid_control_devices_data:
_LOGGER.debug("No kid control devices found, make sure kid-control feature is configured")
for uid, vals in kid_control_devices_data.items():
if uid not in self.data["client_traffic"]:
_LOGGER.debug(f"Skipping unknown device {uid}")
continue
self.data["client_traffic"][uid]["available"] = vals['enabled']
current_tx = vals['bytes-up']
previous_tx = self.data["client_traffic"][uid]['previous-bytes-up']
delta_tx = max(0, current_tx - previous_tx)
self.data["client_traffic"][uid]['wan-tx'] = round(delta_tx / time_diff * uom_div, 2)
self.data["client_traffic"][uid]['previous-bytes-up'] = current_tx
current_rx = vals['bytes-down']
previous_rx = self.data["client_traffic"][uid]['previous-bytes-down']
delta_rx = max(0, current_rx - previous_rx)
self.data["client_traffic"][uid]['wan-rx'] = round(delta_rx / time_diff * uom_div, 2)
self.data["client_traffic"][uid]['previous-bytes-down'] = current_rx
# ---------------------------
# _get_unit_of_measurement
# ---------------------------
def _get_unit_of_measurement(self):
uom_type = self.option_unit_of_measurement
if uom_type == "Kbps":
uom_div = 0.001
elif uom_type == "Mbps":
uom_div = 0.000001
elif uom_type == "B/s":
uom_div = 0.125
elif uom_type == "KB/s":
uom_div = 0.000125
elif uom_type == "MB/s":
uom_div = 0.000000125
else:
uom_type = "bps"
uom_div = 1
return uom_type, uom_div
# ---------------------------
# _address_part_of_local_network
# ---------------------------
def _address_part_of_local_network(self, address):
address = ip_address(address)
for vals in self.data["dhcp-network"].values():
if address in vals["IPv4Network"]:
return True
return False
# ---------------------------
# _get_accounting_uid_by_ip
# ---------------------------
def _get_accounting_uid_by_ip(self, requested_ip):
for mac, vals in self.data["client_traffic"].items():
if vals.get("address") is requested_ip:
return mac
return None
# ---------------------------
# _get_iface_from_entry
# ---------------------------
def _get_iface_from_entry(self, entry):
"""Get interface default-name using name from interface dict"""
uid = None
for ifacename in self.data["interface"]:
if self.data["interface"][ifacename]["name"] == entry["interface"]:
uid = ifacename
break
return uid

View file

@ -52,7 +52,7 @@ class MikrotikAPI:
self._connection_retry_sec = 58 self._connection_retry_sec = 58
self.error = None self.error = None
self.connection_error_reported = False self.connection_error_reported = False
self.accounting_last_run = None self.client_traffic_last_run = None
# Default ports # Default ports
if not self._port: if not self._port:
@ -644,64 +644,65 @@ class MikrotikAPI:
return True, True return True, True
# --------------------------- # ---------------------------
# take_accounting_snapshot # take_client_traffic_snapshot
# Returns float -> period in seconds between last and current run # Returns float -> period in seconds between last and current run
# --------------------------- # ---------------------------
def take_accounting_snapshot(self) -> float: def take_client_traffic_snapshot(self, use_accounting) -> float:
"""Get accounting data""" """Tako accounting snapshot and return time diff"""
if not self.connection_check(): if not self.connection_check():
return 0 return 0
accounting = self.path("/ip/accounting", return_list=False) if use_accounting:
accounting = self.path("/ip/accounting", return_list=False)
self.lock.acquire() self.lock.acquire()
try: try:
# Prepare command # Prepare command
take = accounting("snapshot/take") take = accounting("snapshot/take")
except librouteros.exceptions.ConnectionClosed: except librouteros.exceptions.ConnectionClosed:
self.disconnect() self.disconnect()
self.lock.release() self.lock.release()
return 0 return 0
except ( except (
librouteros.exceptions.TrapError, librouteros.exceptions.TrapError,
librouteros.exceptions.MultiTrapError, librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ProtocolError, librouteros.exceptions.ProtocolError,
librouteros.exceptions.FatalError, librouteros.exceptions.FatalError,
socket_timeout, socket_timeout,
socket_error, socket_error,
ssl.SSLError, ssl.SSLError,
BrokenPipeError, BrokenPipeError,
OSError, OSError,
ValueError, ValueError,
) as api_error: ) as api_error:
self.disconnect("accounting_snapshot", api_error) self.disconnect("accounting_snapshot", api_error)
self.lock.release() self.lock.release()
return 0 return 0
except: except:
self.disconnect("accounting_snapshot") self.disconnect("accounting_snapshot")
self.lock.release() self.lock.release()
return 0 return 0
try: try:
list(take) list(take)
except librouteros.exceptions.ConnectionClosed as api_error: except librouteros.exceptions.ConnectionClosed as api_error:
self.disconnect("accounting_snapshot", api_error) self.disconnect("accounting_snapshot", api_error)
self.lock.release() self.lock.release()
return 0 return 0
except: except:
self.disconnect("accounting_snapshot") self.disconnect("accounting_snapshot")
self.lock.release() self.lock.release()
return 0 return 0
self.lock.release() self.lock.release()
# First request will be discarded because we cannot know when the last data was retrieved # First request will be discarded because we cannot know when the last data was retrieved
# prevents spikes in data # prevents spikes in data
if not self.accounting_last_run: if not self.client_traffic_last_run:
self.accounting_last_run = self._current_milliseconds() self.client_traffic_last_run = self._current_milliseconds()
return 0 return 0
# Calculate time difference in seconds and return # Calculate time difference in seconds and return
time_diff = self._current_milliseconds() - self.accounting_last_run time_diff = self._current_milliseconds() - self.client_traffic_last_run
self.accounting_last_run = self._current_milliseconds() self.client_traffic_last_run = self._current_milliseconds()
return time_diff / 1000 return time_diff / 1000

View file

@ -186,49 +186,49 @@ SENSOR_TYPES = {
ATTR_ATTR: "rx-bits-per-second", ATTR_ATTR: "rx-bits-per-second",
ATTR_CTGR: None, ATTR_CTGR: None,
}, },
"accounting_lan_tx": { "client_traffic_lan_tx": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:upload-network", ATTR_ICON: "mdi:upload-network",
ATTR_LABEL: "LAN TX", ATTR_LABEL: "LAN TX",
ATTR_UNIT: "ps", ATTR_UNIT: "ps",
ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_UNIT_ATTR: "tx-rx-attr",
ATTR_PATH: "accounting", ATTR_PATH: "client_traffic",
ATTR_ATTR: "lan-tx", ATTR_ATTR: "lan-tx",
ATTR_CTGR: None, ATTR_CTGR: None,
}, },
"accounting_lan_rx": { "client_traffic_lan_rx": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:download-network", ATTR_ICON: "mdi:download-network",
ATTR_LABEL: "LAN RX", ATTR_LABEL: "LAN RX",
ATTR_UNIT: "ps", ATTR_UNIT: "ps",
ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_UNIT_ATTR: "tx-rx-attr",
ATTR_PATH: "accounting", ATTR_PATH: "client_traffic",
ATTR_ATTR: "lan-rx", ATTR_ATTR: "lan-rx",
ATTR_CTGR: None, ATTR_CTGR: None,
}, },
"accounting_wan_tx": { "client_traffic_wan_tx": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:upload-network", ATTR_ICON: "mdi:upload-network",
ATTR_LABEL: "WAN TX", ATTR_LABEL: "WAN TX",
ATTR_UNIT: "ps", ATTR_UNIT: "ps",
ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_UNIT_ATTR: "tx-rx-attr",
ATTR_PATH: "accounting", ATTR_PATH: "client_traffic",
ATTR_ATTR: "wan-tx", ATTR_ATTR: "wan-tx",
ATTR_CTGR: None, ATTR_CTGR: None,
}, },
"accounting_wan_rx": { "client_traffic_wan_rx": {
ATTR_DEVICE_CLASS: None, ATTR_DEVICE_CLASS: None,
ATTR_ICON: "mdi:download-network", ATTR_ICON: "mdi:download-network",
ATTR_LABEL: "WAN RX", ATTR_LABEL: "WAN RX",
ATTR_UNIT: "ps", ATTR_UNIT: "ps",
ATTR_UNIT_ATTR: "tx-rx-attr", ATTR_UNIT_ATTR: "tx-rx-attr",
ATTR_PATH: "accounting", ATTR_PATH: "client_traffic",
ATTR_ATTR: "wan-rx", ATTR_ATTR: "wan-rx",
ATTR_CTGR: None, ATTR_CTGR: None,
}, },
} }
DEVICE_ATTRIBUTES_ACCOUNTING = ["address", "mac-address", "host-name"] DEVICE_ATTRIBUTES_CLIENT_TRAFFIC = ["address", "mac-address", "host-name"]
# --------------------------- # ---------------------------
@ -327,6 +327,26 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
) )
new_sensors.append(sensors[item_id]) new_sensors.append(sensors[item_id])
if "client_traffic_" in sensor:
for uid in mikrotik_controller.data["client_traffic"]:
item_id = f"{inst}-{sensor}-{mikrotik_controller.data['client_traffic'][uid]['mac-address']}"
if item_id in sensors:
if sensors[item_id].enabled:
sensors[item_id].async_schedule_update_ha_state()
continue
if (
SENSOR_TYPES[sensor][ATTR_ATTR]
in mikrotik_controller.data["client_traffic"][uid].keys()
):
sensors[item_id] = MikrotikClientTrafficSensor(
mikrotik_controller=mikrotik_controller,
inst=inst,
sensor=sensor,
uid=uid,
)
new_sensors.append(sensors[item_id])
if "traffic_" in sensor: if "traffic_" in sensor:
if not config_entry.options.get( if not config_entry.options.get(
CONF_SENSOR_PORT_TRAFFIC, DEFAULT_SENSOR_PORT_TRAFFIC CONF_SENSOR_PORT_TRAFFIC, DEFAULT_SENSOR_PORT_TRAFFIC
@ -350,25 +370,6 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
) )
new_sensors.append(sensors[item_id]) new_sensors.append(sensors[item_id])
if "accounting_" in sensor:
for uid in mikrotik_controller.data["accounting"]:
item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['mac-address']}"
if item_id in sensors:
if sensors[item_id].enabled:
sensors[item_id].async_schedule_update_ha_state()
continue
if (
SENSOR_TYPES[sensor][ATTR_ATTR]
in mikrotik_controller.data["accounting"][uid].keys()
):
sensors[item_id] = MikrotikAccountingSensor(
mikrotik_controller=mikrotik_controller,
inst=inst,
sensor=sensor,
uid=uid,
)
new_sensors.append(sensors[item_id])
if new_sensors: if new_sensors:
async_add_entities(new_sensors, True) async_add_entities(new_sensors, True)
@ -532,10 +533,10 @@ class MikrotikControllerTrafficSensor(MikrotikControllerSensor):
# --------------------------- # ---------------------------
# MikrotikAccountingSensor # MikrotikClientTrafficSensor
# --------------------------- # ---------------------------
class MikrotikAccountingSensor(MikrotikControllerSensor): class MikrotikClientTrafficSensor(MikrotikControllerSensor):
"""Define an Mikrotik Accounting sensor.""" """Define an Mikrotik MikrotikClientTrafficSensor sensor."""
def __init__(self, mikrotik_controller, inst, sensor, uid): def __init__(self, mikrotik_controller, inst, sensor, uid):
"""Initialize.""" """Initialize."""
@ -583,7 +584,7 @@ class MikrotikAccountingSensor(MikrotikControllerSensor):
def extra_state_attributes(self) -> Dict[str, Any]: def extra_state_attributes(self) -> Dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
attributes = self._attrs attributes = self._attrs
for variable in DEVICE_ATTRIBUTES_ACCOUNTING: for variable in DEVICE_ATTRIBUTES_CLIENT_TRAFFIC:
if variable in self._data: if variable in self._data:
attributes[format_attribute(variable)] = self._data[variable] attributes[format_attribute(variable)] = self._data[variable]