diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index d8dbcf2..e0c7706 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -129,8 +129,8 @@ class MikrotikControllerData: "wireless_hosts": {}, "host": {}, "host_hass": {}, - "accounting": {}, - "environment": {}, + "client_traffic": {}, + "environment": {} } self.listeners = [] @@ -612,12 +612,13 @@ class MikrotikControllerData: if self.api.connected(): await self.hass.async_add_executor_job(self.get_system_resource) - if ( - self.api.connected() - and self.option_sensor_client_traffic - and 0 < self.major_fw_version < 7 - ): - await self.hass.async_add_executor_job(self.process_accounting) + if self.api.connected() and self.option_sensor_client_traffic: + if 0 < self.major_fw_version < 7: + _LOGGER.info("Using accounting feature for client traffic processing") + await self.hass.async_add_executor_job(self.process_accounting) + elif 0 < self.major_fw_version >= 7: + _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: await self.hass.async_add_executor_job(self.get_queue) @@ -1875,8 +1876,8 @@ class MikrotikControllerData: # Build missing hosts from main hosts dict for uid, vals in self.data["host"].items(): - if uid not in self.data["accounting"]: - self.data["accounting"][uid] = { + 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"], @@ -1885,11 +1886,11 @@ class MikrotikControllerData: "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 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"]] = { "wan-tx": 0, "wan-rx": 0, @@ -1897,7 +1898,7 @@ class MikrotikControllerData: "lan-rx": 0, } - time_diff = self.api.take_accounting_snapshot() + time_diff = self.api.take_client_traffic_snapshot(True) if time_diff: accounting_data = parse_api( data={}, @@ -1965,20 +1966,20 @@ class MikrotikControllerData: ) continue - self.data["accounting"][uid]["tx-rx-attr"] = uom_type - self.data["accounting"][uid]["available"] = accounting_enabled - self.data["accounting"][uid]["local_accounting"] = local_traffic_enabled + self.data["client_traffic"][uid]["tx-rx-attr"] = uom_type + self.data["client_traffic"][uid]["available"] = accounting_enabled + self.data["client_traffic"][uid]["local_accounting"] = local_traffic_enabled if not accounting_enabled: # Skip calculation for WAN and LAN if accounting is disabled continue - self.data["accounting"][uid]["wan-tx"] = ( + self.data["client_traffic"][uid]["wan-tx"] = ( round(vals["wan-tx"] / time_diff * uom_div, 2) if vals["wan-tx"] 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) if vals["wan-rx"] else 0.0 @@ -1988,12 +1989,12 @@ class MikrotikControllerData: # Skip calculation for LAN if LAN accounting is disabled continue - self.data["accounting"][uid]["lan-tx"] = ( + self.data["client_traffic"][uid]["lan-tx"] = ( round(vals["lan-tx"] / time_diff * uom_div, 2) if vals["lan-tx"] 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) if vals["lan-rx"] else 0.0 @@ -2033,7 +2034,7 @@ class MikrotikControllerData: # _get_accounting_uid_by_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: return mac return None @@ -2050,3 +2051,125 @@ class MikrotikControllerData: break 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 + diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index 9da31f6..d6c8ec3 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -52,7 +52,7 @@ class MikrotikAPI: self._connection_retry_sec = 58 self.error = None self.connection_error_reported = False - self.accounting_last_run = None + self.client_traffic_last_run = None # Default ports if not self._port: @@ -644,64 +644,65 @@ class MikrotikAPI: return True, True # --------------------------- - # take_accounting_snapshot + # take_client_traffic_snapshot # Returns float -> period in seconds between last and current run # --------------------------- - def take_accounting_snapshot(self) -> float: - """Get accounting data""" + def take_client_traffic_snapshot(self, use_accounting) -> float: + """Tako accounting snapshot and return time diff""" if not self.connection_check(): return 0 - accounting = self.path("/ip/accounting", return_list=False) + if use_accounting: + accounting = self.path("/ip/accounting", return_list=False) - self.lock.acquire() - try: - # Prepare command - take = accounting("snapshot/take") - except librouteros.exceptions.ConnectionClosed: - self.disconnect() - self.lock.release() - return 0 - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ProtocolError, - librouteros.exceptions.FatalError, - socket_timeout, - socket_error, - 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 + self.lock.acquire() + try: + # Prepare command + take = accounting("snapshot/take") + except librouteros.exceptions.ConnectionClosed: + self.disconnect() + self.lock.release() + return 0 + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, + socket_timeout, + socket_error, + 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 - try: - list(take) - except librouteros.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 + try: + list(take) + except librouteros.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 - self.lock.release() + 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() + if not self.client_traffic_last_run: + self.client_traffic_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() + time_diff = self._current_milliseconds() - self.client_traffic_last_run + self.client_traffic_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 7e4c3b3..3c3eede 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -186,49 +186,49 @@ SENSOR_TYPES = { ATTR_ATTR: "rx-bits-per-second", ATTR_CTGR: None, }, - "accounting_lan_tx": { + "client_traffic_lan_tx": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "LAN TX", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", - ATTR_PATH: "accounting", + ATTR_PATH: "client_traffic", ATTR_ATTR: "lan-tx", ATTR_CTGR: None, }, - "accounting_lan_rx": { + "client_traffic_lan_rx": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:download-network", ATTR_LABEL: "LAN RX", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", - ATTR_PATH: "accounting", + ATTR_PATH: "client_traffic", ATTR_ATTR: "lan-rx", ATTR_CTGR: None, }, - "accounting_wan_tx": { + "client_traffic_wan_tx": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:upload-network", ATTR_LABEL: "WAN TX", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", - ATTR_PATH: "accounting", + ATTR_PATH: "client_traffic", ATTR_ATTR: "wan-tx", ATTR_CTGR: None, }, - "accounting_wan_rx": { + "client_traffic_wan_rx": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:download-network", ATTR_LABEL: "WAN RX", ATTR_UNIT: "ps", ATTR_UNIT_ATTR: "tx-rx-attr", - ATTR_PATH: "accounting", + ATTR_PATH: "client_traffic", ATTR_ATTR: "wan-rx", 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]) + 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 not config_entry.options.get( 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]) - 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: async_add_entities(new_sensors, True) @@ -532,10 +533,10 @@ class MikrotikControllerTrafficSensor(MikrotikControllerSensor): # --------------------------- -# MikrotikAccountingSensor +# MikrotikClientTrafficSensor # --------------------------- -class MikrotikAccountingSensor(MikrotikControllerSensor): - """Define an Mikrotik Accounting sensor.""" +class MikrotikClientTrafficSensor(MikrotikControllerSensor): + """Define an Mikrotik MikrotikClientTrafficSensor sensor.""" def __init__(self, mikrotik_controller, inst, sensor, uid): """Initialize.""" @@ -583,7 +584,7 @@ class MikrotikAccountingSensor(MikrotikControllerSensor): def extra_state_attributes(self) -> Dict[str, Any]: """Return the state attributes.""" attributes = self._attrs - for variable in DEVICE_ATTRIBUTES_ACCOUNTING: + for variable in DEVICE_ATTRIBUTES_CLIENT_TRAFFIC: if variable in self._data: attributes[format_attribute(variable)] = self._data[variable]