coordinator update

This commit is contained in:
Tomaae 2023-08-08 00:50:09 +02:00
parent ce9ce906f5
commit a21e95c36a
No known key found for this signature in database
GPG key ID: 2F97770867DAA4E6
14 changed files with 445 additions and 496 deletions

View file

@ -1,80 +1,62 @@
"""Mikrotik Router integration."""
from __future__ import annotations
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import device_registry
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from .const import (
PLATFORMS,
DOMAIN,
RUN_SCRIPT_COMMAND,
)
from .mikrotik_controller import MikrotikControllerData
from .coordinator import MikrotikCoordinator
SCRIPT_SCHEMA = vol.Schema(
{vol.Required("router"): cv.string, vol.Required("script"): cv.string}
)
# ---------------------------
# async_setup
# ---------------------------
async def async_setup(hass, _config):
"""Set up configured Mikrotik Controller."""
hass.data[DOMAIN] = {}
return True
# ---------------------------
# update_listener
# ---------------------------
async def update_listener(hass, config_entry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
# ---------------------------
# async_setup_entry
# ---------------------------
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up Mikrotik Router as config entry."""
controller = MikrotikControllerData(hass, config_entry)
await controller.async_hwinfo_update()
if not controller.connected():
raise ConfigEntryNotReady("Cannot connect to host")
await controller.async_update()
if not controller.data:
raise ConfigEntryNotReady()
await controller.async_init()
hass.data[DOMAIN][config_entry.entry_id] = controller
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up a config entry."""
coordinator = MikrotikCoordinator(hass, config_entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry))
hass.services.async_register(
DOMAIN, RUN_SCRIPT_COMMAND, controller.run_script, schema=SCRIPT_SCHEMA
DOMAIN, RUN_SCRIPT_COMMAND, coordinator.run_script, schema=SCRIPT_SCHEMA
)
return True
# ---------------------------
# async_reload_entry
# ---------------------------
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(config_entry.entry_id)
# ---------------------------
# async_unload_entry
# ---------------------------
async def async_unload_entry(hass, config_entry) -> bool:
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
controller = hass.data[DOMAIN][config_entry.entry_id]
await controller.async_reset()
):
hass.services.async_remove(DOMAIN, RUN_SCRIPT_COMMAND)
hass.data[DOMAIN].pop(config_entry.entry_id)

View file

@ -1,9 +1,12 @@
"""API parser for JSON APIs"""
from pytz import utc
from logging import getLogger
"""API parser for JSON APIs."""
from datetime import datetime
from logging import getLogger
from pytz import utc
from voluptuous import Optional
from homeassistant.components.diagnostics import async_redact_data
from .const import TO_REDACT
_LOGGER = getLogger(__name__)
@ -13,7 +16,7 @@ _LOGGER = getLogger(__name__)
# utc_from_timestamp
# ---------------------------
def utc_from_timestamp(timestamp: float) -> datetime:
"""Return a UTC time from a timestamp"""
"""Return a UTC time from a timestamp."""
return utc.localize(datetime.utcfromtimestamp(timestamp))
@ -21,7 +24,7 @@ def utc_from_timestamp(timestamp: float) -> datetime:
# from_entry
# ---------------------------
def from_entry(entry, param, default="") -> str:
"""Validate and return str value an API dict"""
"""Validate and return str value an API dict."""
if "/" in param:
for tmp_param in param.split("/"):
if isinstance(entry, dict) and tmp_param in entry:
@ -50,7 +53,7 @@ def from_entry(entry, param, default="") -> str:
# from_entry_bool
# ---------------------------
def from_entry_bool(entry, param, default=False, reverse=False) -> bool:
"""Validate and return a bool value from an API dict"""
"""Validate and return a bool value from an API dict."""
if "/" in param:
for tmp_param in param.split("/"):
if isinstance(entry, dict) and tmp_param in entry:
@ -91,8 +94,11 @@ def parse_api(
only=None,
skip=None,
) -> dict:
"""Get data from API"""
"""Get data from API."""
debug = _LOGGER.getEffectiveLevel() == 10
if type(source) == dict:
tmp = source
source = [tmp]
if not source:
if not key and not key_search:
@ -138,7 +144,7 @@ def parse_api(
# get_uid
# ---------------------------
def get_uid(entry, key, key_secondary, key_search, keymap) -> Optional(str):
"""Get UID for data list"""
"""Get UID for data list."""
uid = None
if not key_search:
key_primary_found = key in entry
@ -167,7 +173,7 @@ def get_uid(entry, key, key_secondary, key_search, keymap) -> Optional(str):
# generate_keymap
# ---------------------------
def generate_keymap(data, key_search) -> Optional(dict):
"""Generate keymap"""
"""Generate keymap."""
return (
{data[uid][key_search]: uid for uid in data if key_search in data[uid]}
if key_search
@ -179,7 +185,7 @@ def generate_keymap(data, key_search) -> Optional(dict):
# matches_only
# ---------------------------
def matches_only(entry, only) -> bool:
"""Return True if all variables are matched"""
"""Return True if all variables are matched."""
ret = False
for val in only:
if val["key"] in entry and entry[val["key"]] == val["value"]:
@ -195,7 +201,7 @@ def matches_only(entry, only) -> bool:
# can_skip
# ---------------------------
def can_skip(entry, skip) -> bool:
"""Return True if at least one variable matches"""
"""Return True if at least one variable matches."""
ret = False
for val in skip:
if val["name"] in entry and entry[val["name"]] == val["value"]:
@ -213,7 +219,7 @@ def can_skip(entry, skip) -> bool:
# fill_defaults
# ---------------------------
def fill_defaults(data, vals) -> dict:
"""Fill defaults if source is not present"""
"""Fill defaults if source is not present."""
for val in vals:
_name = val["name"]
_type = val["type"] if "type" in val else "str"
@ -242,7 +248,7 @@ def fill_defaults(data, vals) -> dict:
# fill_vals
# ---------------------------
def fill_vals(data, entry, uid, vals) -> dict:
"""Fill all data"""
"""Fill all data."""
for val in vals:
_name = val["name"]
_type = val["type"] if "type" in val else "str"
@ -292,7 +298,7 @@ def fill_vals(data, entry, uid, vals) -> dict:
# fill_ensure_vals
# ---------------------------
def fill_ensure_vals(data, uid, ensure_vals) -> dict:
"""Add required keys which are not available in data"""
"""Add required keys which are not available in data."""
for val in ensure_vals:
if uid:
if val["name"] not in data[uid]:
@ -310,7 +316,7 @@ def fill_ensure_vals(data, uid, ensure_vals) -> dict:
# fill_vals_proc
# ---------------------------
def fill_vals_proc(data, uid, vals_proc) -> dict:
"""Add custom keys"""
"""Add custom keys."""
_data = data[uid] if uid else data
for val_sub in vals_proc:
_name = None

View file

@ -13,7 +13,7 @@ from .const import (
CONF_SENSOR_PORT_TRACKER,
DEFAULT_SENSOR_PORT_TRACKER,
)
from .model import model_async_setup_entry, MikrotikEntity
from .entity import model_async_setup_entry, MikrotikEntity
from .binary_sensor_types import (
SENSOR_TYPES,
SENSOR_SERVICES,

View file

@ -2,7 +2,7 @@
import logging
from homeassistant.components.button import ButtonEntity
from .model import model_async_setup_entry, MikrotikEntity
from .entity import model_async_setup_entry, MikrotikEntity
from .button_types import (
SENSOR_TYPES,
SENSOR_SERVICES,

View file

@ -12,7 +12,6 @@ from homeassistant.const import (
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SSL,
@ -55,8 +54,6 @@ from .const import (
DEFAULT_SENSOR_ENVIRONMENT,
CONF_TRACK_HOSTS_TIMEOUT,
DEFAULT_TRACK_HOST_TIMEOUT,
LIST_UNIT_OF_MEASUREMENT,
DEFAULT_UNIT_OF_MEASUREMENT,
DEFAULT_HOST,
DEFAULT_USERNAME,
DEFAULT_PORT,
@ -193,12 +190,6 @@ class MikrotikControllerOptionsFlowHandler(OptionsFlow):
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): int,
vol.Optional(
CONF_UNIT_OF_MEASUREMENT,
default=self.config_entry.options.get(
CONF_UNIT_OF_MEASUREMENT, DEFAULT_UNIT_OF_MEASUREMENT
),
): vol.In(LIST_UNIT_OF_MEASUREMENT),
vol.Optional(
CONF_TRACK_IFACE_CLIENTS,
default=self.config_entry.options.get(

View file

@ -3,11 +3,11 @@ from homeassistant.const import Platform
PLATFORMS = [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.SWITCH,
Platform.BUTTON,
Platform.UPDATE,
# Platform.BINARY_SENSOR,
# Platform.DEVICE_TRACKER,
# Platform.SWITCH,
# Platform.BUTTON,
# Platform.UPDATE,
]
DOMAIN = "mikrotik_router"
@ -27,8 +27,6 @@ DEFAULT_SSL = False
CONF_SCAN_INTERVAL = "scan_interval"
DEFAULT_SCAN_INTERVAL = 30
LIST_UNIT_OF_MEASUREMENT = ["bps", "Kbps", "Mbps", "B/s", "KB/s", "MB/s"]
DEFAULT_UNIT_OF_MEASUREMENT = "Kbps"
CONF_TRACK_IFACE_CLIENTS = "track_iface_clients"
DEFAULT_TRACK_IFACE_CLIENTS = True
CONF_TRACK_HOSTS = "track_network_hosts"

View file

@ -1,26 +1,31 @@
"""Mikrotik Controller for Mikrotik Router."""
"""Mikrotik coordinator."""
from __future__ import annotations
import asyncio
import ipaddress
import logging
import re
import pytz
from typing import Any
from datetime import datetime, timedelta
from ipaddress import ip_address, IPv4Network
from mac_vendor_lookup import AsyncMacLookup
from homeassistant.core import callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers import entity_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.dt import utcnow
from homeassistant.const import (
CONF_NAME,
CONF_HOST,
CONF_PORT,
CONF_UNIT_OF_MEASUREMENT,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SSL,
@ -36,7 +41,6 @@ from .const import (
DEFAULT_TRACK_HOSTS,
CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL,
DEFAULT_UNIT_OF_MEASUREMENT,
CONF_SENSOR_PORT_TRAFFIC,
DEFAULT_SENSOR_PORT_TRAFFIC,
CONF_SENSOR_CLIENT_TRAFFIC,
@ -95,13 +99,21 @@ def as_local(dattim: datetime) -> datetime:
# ---------------------------
# MikrotikControllerData
# ---------------------------
class MikrotikControllerData:
"""MikrotikController Class"""
class MikrotikCoordinator(DataUpdateCoordinator):
"""MikrotikCoordinator Class"""
def __init__(self, hass, config_entry):
"""Initialize MikrotikController."""
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
"""Initialize MikrotikCoordinator."""
self.hass = hass
self.config_entry = config_entry
self.config_entry: ConfigEntry = config_entry
super().__init__(
self.hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(
seconds=10
), # update_interval=self.option_scan_interval
)
self.name = config_entry.data[CONF_NAME]
self.host = config_entry.data[CONF_HOST]
@ -143,7 +155,7 @@ class MikrotikControllerData:
self.notified_flags = []
self.listeners = []
# self.listeners = []
self.lock = asyncio.Lock()
self.lock_ping = asyncio.Lock()
@ -185,22 +197,32 @@ class MikrotikControllerData:
self.async_mac_lookup = AsyncMacLookup()
self.accessrights_reported = False
async def async_init(self):
self.listeners.append(
async_track_time_interval(
self.hass, self.force_update, self.option_scan_interval
)
)
self.listeners.append(
async_track_time_interval(
self.hass, self.force_fwupdate_check, timedelta(hours=4)
)
)
self.listeners.append(
async_track_time_interval(
self.hass, self.async_ping_tracked_hosts, timedelta(seconds=15)
)
)
# self.listeners.append(
# async_track_time_interval(
# self.hass, self.force_fwupdate_check, timedelta(hours=4)
# )
# )
# self.listeners.append(
# async_track_time_interval(
# self.hass, self.async_ping_tracked_hosts, timedelta(seconds=15)
# )
# )
# ---------------------------
# async_init
# ---------------------------
# async def async_init(self):
# print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
# self.listeners.append(
# async_track_time_interval(
# self.hass, self.force_fwupdate_check, timedelta(hours=4)
# )
# )
# self.listeners.append(
# async_track_time_interval(
# self.hass, self.async_ping_tracked_hosts, timedelta(seconds=15)
# )
# )
# ---------------------------
# option_track_iface_clients
@ -333,16 +355,6 @@ class MikrotikControllerData:
)
return timedelta(seconds=scan_interval)
# ---------------------------
# option_unit_of_measurement
# ---------------------------
@property
def option_unit_of_measurement(self):
"""Config entry option to not track ARP."""
return self.config_entry.options.get(
CONF_UNIT_OF_MEASUREMENT, DEFAULT_UNIT_OF_MEASUREMENT
)
# ---------------------------
# option_zone
# ---------------------------
@ -351,25 +363,6 @@ class MikrotikControllerData:
"""Config entry option zones."""
return self.config_entry.options.get(CONF_ZONE, STATE_HOME)
# ---------------------------
# signal_update
# ---------------------------
@property
def signal_update(self):
"""Event to signal new data."""
return f"{DOMAIN}-update-{self.name}"
# ---------------------------
# async_reset
# ---------------------------
async def async_reset(self):
"""Reset dispatchers"""
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
self.listeners = []
return True
# ---------------------------
# connected
# ---------------------------
@ -490,8 +483,8 @@ class MikrotikControllerData:
"""Update Mikrotik hardware info"""
try:
await asyncio.wait_for(self.lock.acquire(), timeout=30)
except Exception:
return
except Exception as error:
raise UpdateFailed(error) from error
await self.hass.async_add_executor_job(self.get_access)
@ -532,7 +525,6 @@ class MikrotikControllerData:
async def async_fwupdate_check(self):
"""Update Mikrotik data"""
await self.hass.async_add_executor_job(self.get_firmware_update)
async_dispatcher_send(self.hass, self.signal_update)
# ---------------------------
# async_ping_tracked_hosts
@ -596,25 +588,20 @@ class MikrotikControllerData:
self.lock_ping.release()
# ---------------------------
# force_update
# _async_update_data
# ---------------------------
@callback
async def force_update(self, _now=None):
"""Trigger update by timer"""
await self.async_update()
# ---------------------------
# async_update
# ---------------------------
async def async_update(self):
async def _async_update_data(self):
"""Update Mikrotik data"""
print(
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
)
if self.api.has_reconnected():
await self.async_hwinfo_update()
try:
await asyncio.wait_for(self.lock.acquire(), timeout=10)
except Exception:
return
except Exception as error:
raise UpdateFailed(error) from error
await self.hass.async_add_executor_job(self.get_system_resource)
@ -693,13 +680,14 @@ class MikrotikControllerData:
if self.api.connected() and self.support_gps:
await self.hass.async_add_executor_job(self.get_gps)
async_dispatcher_send(self.hass, self.signal_update)
self.lock.release()
# async_dispatcher_send(self.hass, "update_sensors", self)
return self.data
# ---------------------------
# get_access
# ---------------------------
def get_access(self):
def get_access(self) -> None:
"""Get access rights from Mikrotik"""
tmp_user = parse_api(
data={},
@ -742,7 +730,7 @@ class MikrotikControllerData:
# ---------------------------
# get_interface
# ---------------------------
def get_interface(self):
def get_interface(self) -> None:
"""Get all interfaces data from Mikrotik"""
self.data["interface"] = parse_api(
data=self.data["interface"],
@ -794,30 +782,22 @@ class MikrotikControllerData:
)
if self.option_sensor_port_traffic:
uom_type, uom_div = self._get_unit_of_measurement()
for uid, vals in self.data["interface"].items():
self.data["interface"][uid]["rx-attr"] = uom_type
self.data["interface"][uid]["tx-attr"] = uom_type
current_tx = vals["tx-current"]
previous_tx = vals["tx-previous"]
if not previous_tx:
previous_tx = current_tx
previous_tx = vals["tx-previous"] or current_tx
delta_tx = max(0, current_tx - previous_tx) * 8
delta_tx = max(0, current_tx - previous_tx)
self.data["interface"][uid]["tx"] = round(
delta_tx / self.option_scan_interval.seconds * uom_div, 2
delta_tx / self.option_scan_interval.seconds
)
self.data["interface"][uid]["tx-previous"] = current_tx
current_rx = vals["rx-current"]
previous_rx = vals["rx-previous"]
if not previous_rx:
previous_rx = current_rx
previous_rx = vals["rx-previous"] or current_rx
delta_rx = max(0, current_rx - previous_rx) * 8
delta_rx = max(0, current_rx - previous_rx)
self.data["interface"][uid]["rx"] = round(
delta_rx / self.option_scan_interval.seconds * uom_div, 2
delta_rx / self.option_scan_interval.seconds
)
self.data["interface"][uid]["rx-previous"] = current_rx
@ -914,7 +894,7 @@ class MikrotikControllerData:
# ---------------------------
# get_bridge
# ---------------------------
def get_bridge(self):
def get_bridge(self) -> None:
"""Get system resources data from Mikrotik"""
self.data["bridge_host"] = parse_api(
data=self.data["bridge_host"],
@ -940,7 +920,7 @@ class MikrotikControllerData:
# ---------------------------
# process_interface_client
# ---------------------------
def process_interface_client(self):
def process_interface_client(self) -> None:
# Remove data if disabled
if not self.option_track_iface_clients:
for uid in self.data["interface"]:
@ -978,7 +958,7 @@ class MikrotikControllerData:
# ---------------------------
# get_nat
# ---------------------------
def get_nat(self):
def get_nat(self) -> None:
"""Get NAT data from Mikrotik"""
self.data["nat"] = parse_api(
data=self.data["nat"],
@ -1060,7 +1040,7 @@ class MikrotikControllerData:
# ---------------------------
# get_mangle
# ---------------------------
def get_mangle(self):
def get_mangle(self) -> None:
"""Get Mangle data from Mikrotik"""
self.data["mangle"] = parse_api(
data=self.data["mangle"],
@ -1154,7 +1134,7 @@ class MikrotikControllerData:
# ---------------------------
# get_filter
# ---------------------------
def get_filter(self):
def get_filter(self) -> None:
"""Get Filter data from Mikrotik"""
self.data["filter"] = parse_api(
data=self.data["filter"],
@ -1266,7 +1246,7 @@ class MikrotikControllerData:
# ---------------------------
# get_kidcontrol
# ---------------------------
def get_kidcontrol(self):
def get_kidcontrol(self) -> None:
"""Get Kid-control data from Mikrotik"""
self.data["kid-control"] = parse_api(
data=self.data["kid-control"],
@ -1302,7 +1282,7 @@ class MikrotikControllerData:
# ---------------------------
# get_ppp
# ---------------------------
def get_ppp(self):
def get_ppp(self) -> None:
"""Get PPP data from Mikrotik"""
self.data["ppp_secret"] = parse_api(
data=self.data["ppp_secret"],
@ -1366,7 +1346,7 @@ class MikrotikControllerData:
# ---------------------------
# get_system_routerboard
# ---------------------------
def get_system_routerboard(self):
def get_system_routerboard(self) -> None:
"""Get routerboard data from Mikrotik"""
if self.data["resource"]["board-name"] in ("x86", "CHR"):
self.data["routerboard"]["routerboard"] = False
@ -1396,7 +1376,7 @@ class MikrotikControllerData:
# ---------------------------
# get_system_health
# ---------------------------
def get_system_health(self):
def get_system_health(self) -> None:
"""Get routerboard data from Mikrotik"""
if (
"write" not in self.data["access"]
@ -1434,7 +1414,7 @@ class MikrotikControllerData:
# ---------------------------
# get_system_resource
# ---------------------------
def get_system_resource(self):
def get_system_resource(self) -> None:
"""Get system resources data from Mikrotik"""
tmp_rebootcheck = 0
if "uptime_epoch" in self.data["resource"]:
@ -1487,16 +1467,12 @@ class MikrotikControllerData:
if not self.data["resource"]["uptime"]:
update_uptime = True
else:
uptime_old = datetime.timestamp(
datetime.fromisoformat(self.data["resource"]["uptime"])
)
uptime_old = datetime.timestamp(self.data["resource"]["uptime"])
if uptime_tm > uptime_old + 10:
update_uptime = True
if update_uptime:
self.data["resource"]["uptime"] = str(
as_local(utc_from_timestamp(uptime_tm)).isoformat()
)
self.data["resource"]["uptime"] = utc_from_timestamp(uptime_tm)
if self.data["resource"]["total-memory"] > 0:
self.data["resource"]["memory-usage"] = round(
@ -1535,7 +1511,7 @@ class MikrotikControllerData:
# ---------------------------
# get_firmware_update
# ---------------------------
def get_firmware_update(self):
def get_firmware_update(self) -> None:
"""Check for firmware update on Mikrotik"""
if (
"write" not in self.data["access"]
@ -1579,7 +1555,7 @@ class MikrotikControllerData:
# ---------------------------
# get_ups
# ---------------------------
def get_ups(self):
def get_ups(self) -> None:
"""Get UPS info from Mikrotik"""
self.data["ups"] = parse_api(
data=self.data["ups"],
@ -1632,7 +1608,7 @@ class MikrotikControllerData:
# ---------------------------
# get_gps
# ---------------------------
def get_gps(self):
def get_gps(self) -> None:
"""Get GPS data from Mikrotik"""
self.data["gps"] = parse_api(
data=self.data["gps"],
@ -1659,7 +1635,7 @@ class MikrotikControllerData:
# ---------------------------
# get_script
# ---------------------------
def get_script(self):
def get_script(self) -> None:
"""Get list of all scripts from Mikrotik"""
self.data["script"] = parse_api(
data=self.data["script"],
@ -1675,7 +1651,7 @@ class MikrotikControllerData:
# ---------------------------
# get_environment
# ---------------------------
def get_environment(self):
def get_environment(self) -> None:
"""Get list of all environment variables from Mikrotik"""
self.data["environment"] = parse_api(
data=self.data["environment"],
@ -1690,7 +1666,7 @@ class MikrotikControllerData:
# ---------------------------
# get_captive
# ---------------------------
def get_captive(self):
def get_captive(self) -> None:
"""Get list of all environment variables from Mikrotik"""
self.data["hostspot_host"] = parse_api(
data={},
@ -1713,7 +1689,7 @@ class MikrotikControllerData:
# ---------------------------
# get_queue
# ---------------------------
def get_queue(self):
def get_queue(self) -> None:
"""Get Queue data from Mikrotik"""
self.data["queue"] = parse_api(
data=self.data["queue"],
@ -1741,59 +1717,50 @@ class MikrotikControllerData:
],
)
uom_type, uom_div = self._get_unit_of_measurement()
for uid, vals in self.data["queue"].items():
self.data["queue"][uid]["comment"] = str(self.data["queue"][uid]["comment"])
upload_max_limit_bps, download_max_limit_bps = [
int(x) for x in vals["max-limit"].split("/")
]
self.data["queue"][uid][
"upload-max-limit"
] = f"{round(upload_max_limit_bps * uom_div)} {uom_type}"
self.data["queue"][uid]["upload-max-limit"] = f"{upload_max_limit_bps} bps"
self.data["queue"][uid][
"download-max-limit"
] = f"{round(download_max_limit_bps * uom_div)} {uom_type}"
] = f"{download_max_limit_bps} bps"
upload_rate_bps, download_rate_bps = [
int(x) for x in vals["rate"].split("/")
]
self.data["queue"][uid][
"upload-rate"
] = f"{round(upload_rate_bps * uom_div)} {uom_type}"
self.data["queue"][uid][
"download-rate"
] = f"{round(download_rate_bps * uom_div)} {uom_type}"
self.data["queue"][uid]["upload-rate"] = f"{upload_rate_bps} bps"
self.data["queue"][uid]["download-rate"] = f"{download_rate_bps} bps"
upload_limit_at_bps, download_limit_at_bps = [
int(x) for x in vals["limit-at"].split("/")
]
self.data["queue"][uid][
"upload-limit-at"
] = f"{round(upload_limit_at_bps * uom_div)} {uom_type}"
self.data["queue"][uid]["upload-limit-at"] = f"{upload_limit_at_bps} bps"
self.data["queue"][uid][
"download-limit-at"
] = f"{round(download_limit_at_bps * uom_div)} {uom_type}"
] = f"{download_limit_at_bps} bps"
upload_burst_limit_bps, download_burst_limit_bps = [
int(x) for x in vals["burst-limit"].split("/")
]
self.data["queue"][uid][
"upload-burst-limit"
] = f"{round(upload_burst_limit_bps * uom_div)} {uom_type}"
] = f"{upload_burst_limit_bps} bps"
self.data["queue"][uid][
"download-burst-limit"
] = f"{round(download_burst_limit_bps * uom_div)} {uom_type}"
] = f"{download_burst_limit_bps} bps"
upload_burst_threshold_bps, download_burst_threshold_bps = [
int(x) for x in vals["burst-threshold"].split("/")
]
self.data["queue"][uid][
"upload-burst-threshold"
] = f"{round(upload_burst_threshold_bps * uom_div)} {uom_type}"
] = f"{upload_burst_threshold_bps} bps"
self.data["queue"][uid][
"download-burst-threshold"
] = f"{round(download_burst_threshold_bps * uom_div)} {uom_type}"
] = f"{download_burst_threshold_bps} bps"
upload_burst_time, download_burst_time = vals["burst-time"].split("/")
self.data["queue"][uid]["upload-burst-time"] = upload_burst_time
@ -1802,7 +1769,7 @@ class MikrotikControllerData:
# ---------------------------
# get_arp
# ---------------------------
def get_arp(self):
def get_arp(self) -> None:
"""Get ARP data from Mikrotik"""
self.data["arp"] = parse_api(
data=self.data["arp"],
@ -1835,7 +1802,7 @@ class MikrotikControllerData:
# ---------------------------
# get_dns
# ---------------------------
def get_dns(self):
def get_dns(self) -> None:
"""Get static DNS data from Mikrotik"""
self.data["dns"] = parse_api(
data=self.data["dns"],
@ -1850,7 +1817,7 @@ class MikrotikControllerData:
# ---------------------------
# get_dhcp
# ---------------------------
def get_dhcp(self):
def get_dhcp(self) -> None:
"""Get DHCP data from Mikrotik"""
self.data["dhcp"] = parse_api(
data=self.data["dhcp"],
@ -1926,7 +1893,7 @@ class MikrotikControllerData:
# ---------------------------
# get_dhcp_server
# ---------------------------
def get_dhcp_server(self):
def get_dhcp_server(self) -> None:
"""Get DHCP server data from Mikrotik"""
self.data["dhcp-server"] = parse_api(
data=self.data["dhcp-server"],
@ -1941,7 +1908,7 @@ class MikrotikControllerData:
# ---------------------------
# get_dhcp_client
# ---------------------------
def get_dhcp_client(self):
def get_dhcp_client(self) -> None:
"""Get DHCP client data from Mikrotik"""
self.data["dhcp-client"] = parse_api(
data=self.data["dhcp-client"],
@ -1956,7 +1923,7 @@ class MikrotikControllerData:
# ---------------------------
# get_dhcp_network
# ---------------------------
def get_dhcp_network(self):
def get_dhcp_network(self) -> None:
"""Get DHCP network data from Mikrotik"""
self.data["dhcp-network"] = parse_api(
data=self.data["dhcp-network"],
@ -1981,7 +1948,7 @@ class MikrotikControllerData:
# ---------------------------
# get_capsman_hosts
# ---------------------------
def get_capsman_hosts(self):
def get_capsman_hosts(self) -> None:
"""Get CAPS-MAN hosts data from Mikrotik"""
self.data["capsman_hosts"] = parse_api(
data={},
@ -1997,7 +1964,7 @@ class MikrotikControllerData:
# ---------------------------
# get_wireless
# ---------------------------
def get_wireless(self):
def get_wireless(self) -> None:
"""Get wireless data from Mikrotik"""
wifimodule = "wifiwave2" if self.support_wifiwave2 else "wireless"
self.data["wireless"] = parse_api(
@ -2047,7 +2014,7 @@ class MikrotikControllerData:
# ---------------------------
# get_wireless_hosts
# ---------------------------
def get_wireless_hosts(self):
def get_wireless_hosts(self) -> None:
"""Get wireless hosts data from Mikrotik"""
wifimodule = "wifiwave2" if self.support_wifiwave2 else "wireless"
self.data["wireless_hosts"] = parse_api(
@ -2065,7 +2032,7 @@ class MikrotikControllerData:
# ---------------------------
# async_process_host
# ---------------------------
async def async_process_host(self):
async def async_process_host(self) -> None:
"""Get host tracking data"""
# Add hosts from CAPS-MAN
capsman_detected = {}
@ -2281,14 +2248,13 @@ class MikrotikControllerData:
# ---------------------------
# process_accounting
# ---------------------------
def process_accounting(self):
def process_accounting(self) -> None:
"""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()
uom_type, uom_div = self._get_unit_of_measurement()
# Build missing hosts from main hosts dict
for uid, vals in self.data["host"].items():
@ -2297,7 +2263,6 @@ class MikrotikControllerData:
"address": vals["address"],
"mac-address": vals["mac-address"],
"host-name": vals["host-name"],
"tx-rx-attr": uom_type,
"available": False,
"local_accounting": False,
}
@ -2351,7 +2316,7 @@ class MikrotikControllerData:
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
bits_count = int(str(item.get("bytes")).strip())
if self._address_part_of_local_network(
source_ip
@ -2385,7 +2350,6 @@ class MikrotikControllerData:
)
continue
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
@ -2394,14 +2358,10 @@ class MikrotikControllerData:
continue
self.data["client_traffic"][uid]["wan-tx"] = (
round(vals["wan-tx"] / time_diff * uom_div, 2)
if vals["wan-tx"]
else 0.0
round(vals["wan-tx"] / time_diff) if vals["wan-tx"] else 0.0
)
self.data["client_traffic"][uid]["wan-rx"] = (
round(vals["wan-rx"] / time_diff * uom_div, 2)
if vals["wan-rx"]
else 0.0
round(vals["wan-rx"] / time_diff) if vals["wan-rx"] else 0.0
)
if not local_traffic_enabled:
@ -2409,40 +2369,16 @@ class MikrotikControllerData:
continue
self.data["client_traffic"][uid]["lan-tx"] = (
round(vals["lan-tx"] / time_diff * uom_div, 2)
if vals["lan-tx"]
else 0.0
round(vals["lan-tx"] / time_diff) if vals["lan-tx"] else 0.0
)
self.data["client_traffic"][uid]["lan-rx"] = (
round(vals["lan-rx"] / time_diff * uom_div, 2)
if vals["lan-rx"]
else 0.0
round(vals["lan-rx"] / time_diff) if vals["lan-rx"] else 0.0
)
# ---------------------------
# _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):
def _address_part_of_local_network(self, address) -> bool:
address = ip_address(address)
for vals in self.data["dhcp-network"].values():
if address in vals["IPv4Network"]:
@ -2474,11 +2410,9 @@ class MikrotikControllerData:
# ---------------------------
# process_kid_control
# ---------------------------
def process_kid_control_devices(self):
def process_kid_control_devices(self) -> None:
"""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"]:
@ -2490,7 +2424,6 @@ class MikrotikControllerData:
"previous-bytes-down": 0.0,
"tx": 0.0,
"rx": 0.0,
"tx-rx-attr": uom_type,
"available": False,
"local_accounting": False,
}
@ -2538,17 +2471,13 @@ class MikrotikControllerData:
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]["tx"] = round(
delta_tx / time_diff * uom_div, 2
)
delta_tx = max(0, current_tx - previous_tx)
self.data["client_traffic"][uid]["tx"] = round(delta_tx / time_diff)
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]["rx"] = round(
delta_rx / time_diff * uom_div, 2
)
delta_rx = max(0, current_rx - previous_rx)
self.data["client_traffic"][uid]["rx"] = round(delta_rx / time_diff)
self.data["client_traffic"][uid]["previous-bytes-down"] = current_rx

View file

@ -14,7 +14,7 @@ from .const import (
CONF_TRACK_HOSTS_TIMEOUT,
DEFAULT_TRACK_HOST_TIMEOUT,
)
from .model import model_async_setup_entry, MikrotikEntity
from .entity import model_async_setup_entry, MikrotikEntity
from .device_tracker_types import SENSOR_TYPES, SENSOR_SERVICES
_LOGGER = logging.getLogger(__name__)

View file

@ -1,13 +1,22 @@
"""Mikrotik HA shared entity model"""
from logging import getLogger
from typing import Any
from __future__ import annotations
from collections.abc import Mapping
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from logging import getLogger
from typing import Any, Callable
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_HOST
from .helper import format_attribute
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
entity_platform as ep,
entity_registry as er,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import (
DOMAIN,
ATTRIBUTION,
@ -18,14 +27,16 @@ from .const import (
CONF_SENSOR_PORT_TRACKER,
DEFAULT_SENSOR_PORT_TRACKER,
)
from .coordinator import MikrotikCoordinator
from .helper import format_attribute
_LOGGER = getLogger(__name__)
def _skip_sensor(config_entry, uid_sensor, uid_data, uid) -> bool:
def _skip_sensor(config_entry, entity_description, data, uid) -> bool:
# Sensors
if (
uid_sensor.func == "MikrotikInterfaceTrafficSensor"
entity_description.func == "MikrotikInterfaceTrafficSensor"
and not config_entry.options.get(
CONF_SENSOR_PORT_TRAFFIC, DEFAULT_SENSOR_PORT_TRAFFIC
)
@ -33,33 +44,36 @@ def _skip_sensor(config_entry, uid_sensor, uid_data, uid) -> bool:
return True
if (
uid_sensor.func == "MikrotikInterfaceTrafficSensor"
and uid_data[uid]["type"] == "bridge"
entity_description.func == "MikrotikInterfaceTrafficSensor"
and data[uid]["type"] == "bridge"
):
return True
if (
uid_sensor.func == "MikrotikClientTrafficSensor"
and uid_sensor.data_attribute not in uid_data[uid].keys()
entity_description.func == "MikrotikClientTrafficSensor"
and entity_description.data_attribute not in data[uid].keys()
):
return True
# Binary sensors
if (
uid_sensor.func == "MikrotikPortBinarySensor"
and uid_data[uid]["type"] == "wlan"
entity_description.func == "MikrotikPortBinarySensor"
and data[uid]["type"] == "wlan"
):
return True
if uid_sensor.func == "MikrotikPortBinarySensor" and not config_entry.options.get(
if (
entity_description.func == "MikrotikPortBinarySensor"
and not config_entry.options.get(
CONF_SENSOR_PORT_TRACKER, DEFAULT_SENSOR_PORT_TRACKER
)
):
return True
# Device Tracker
if (
# Skip if host tracking is disabled
uid_sensor.func == "MikrotikHostDeviceTracker"
entity_description.func == "MikrotikHostDeviceTracker"
and not config_entry.options.get(CONF_TRACK_HOSTS, DEFAULT_TRACK_HOSTS)
):
return True
@ -68,125 +82,99 @@ def _skip_sensor(config_entry, uid_sensor, uid_data, uid) -> bool:
# ---------------------------
# model_async_setup_entry
# async_add_entities
# ---------------------------
async def model_async_setup_entry(
hass, config_entry, async_add_entities, sensor_services, sensor_types, dispatcher
async def async_add_entities(
hass: HomeAssistant, config_entry: ConfigEntry, dispatcher: dict[str, Callable]
):
inst = config_entry.data[CONF_NAME]
mikrotik_controller = hass.data[DOMAIN][config_entry.entry_id]
sensors = {}
"""Add entities."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
platform = ep.async_get_current_platform()
services = platform.platform.SENSOR_SERVICES
descriptions = platform.platform.SENSOR_TYPES
platform = entity_platform.async_get_current_platform()
for service in sensor_services:
for service in services:
platform.async_register_entity_service(service[0], service[1], service[2])
@callback
def update_controller():
"""Update the values of the controller"""
model_update_items(
inst,
config_entry,
mikrotik_controller,
async_add_entities,
sensors,
dispatcher,
sensor_types,
)
async def async_update_controller(coordinator):
"""Update the values of the controller."""
mikrotik_controller.listeners.append(
async_dispatcher_connect(
hass, mikrotik_controller.signal_update, update_controller
)
)
update_controller()
# ---------------------------
# model_update_items
# ---------------------------
def model_update_items(
inst,
config_entry,
mikrotik_controller,
async_add_entities,
sensors,
dispatcher,
sensor_types,
):
def _register_entity(_sensors, _item_id, _uid, _uid_sensor):
_LOGGER.debug("Updating entity %s (%s)", inst, _item_id)
if _item_id in _sensors:
return None
return dispatcher[_uid_sensor.func](
inst=inst,
uid=_uid,
mikrotik_controller=mikrotik_controller,
entity_description=_uid_sensor,
)
new_sensors = []
for sensor in sensor_types:
uid_sensor = sensor_types[sensor]
if not uid_sensor.data_reference:
if (
uid_sensor.data_attribute
not in mikrotik_controller.data[uid_sensor.data_path]
or mikrotik_controller.data[uid_sensor.data_path][
uid_sensor.data_attribute
]
== "unknown"
):
continue
item_id = f"{inst}-{sensor}"
if tmp := _register_entity(sensors, item_id, "", uid_sensor):
sensors[item_id] = tmp
new_sensors.append(sensors[item_id])
async def async_check_exist(obj, coordinator, uid: None) -> None:
"""Check entity exists."""
entity_registry = er.async_get(hass)
if uid:
unique_id = f"{obj._inst.lower()}-{obj.entity_description.key}-{slugify(str(obj._data[obj.entity_description.data_reference]).lower())}"
else:
for uid in mikrotik_controller.data[uid_sensor.data_path]:
uid_data = mikrotik_controller.data[uid_sensor.data_path]
if _skip_sensor(config_entry, uid_sensor, uid_data, uid):
unique_id = f"{obj._inst.lower()}-{obj.entity_description.key}"
entity_id = entity_registry.async_get_entity_id(
platform.domain, DOMAIN, unique_id
)
entity = entity_registry.async_get(entity_id)
if entity is None or (
(entity_id not in platform.entities) and (entity.disabled is False)
):
_LOGGER.debug("Add entity %s", entity_id)
await platform.async_add_entities([obj])
for entity_description in descriptions:
data = coordinator.data[entity_description.data_path]
if not entity_description.data_reference:
if data.get(entity_description.data_attribute) is None:
continue
obj = dispatcher[entity_description.func](
coordinator, entity_description
)
await async_check_exist(obj, coordinator, None)
else:
for uid in data:
if _skip_sensor(config_entry, entity_description, data, uid):
continue
obj = dispatcher[entity_description.func](
coordinator, entity_description, uid
)
await async_check_exist(obj, coordinator, uid)
item_id = f"{inst}-{sensor}-{str(uid_data[uid][uid_sensor.data_reference]).lower()}"
if tmp := _register_entity(sensors, item_id, uid, uid_sensor):
sensors[item_id] = tmp
new_sensors.append(sensors[item_id])
if new_sensors:
async_add_entities(new_sensors, True)
await async_update_controller(coordinator)
unsub = async_dispatcher_connect(hass, "update_sensors", async_update_controller)
config_entry.async_on_unload(unsub)
# ---------------------------
# MikrotikEntity
# ---------------------------
class MikrotikEntity:
class MikrotikEntity(CoordinatorEntity[MikrotikCoordinator], Entity):
"""Define entity"""
_attr_has_entity_name = True
def __init__(
self,
inst,
uid: "",
mikrotik_controller,
coordinator: MikrotikCoordinator,
entity_description,
uid: str | None = None,
):
"""Initialize entity"""
super().__init__(coordinator)
self.coordinator = coordinator
self.entity_description = entity_description
self._inst = inst
self._ctrl = mikrotik_controller
self._config_entry = self._ctrl.config_entry
self._inst = coordinator.config_entry.data[CONF_NAME]
self._config_entry = self.coordinator.config_entry
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION}
self._uid = uid
self._data = coordinator.data[self.entity_description.data_path]
if self._uid:
self._data = mikrotik_controller.data[self.entity_description.data_path][
self._data = coordinator.data[self.entity_description.data_path][self._uid]
@callback
def _handle_coordinator_update(self) -> None:
self._data = self.coordinator.data[self.entity_description.data_path]
if self._uid:
self._data = self.coordinator.data[self.entity_description.data_path][
self._uid
]
else:
self._data = mikrotik_controller.data[self.entity_description.data_path]
super()._handle_coordinator_update()
@property
def name(self) -> str:
@ -215,14 +203,14 @@ class MikrotikEntity:
def unique_id(self) -> str:
"""Return a unique id for this entity"""
if self._uid:
return f"{self._inst.lower()}-{self.entity_description.key}-{str(self._data[self.entity_description.data_reference]).lower()}"
return f"{self._inst.lower()}-{self.entity_description.key}-{slugify(str(self._data[self.entity_description.data_reference]).lower())}"
else:
return f"{self._inst.lower()}-{self.entity_description.key}"
@property
def available(self) -> bool:
"""Return if controller is available"""
return self._ctrl.connected()
return self.coordinator.connected()
@property
def device_info(self) -> DeviceInfo:
@ -231,8 +219,8 @@ class MikrotikEntity:
dev_connection_value = self.entity_description.data_reference
dev_group = self.entity_description.ha_group
if self.entity_description.ha_group == "System":
dev_group = self._ctrl.data["resource"]["board-name"]
dev_connection_value = self._ctrl.data["routerboard"]["serial-number"]
dev_group = self.coordinator.data["resource"]["board-name"]
dev_connection_value = self.coordinator.data["routerboard"]["serial-number"]
if self.entity_description.ha_group.startswith("data__"):
dev_group = self.entity_description.ha_group[6:]
@ -253,19 +241,24 @@ class MikrotikEntity:
connections={(dev_connection, f"{dev_connection_value}")},
identifiers={(dev_connection, f"{dev_connection_value}")},
default_name=f"{self._inst} {dev_group}",
default_model=f"{self._ctrl.data['resource']['board-name']}",
default_manufacturer=f"{self._ctrl.data['resource']['platform']}",
sw_version=f"{self._ctrl.data['resource']['version']}",
configuration_url=f"http://{self._ctrl.config_entry.data[CONF_HOST]}",
via_device=(DOMAIN, f"{self._ctrl.data['routerboard']['serial-number']}"),
default_model=f"{self.coordinator.data['resource']['board-name']}",
default_manufacturer=f"{self.coordinator.data['resource']['platform']}",
sw_version=f"{self.coordinator.data['resource']['version']}",
configuration_url=f"http://{self.coordinator.config_entry.data[CONF_HOST]}",
via_device=(
DOMAIN,
f"{self.coordinator.data['routerboard']['serial-number']}",
),
)
if "mac-address" in self.entity_description.data_reference:
dev_group = self._data[self.entity_description.data_name]
dev_manufacturer = ""
if dev_connection_value in self._ctrl.data["host"]:
dev_group = self._ctrl.data["host"][dev_connection_value]["host-name"]
dev_manufacturer = self._ctrl.data["host"][dev_connection_value][
if dev_connection_value in self.coordinator.data["host"]:
dev_group = self.coordinator.data["host"][dev_connection_value][
"host-name"
]
dev_manufacturer = self.coordinator.data["host"][dev_connection_value][
"manufacturer"
]
@ -275,7 +268,7 @@ class MikrotikEntity:
default_manufacturer=f"{dev_manufacturer}",
via_device=(
DOMAIN,
f"{self._ctrl.data['routerboard']['serial-number']}",
f"{self.coordinator.data['routerboard']['serial-number']}",
),
)
@ -301,16 +294,16 @@ class MikrotikEntity:
async def start(self):
"""Dummy run function"""
_LOGGER.error("Start functionality does not exist for %s", self.unique_id)
raise NotImplementedError()
async def stop(self):
"""Dummy stop function"""
_LOGGER.error("Stop functionality does not exist for %s", self.unique_id)
raise NotImplementedError()
async def restart(self):
"""Dummy restart function"""
_LOGGER.error("Restart functionality does not exist for %s", self.unique_id)
raise NotImplementedError()
async def reload(self):
"""Dummy reload function"""
_LOGGER.error("Reload functionality does not exist for %s", self.unique_id)
raise NotImplementedError()

View file

@ -44,7 +44,7 @@ class MikrotikAPI:
self._connection = None
self._connected = False
self._reconnected = False
self._reconnected = True
self._connection_epoch = 0
self._connection_retry_sec = 58
self.error = None

View file

@ -1,11 +1,21 @@
"""Implementation of Mikrotik Router sensor entities."""
"""Mikrotik sensor platform."""
from __future__ import annotations
import logging
from typing import Any, Optional
from logging import getLogger
from collections.abc import Mapping
from datetime import date, datetime
from decimal import Decimal
from typing import Any
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import MikrotikEntity, async_add_entities
from .coordinator import MikrotikCoordinator
from .helper import format_attribute
from .model import model_async_setup_entry, MikrotikEntity
from .sensor_types import (
SENSOR_TYPES,
SENSOR_SERVICES,
@ -14,52 +24,56 @@ from .sensor_types import (
DEVICE_ATTRIBUTES_IFACE_WIRELESS,
)
_LOGGER = logging.getLogger(__name__)
_LOGGER = getLogger(__name__)
# ---------------------------
# async_setup_entry
# ---------------------------
async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
_async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry for component"""
dispatcher = {
"MikrotikSensor": MikrotikSensor,
"MikrotikInterfaceTrafficSensor": MikrotikInterfaceTrafficSensor,
"MikrotikClientTrafficSensor": MikrotikClientTrafficSensor,
}
await model_async_setup_entry(
hass,
config_entry,
async_add_entities,
SENSOR_SERVICES,
SENSOR_TYPES,
dispatcher,
)
await async_add_entities(hass, config_entry, dispatcher)
# ---------------------------
# MikrotikSensor
# ---------------------------
class MikrotikSensor(MikrotikEntity, SensorEntity):
"""Define an Mikrotik Controller sensor."""
"""Define an Mikrotik sensor."""
def __init__(
self,
coordinator: MikrotikCoordinator,
entity_description,
uid: str | None = None,
):
super().__init__(coordinator, entity_description, uid)
self._attr_suggested_unit_of_measurement = (
self.entity_description.suggested_unit_of_measurement
)
@property
def state(self) -> Optional[str]:
"""Return the state."""
if self.entity_description.data_attribute:
def native_value(self) -> StateType | date | datetime | Decimal:
"""Return the value reported by the sensor."""
return self._data[self.entity_description.data_attribute]
else:
return "unknown"
@property
def native_unit_of_measurement(self):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
if self.entity_description.native_unit_of_measurement:
if self.entity_description.native_unit_of_measurement.startswith("data__"):
uom = self.entity_description.native_unit_of_measurement[6:]
if uom in self._data:
uom = self._data[uom]
return uom
return self._data[uom]
return self.entity_description.native_unit_of_measurement
@ -113,9 +127,9 @@ class MikrotikClientTrafficSensor(MikrotikSensor):
"""
if self.entity_description.data_attribute in ["lan-tx", "lan-rx"]:
return (
self._ctrl.connected()
self.coordinator.connected()
and self._data["available"]
and self._data["local_accounting"]
)
else:
return self._ctrl.connected() and self._data["available"]
return self.coordinator.connected() and self._data["available"]

View file

@ -1,4 +1,6 @@
"""Definitions for Mikrotik Router sensor entities."""
"""Definitions for sensor entities."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@ -9,11 +11,13 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
)
from homeassistant.const import (
TEMP_CELSIUS,
ELECTRIC_POTENTIAL_VOLT,
POWER_WATT,
PERCENTAGE,
DATA_BYTES,
REVOLUTIONS_PER_MINUTE,
UnitOfTemperature,
UnitOfDataRate,
UnitOfInformation,
UnitOfElectricPotential,
UnitOfPower,
)
from .const import DOMAIN
@ -113,28 +117,30 @@ DEVICE_ATTRIBUTES_GPS = [
class MikrotikSensorEntityDescription(SensorEntityDescription):
"""Class describing mikrotik entities."""
ha_group: str = ""
ha_connection: str = ""
ha_connection_value: str = ""
data_path: str = ""
data_attribute: str = ""
data_name: str = ""
ha_group: str | None = None
ha_connection: str | None = None
ha_connection_value: str | None = None
data_path: str | None = None
data_attribute: str | None = None
data_name: str | None = None
data_name_comment: bool = False
data_uid: str = ""
data_reference: str = ""
data_uid: str | None = None
data_reference: str | None = None
data_attributes_list: List = field(default_factory=lambda: [])
func: str = "MikrotikSensor"
SENSOR_TYPES = {
"system_temperature": MikrotikSensorEntityDescription(
SENSOR_TYPES: tuple[MikrotikSensorEntityDescription, ...] = (
MikrotikSensorEntityDescription(
key="system_temperature",
name="Temperature",
icon="mdi:thermometer",
native_unit_of_measurement=TEMP_CELSIUS,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=0,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="temperature",
@ -142,11 +148,13 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_voltage": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_voltage",
name="Voltage",
icon="mdi:lightning-bolt",
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=1,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@ -157,14 +165,16 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_cpu-temperature": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_cpu-temperature",
name="CPU temperature",
icon="mdi:thermometer",
native_unit_of_measurement=TEMP_CELSIUS,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=0,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="cpu-temperature",
@ -172,14 +182,16 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_switch-temperature": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_switch-temperature",
name="Switch temperature",
icon="mdi:thermometer",
native_unit_of_measurement=TEMP_CELSIUS,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=0,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="switch-temperature",
@ -187,14 +199,16 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_board-temperature1": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_board-temperature1",
name="Board temperature",
icon="mdi:thermometer",
native_unit_of_measurement=TEMP_CELSIUS,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_unit_of_measurement=UnitOfTemperature.CELSIUS,
suggested_display_precision=0,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="board-temperature1",
@ -202,14 +216,16 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_power-consumption": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_power-consumption",
name="Power consumption",
icon="mdi:transmission-tower",
native_unit_of_measurement=POWER_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=0,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="power-consumption",
@ -217,14 +233,14 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_fan1-speed": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_fan1-speed",
name="Fan1 speed",
icon="mdi:fan",
native_unit_of_measurement="RPM",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="fan1-speed",
@ -232,14 +248,14 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_fan2-speed": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_fan2-speed",
name="Fan2 speed",
icon="mdi:fan",
native_unit_of_measurement="RPM",
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
entity_category=EntityCategory.DIAGNOSTIC,
ha_group="System",
data_path="health",
data_attribute="fan2-speed",
@ -247,7 +263,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_uptime": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_uptime",
name="Uptime",
icon=None,
@ -262,7 +278,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_cpu-load": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_cpu-load",
name="CPU load",
icon="mdi:speedometer",
@ -277,7 +293,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_memory-usage": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_memory-usage",
name="Memory usage",
icon="mdi:memory",
@ -292,7 +308,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_hdd-usage": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_hdd-usage",
name="HDD usage",
icon="mdi:harddisk",
@ -307,7 +323,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_clients-wired": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_clients-wired",
name="Wired clients",
icon="mdi:lan",
@ -322,7 +338,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_clients-wireless": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_clients-wireless",
name="Wireless clients",
icon="mdi:wifi",
@ -337,7 +353,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_captive-authorized": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_captive-authorized",
name="Captive portal clients",
icon="mdi:key-wireless",
@ -352,7 +368,7 @@ SENSOR_TYPES = {
data_uid="",
data_reference="",
),
"system_gps-latitude": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_gps-latitude",
name="Latitude",
icon="mdi:latitude",
@ -368,7 +384,7 @@ SENSOR_TYPES = {
data_reference="",
data_attributes_list=DEVICE_ATTRIBUTES_GPS,
),
"system_gps-longitude": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="system_gps-longitude",
name="Longitude",
icon="mdi:longitude",
@ -384,12 +400,14 @@ SENSOR_TYPES = {
data_reference="",
data_attributes_list=DEVICE_ATTRIBUTES_GPS,
),
"traffic_tx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="traffic_tx",
name="TX",
icon="mdi:upload-network-outline",
native_unit_of_measurement="data__tx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="data__default-name",
@ -403,12 +421,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_IFACE,
func="MikrotikInterfaceTrafficSensor",
),
"traffic_rx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="traffic_rx",
name="RX",
icon="mdi:download-network-outline",
native_unit_of_measurement="data__rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="data__default-name",
@ -422,12 +442,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_IFACE,
func="MikrotikInterfaceTrafficSensor",
),
"total_tx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="tx-total",
name="TX total",
icon="mdi:upload-network",
native_unit_of_measurement=DATA_BYTES,
device_class=None,
native_unit_of_measurement=UnitOfInformation.BITS,
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_SIZE,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=None,
ha_group="data__default-name",
@ -441,12 +463,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_IFACE,
func="MikrotikInterfaceTrafficSensor",
),
"total_rx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="rx-total",
name="RX total",
icon="mdi:download-network",
native_unit_of_measurement=DATA_BYTES,
device_class=None,
native_unit_of_measurement=UnitOfInformation.BITS,
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_SIZE,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=None,
ha_group="data__default-name",
@ -460,12 +484,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_IFACE,
func="MikrotikInterfaceTrafficSensor",
),
"client_traffic_lan_tx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_lan_tx",
name="LAN TX",
icon="mdi:upload-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -479,12 +505,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"client_traffic_lan_rx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_lan_rx",
name="LAN RX",
icon="mdi:download-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -498,12 +526,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"client_traffic_wan_tx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_wan_tx",
name="WAN TX",
icon="mdi:upload-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -517,12 +547,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"client_traffic_wan_rx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_wan_rx",
name="WAN RX",
icon="mdi:download-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -536,12 +568,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"client_traffic_tx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_tx",
name="TX",
icon="mdi:upload-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -555,12 +589,14 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"client_traffic_rx": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="client_traffic_rx",
name="RX",
icon="mdi:download-network",
native_unit_of_measurement="data__tx-rx-attr",
device_class=None,
native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND,
suggested_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
suggested_display_precision=1,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=None,
ha_group="",
@ -574,7 +610,7 @@ SENSOR_TYPES = {
data_attributes_list=DEVICE_ATTRIBUTES_CLIENT_TRAFFIC,
func="MikrotikClientTrafficSensor",
),
"environment": MikrotikSensorEntityDescription(
MikrotikSensorEntityDescription(
key="environment",
name="",
icon="mdi:clipboard-list",
@ -591,6 +627,6 @@ SENSOR_TYPES = {
data_uid="name",
data_reference="name",
),
}
)
SENSOR_SERVICES = {}
SENSOR_SERVICES = []

View file

@ -6,7 +6,7 @@ from collections.abc import Mapping
from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.restore_state import RestoreEntity
from .helper import format_attribute
from .model import model_async_setup_entry, MikrotikEntity
from .entity import model_async_setup_entry, MikrotikEntity
from .switch_types import (
SENSOR_TYPES,
SENSOR_SERVICES,

View file

@ -8,7 +8,7 @@ from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntityFeature,
)
from .model import model_async_setup_entry, MikrotikEntity
from .entity import model_async_setup_entry, MikrotikEntity
from .update_types import (
SENSOR_TYPES,
SENSOR_SERVICES,