Merge remote-tracking branch 'origin/master'

This commit is contained in:
Tomaae 2022-01-21 15:50:42 +01:00
commit 8d3e86a988
5 changed files with 202 additions and 95 deletions

View file

@ -47,7 +47,7 @@ Monitor and control your Mikrotik device from Home Assistant.
* Enable/disable Filter switches * Enable/disable Filter switches
* Monitor and control PPP users * Monitor and control PPP users
* Kid Control * Kid Control
* Mikrotik Accounting traffic sensors per hosts for RX/TX WAN/LAN * Client Traffic RX/TX WAN/LAN monitoring though Accounting or Kid Control Devices (depending on RouterOS FW version)
* Device tracker for hosts in network * Device tracker for hosts in network
* System sensors (CPU, Memory, HDD, Temperature) * System sensors (CPU, Memory, HDD, Temperature)
* Check firmware update * Check firmware update
@ -112,9 +112,10 @@ Monitor and control.
![Kid Control](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/kidcontrol_switch.png) ![Kid Control](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/kidcontrol_switch.png)
## Client Traffic
## Accounting #### RouterOS v6
*RouterOS 7+ no longer support accounting* ###### Accounting
Monitor per-IP throughput tracking based on Mikrotik Accounting. Monitor per-IP throughput tracking based on Mikrotik Accounting.
@ -124,6 +125,20 @@ More information about Accounting can be found on [Mikrotik support page](https:
NOTE: Accounting does not count in FastTracked packets. NOTE: Accounting does not count in FastTracked packets.
#### RouterOS v7
###### Kid Control Devices
In RouterOS v7 Accounting feature is deprecated so alternative approach for is to use
Kid Control Devices feature (IP - Kid Control - Devices).
This feature requires at least one 'kid' to be defined,
after that Mikrotik will dynamically start tracking bandwidth usage of all known devices.
Simple dummy Kid entry can be defined with
```/ip kid-control add name=Monitor mon=0s-1d tue=0s-1d wed=0s-1d thu=0s-1d fri=0s-1d sat=0s-1d sun=0s-1d```
![Accounting sensor](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/accounting_sensor.png) ![Accounting sensor](https://raw.githubusercontent.com/tomaae/homeassistant-mikrotik_router/master/docs/assets/images/ui/accounting_sensor.png)
# Install integration # Install integration
@ -132,14 +147,12 @@ This integration is distributed using [HACS](https://hacs.xyz/).
You can find it under "Integrations", named "Mikrotik Router" You can find it under "Integrations", named "Mikrotik Router"
Minimum requirements: Minimum requirements:
* RouterOS v6.43 * RouterOS v6.43/v7.1
* Home Assistant 0.114.0 * Home Assistant 0.114.0
## Using Mikrotik development branch ## Using Mikrotik development branch
If you are using development branch for mikrotik, some features may stop working due to major changes in RouterOS. If you are using development branch for mikrotik, some features may stop working due to major changes in RouterOS.
Use integration master branch instead of latest release to keep up with RouterOS beta adjustments. Use integration master branch instead of latest release to keep up with RouterOS beta adjustments.
* beta 7.3 was fully tested.
* beta 7.4 have been reported to miss system health information.
## Setup integration ## Setup integration
1. Create user for homeassistant on your mikrotik router with following permissions: 1. Create user for homeassistant on your mikrotik router with following permissions:

View file

@ -129,10 +129,12 @@ class MikrotikControllerData:
"wireless_hosts": {}, "wireless_hosts": {},
"host": {}, "host": {},
"host_hass": {}, "host_hass": {},
"accounting": {}, "client_traffic": {},
"environment": {}, "environment": {},
} }
self.notified_flags = []
self.listeners = [] self.listeners = []
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.lock_ping = asyncio.Lock() self.lock_ping = asyncio.Lock()
@ -612,12 +614,11 @@ 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
and 0 < self.major_fw_version < 7
):
await self.hass.async_add_executor_job(self.process_accounting) 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_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)
@ -952,6 +953,8 @@ class MikrotikControllerData:
{"name": "src-port", "default": "any"}, {"name": "src-port", "default": "any"},
{"name": "dst-address", "default": "any"}, {"name": "dst-address", "default": "any"},
{"name": "dst-port", "default": "any"}, {"name": "dst-port", "default": "any"},
{"name": "src-address-list", "default": "any"},
{"name": "dst-address-list", "default": "any"},
{ {
"name": "enabled", "name": "enabled",
"source": "disabled", "source": "disabled",
@ -976,6 +979,10 @@ class MikrotikControllerData:
{"key": "dst-address"}, {"key": "dst-address"},
{"text": ":"}, {"text": ":"},
{"key": "dst-port"}, {"key": "dst-port"},
{"text": ","},
{"key": "src-address-list"},
{"text": "-"},
{"key": "dst-address-list"},
], ],
[ [
{"name": "name"}, {"name": "name"},
@ -1869,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"],
@ -1879,11 +1886,13 @@ 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,
@ -1891,7 +1900,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={},
@ -1959,20 +1968,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
@ -1982,12 +1991,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
@ -2027,7 +2036,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
@ -2044,3 +2053,85 @@ 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"
)
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,
},
],
)
time_diff = self.api.take_client_traffic_snapshot(False)
if not kid_control_devices_data:
if "kid-control-devices" not in self.notified_flags:
_LOGGER.error(
"No kid control devices found on your Mikrotik device, make sure kid-control feature is configured"
)
self.notified_flags.append("kid-control-devices")
return
elif "kid-control-devices" in self.notified_flags:
self.notified_flags.remove("kid-control-devices")
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"]
if time_diff:
delta_tx = max(0, current_tx - previous_tx) * 8
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"]
if time_diff:
delta_rx = max(0, current_rx - previous_rx) * 8
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

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,14 +644,15 @@ 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
if use_accounting:
accounting = self.path("/ip/accounting", return_list=False) accounting = self.path("/ip/accounting", return_list=False)
self.lock.acquire() self.lock.acquire()
@ -697,11 +698,11 @@ class MikrotikAPI:
# 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"]
# --------------------------- # ---------------------------
@ -305,7 +305,7 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
new_sensors.append(sensors[item_id]) new_sensors.append(sensors[item_id])
for sensor in SENSOR_TYPES: for sensor in SENSOR_TYPES:
if "system_" in sensor: if sensor.startswith("system_"):
if ( if (
SENSOR_TYPES[sensor][ATTR_ATTR] SENSOR_TYPES[sensor][ATTR_ATTR]
not in mikrotik_controller.data[SENSOR_TYPES[sensor][ATTR_PATH]] not in mikrotik_controller.data[SENSOR_TYPES[sensor][ATTR_PATH]]
@ -327,7 +327,7 @@ 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 "traffic_" in sensor: if sensor.startswith("traffic_"):
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,9 +350,9 @@ 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: if sensor.startswith("client_traffic_"):
for uid in mikrotik_controller.data["accounting"]: for uid in mikrotik_controller.data["client_traffic"]:
item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['mac-address']}" item_id = f"{inst}-{sensor}-{mikrotik_controller.data['client_traffic'][uid]['mac-address']}"
if item_id in sensors: if item_id in sensors:
if sensors[item_id].enabled: if sensors[item_id].enabled:
sensors[item_id].async_schedule_update_ha_state() sensors[item_id].async_schedule_update_ha_state()
@ -360,9 +360,9 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
if ( if (
SENSOR_TYPES[sensor][ATTR_ATTR] SENSOR_TYPES[sensor][ATTR_ATTR]
in mikrotik_controller.data["accounting"][uid].keys() in mikrotik_controller.data["client_traffic"][uid].keys()
): ):
sensors[item_id] = MikrotikAccountingSensor( sensors[item_id] = MikrotikClientTrafficSensor(
mikrotik_controller=mikrotik_controller, mikrotik_controller=mikrotik_controller,
inst=inst, inst=inst,
sensor=sensor, sensor=sensor,
@ -534,10 +534,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."""
@ -585,7 +585,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]

View file

@ -619,7 +619,8 @@ class MikrotikControllerMangleSwitch(MikrotikControllerSwitch):
if self._ctrl.data["mangle"][uid]["uniq-id"] == ( if self._ctrl.data["mangle"][uid]["uniq-id"] == (
f"{self._data['chain']},{self._data['action']},{self._data['protocol']}," f"{self._data['chain']},{self._data['action']},{self._data['protocol']},"
f"{self._data['src-address']}:{self._data['src-port']}-" f"{self._data['src-address']}:{self._data['src-port']}-"
f"{self._data['dst-address']}:{self._data['dst-port']}" f"{self._data['dst-address']}:{self._data['dst-port']},"
f"{self._data['src-address-list']}-{self._data['dst-address-list']}"
): ):
value = self._ctrl.data["mangle"][uid][".id"] value = self._ctrl.data["mangle"][uid][".id"]
@ -637,7 +638,8 @@ class MikrotikControllerMangleSwitch(MikrotikControllerSwitch):
if self._ctrl.data["mangle"][uid]["uniq-id"] == ( if self._ctrl.data["mangle"][uid]["uniq-id"] == (
f"{self._data['chain']},{self._data['action']},{self._data['protocol']}," f"{self._data['chain']},{self._data['action']},{self._data['protocol']},"
f"{self._data['src-address']}:{self._data['src-port']}-" f"{self._data['src-address']}:{self._data['src-port']}-"
f"{self._data['dst-address']}:{self._data['dst-port']}" f"{self._data['dst-address']}:{self._data['dst-port']},"
f"{self._data['src-address-list']}-{self._data['dst-address-list']}"
): ):
value = self._ctrl.data["mangle"][uid][".id"] value = self._ctrl.data["mangle"][uid][".id"]