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
* Monitor and control PPP users
* 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
* System sensors (CPU, Memory, HDD, Temperature)
* 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)
## Client Traffic
## Accounting
*RouterOS 7+ no longer support accounting*
#### RouterOS v6
###### 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.
#### 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)
# Install integration
@ -132,14 +147,12 @@ This integration is distributed using [HACS](https://hacs.xyz/).
You can find it under "Integrations", named "Mikrotik Router"
Minimum requirements:
* RouterOS v6.43
* RouterOS v6.43/v7.1
* Home Assistant 0.114.0
## Using Mikrotik development branch
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.
* beta 7.3 was fully tested.
* beta 7.4 have been reported to miss system health information.
## Setup integration
1. Create user for homeassistant on your mikrotik router with following permissions:

View file

@ -129,10 +129,12 @@ class MikrotikControllerData:
"wireless_hosts": {},
"host": {},
"host_hass": {},
"accounting": {},
"client_traffic": {},
"environment": {},
}
self.notified_flags = []
self.listeners = []
self.lock = asyncio.Lock()
self.lock_ping = asyncio.Lock()
@ -612,12 +614,11 @@ 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
):
if self.api.connected() and self.option_sensor_client_traffic:
if 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_kid_control_devices)
if self.api.connected() and self.option_sensor_simple_queues:
await self.hass.async_add_executor_job(self.get_queue)
@ -952,6 +953,8 @@ class MikrotikControllerData:
{"name": "src-port", "default": "any"},
{"name": "dst-address", "default": "any"},
{"name": "dst-port", "default": "any"},
{"name": "src-address-list", "default": "any"},
{"name": "dst-address-list", "default": "any"},
{
"name": "enabled",
"source": "disabled",
@ -976,6 +979,10 @@ class MikrotikControllerData:
{"key": "dst-address"},
{"text": ":"},
{"key": "dst-port"},
{"text": ","},
{"key": "src-address-list"},
{"text": "-"},
{"key": "dst-address-list"},
],
[
{"name": "name"},
@ -1869,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"],
@ -1879,11 +1886,13 @@ 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,
@ -1891,7 +1900,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={},
@ -1959,20 +1968,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
@ -1982,12 +1991,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
@ -2027,7 +2036,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
@ -2044,3 +2053,85 @@ 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"
)
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.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,14 +644,15 @@ 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
if use_accounting:
accounting = self.path("/ip/accounting", return_list=False)
self.lock.acquire()
@ -697,11 +698,11 @@ class MikrotikAPI:
# 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

View file

@ -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"]
# ---------------------------
@ -305,7 +305,7 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
new_sensors.append(sensors[item_id])
for sensor in SENSOR_TYPES:
if "system_" in sensor:
if sensor.startswith("system_"):
if (
SENSOR_TYPES[sensor][ATTR_ATTR]
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])
if "traffic_" in sensor:
if sensor.startswith("traffic_"):
if not config_entry.options.get(
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])
if "accounting_" in sensor:
for uid in mikrotik_controller.data["accounting"]:
item_id = f"{inst}-{sensor}-{mikrotik_controller.data['accounting'][uid]['mac-address']}"
if sensor.startswith("client_traffic_"):
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()
@ -360,9 +360,9 @@ def update_items(inst, config_entry, mikrotik_controller, async_add_entities, se
if (
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,
inst=inst,
sensor=sensor,
@ -534,10 +534,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."""
@ -585,7 +585,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]

View file

@ -619,7 +619,8 @@ class MikrotikControllerMangleSwitch(MikrotikControllerSwitch):
if self._ctrl.data["mangle"][uid]["uniq-id"] == (
f"{self._data['chain']},{self._data['action']},{self._data['protocol']},"
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"]
@ -637,7 +638,8 @@ class MikrotikControllerMangleSwitch(MikrotikControllerSwitch):
if self._ctrl.data["mangle"][uid]["uniq-id"] == (
f"{self._data['chain']},{self._data['action']},{self._data['protocol']},"
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"]