diff --git a/custom_components/mikrotik_router/__init__.py b/custom_components/mikrotik_router/__init__.py index 47f6325..7674a8e 100644 --- a/custom_components/mikrotik_router/__init__.py +++ b/custom_components/mikrotik_router/__init__.py @@ -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) diff --git a/custom_components/mikrotik_router/apiparser.py b/custom_components/mikrotik_router/apiparser.py index a352da6..da6cda3 100644 --- a/custom_components/mikrotik_router/apiparser.py +++ b/custom_components/mikrotik_router/apiparser.py @@ -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 diff --git a/custom_components/mikrotik_router/binary_sensor.py b/custom_components/mikrotik_router/binary_sensor.py index ad07776..67e5fcb 100644 --- a/custom_components/mikrotik_router/binary_sensor.py +++ b/custom_components/mikrotik_router/binary_sensor.py @@ -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, diff --git a/custom_components/mikrotik_router/button.py b/custom_components/mikrotik_router/button.py index 536adf8..9852651 100644 --- a/custom_components/mikrotik_router/button.py +++ b/custom_components/mikrotik_router/button.py @@ -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, diff --git a/custom_components/mikrotik_router/config_flow.py b/custom_components/mikrotik_router/config_flow.py index 6fb0e98..935a39b 100644 --- a/custom_components/mikrotik_router/config_flow.py +++ b/custom_components/mikrotik_router/config_flow.py @@ -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( diff --git a/custom_components/mikrotik_router/const.py b/custom_components/mikrotik_router/const.py index 7ae0301..0491d85 100644 --- a/custom_components/mikrotik_router/const.py +++ b/custom_components/mikrotik_router/const.py @@ -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" diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/coordinator.py similarity index 93% rename from custom_components/mikrotik_router/mikrotik_controller.py rename to custom_components/mikrotik_router/coordinator.py index 41bc38b..58fcccf 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/coordinator.py @@ -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 diff --git a/custom_components/mikrotik_router/device_tracker.py b/custom_components/mikrotik_router/device_tracker.py index 59798ce..5417de5 100644 --- a/custom_components/mikrotik_router/device_tracker.py +++ b/custom_components/mikrotik_router/device_tracker.py @@ -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__) diff --git a/custom_components/mikrotik_router/model.py b/custom_components/mikrotik_router/entity.py similarity index 51% rename from custom_components/mikrotik_router/model.py rename to custom_components/mikrotik_router/entity.py index 04601dd..cf0d520 100644 --- a/custom_components/mikrotik_router/model.py +++ b/custom_components/mikrotik_router/entity.py @@ -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( - CONF_SENSOR_PORT_TRACKER, DEFAULT_SENSOR_PORT_TRACKER + 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() + 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: + unique_id = f"{obj._inst.lower()}-{obj.entity_description.key}" - -# --------------------------- -# 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" + 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) ): - continue + _LOGGER.debug("Add entity %s", entity_id) + await platform.async_add_entities([obj]) - item_id = f"{inst}-{sensor}" - if tmp := _register_entity(sensors, item_id, "", uid_sensor): - sensors[item_id] = tmp - new_sensors.append(sensors[item_id]) - 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): + 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() diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index 2732218..5c24290 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -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 diff --git a/custom_components/mikrotik_router/sensor.py b/custom_components/mikrotik_router/sensor.py index 74d90c8..f3851a7 100644 --- a/custom_components/mikrotik_router/sensor.py +++ b/custom_components/mikrotik_router/sensor.py @@ -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: - return self._data[self.entity_description.data_attribute] - else: - return "unknown" + def native_value(self) -> StateType | date | datetime | Decimal: + """Return the value reported by the sensor.""" + return self._data[self.entity_description.data_attribute] @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"] diff --git a/custom_components/mikrotik_router/sensor_types.py b/custom_components/mikrotik_router/sensor_types.py index 59de5e1..87bf227 100644 --- a/custom_components/mikrotik_router/sensor_types.py +++ b/custom_components/mikrotik_router/sensor_types.py @@ -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 = [] diff --git a/custom_components/mikrotik_router/switch.py b/custom_components/mikrotik_router/switch.py index 13e9c17..0be9541 100644 --- a/custom_components/mikrotik_router/switch.py +++ b/custom_components/mikrotik_router/switch.py @@ -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, diff --git a/custom_components/mikrotik_router/update.py b/custom_components/mikrotik_router/update.py index 4c51e64..052bafe 100644 --- a/custom_components/mikrotik_router/update.py +++ b/custom_components/mikrotik_router/update.py @@ -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,