diff --git a/custom_components/mikrotik_router/device_tracker.py b/custom_components/mikrotik_router/device_tracker.py index 0688ce0..d06db72 100644 --- a/custom_components/mikrotik_router/device_tracker.py +++ b/custom_components/mikrotik_router/device_tracker.py @@ -20,7 +20,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DEVICE_ATTRIBUTES = [ +DEVICE_ATTRIBUTES_IFACE = [ "running", "enabled", "comment", @@ -36,6 +36,15 @@ DEVICE_ATTRIBUTES = [ "default-name", ] +DEVICE_ATTRIBUTES_HOST = [ + "mac-address", + "address", + "host-name", + "status", + "last-seen", + "interface", +] + # --------------------------- # format_attribute @@ -80,18 +89,24 @@ def update_items(inst, mikrotik_controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - for uid in mikrotik_controller.data["interface"]: - if mikrotik_controller.data["interface"][uid]["type"] == "ether": - item_id = f"{inst}-{mikrotik_controller.data['interface'][uid]['default-name']}" + # Add switches + for sid, sid_uid, sid_func in zip( + ["interface", "dhcp"], + ["default-name", "mac-address"], + [ + MikrotikControllerPortDeviceTracker, + MikrotikControllerHostDeviceTracker, + ], + ): + for uid in mikrotik_controller.data[sid]: + item_id = f"{inst}-{sid}-{mikrotik_controller.data[sid][uid][sid_uid]}" _LOGGER.debug("Updating device_tracker %s", item_id) if item_id in tracked: if tracked[item_id].enabled: tracked[item_id].async_schedule_update_ha_state() continue - tracked[item_id] = MikrotikControllerPortDeviceTracker( - inst, uid, mikrotik_controller - ) + tracked[item_id] = sid_func(inst, uid, mikrotik_controller) new_tracked.append(tracked[item_id]) if new_tracked: @@ -186,7 +201,106 @@ class MikrotikControllerPortDeviceTracker(ScannerEntity): """Return the port state attributes.""" attributes = self._attrs - for variable in DEVICE_ATTRIBUTES: + for variable in DEVICE_ATTRIBUTES_IFACE: + if variable in self._data: + attributes[format_attribute(variable)] = self._data[variable] + + return attributes + + +# --------------------------- +# MikrotikControllerHostDeviceTracker +# --------------------------- +class MikrotikControllerHostDeviceTracker(ScannerEntity): + """Representation of a network device.""" + + def __init__(self, inst, uid, mikrotik_controller): + """Set up tracked port.""" + self._inst = inst + self._ctrl = mikrotik_controller + self._data = mikrotik_controller.data["dhcp"][uid] + + self._attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True + + async def async_added_to_hass(self): + """Host entity created.""" + _LOGGER.debug( + "New host tracker %s (%s - %s)", + self._inst, + self._data["host-name"], + self._data["mac-address"], + ) + + async def async_update(self): + """Synchronize state with controller.""" + + @property + def is_connected(self): + """Return true if the host is connected to the network.""" + return self._data["available"] + + @property + def source_type(self): + """Return the source type of the host.""" + return SOURCE_TYPE_ROUTER + + @property + def name(self): + """Return the name of the host.""" + return f"{self._inst} {self._data['host-name']}" + + @property + def unique_id(self): + """Return a unique identifier for this host.""" + return f"{self._inst.lower()}-{self._data['mac-address']}" + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self._ctrl.connected() + + @property + def icon(self): + """Return the icon.""" + if self._data["available"]: + icon = "mdi:lan-connect" + else: + icon = "mdi:lan-disconnect" + + return icon + + @property + def device_info(self): + """Return a host description for device registry.""" + info = { + "identifiers": { + ( + DOMAIN, + "serial-number", + self._ctrl.data["routerboard"]["serial-number"], + "switch", + "Hosts", + ) + }, + "manufacturer": self._ctrl.data["resource"]["platform"], + "model": self._ctrl.data["resource"]["board-name"], + "name": "Hosts", + } + return info + + @property + def device_state_attributes(self): + """Return the host state attributes.""" + attributes = self._attrs + + for variable in DEVICE_ATTRIBUTES_HOST: if variable in self._data: attributes[format_attribute(variable)] = self._data[variable] diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 8cb58e1..0ea7db5 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -58,6 +58,8 @@ class MikrotikControllerData: "fw-update": {}, "script": {}, "queue": {}, + "dhcp-server": {}, + "dhcp": {}, } self.listeners = [] @@ -193,6 +195,7 @@ class MikrotikControllerData: await self.hass.async_add_executor_job(self.get_system_resource) await self.hass.async_add_executor_job(self.get_script) await self.hass.async_add_executor_job(self.get_queue) + await self.hass.async_add_executor_job(self.get_dhcp) async_dispatcher_send(self.hass, self.signal_update) self.lock.release() @@ -655,3 +658,42 @@ class MikrotikControllerData: upload_burst_time, download_burst_time = self.data["queue"][uid]["burst-time"].split('/') self.data["queue"][uid]["upload-burst-time"] = upload_burst_time self.data["queue"][uid]["download-burst-time"] = download_burst_time + + # --------------------------- + # get_dhcp + # --------------------------- + def get_dhcp(self): + """Get DHCP data from Mikrotik""" + + self.data["dhcp-server"] = parse_api( + data=self.data["dhcp-server"], + source=self.api.path("/ip/dhcp-server"), + key="name", + vals=[ + {"name": "name"}, + {"name": "interface", "default": ""}, + ] + ) + + self.data["dhcp"] = parse_api( + data=self.data["dhcp"], + source=self.api.path("/ip/dhcp-server/lease"), + key="mac-address", + vals=[ + {"name": "mac-address"}, + {"name": "address", "default": "unknown"}, + {"name": "host-name", "default": "unknown"}, + {"name": "status", "default": "unknown"}, + {"name": "last-seen", "default": "unknown"}, + {"name": "server", "default": "unknown"}, + {"name": "interface", "default": ""}, + {"name": "available", "type": "bool", "default": False}, + ] + ) + + for uid in self.data["dhcp"]: + self.data["dhcp"][uid]['interface'] = \ + self.data["dhcp-server"][self.data["dhcp"][uid]['server']]["interface"] + + self.data["dhcp"][uid]['available'] = \ + self.api.arp_ping(self.data["dhcp"][uid]['address'], self.data["dhcp"][uid]['interface']) diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index a131eba..e28938c 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -399,3 +399,67 @@ class MikrotikAPI: self.lock.release() return traffic if traffic else None + + # --------------------------- + # arp_ping + # --------------------------- + def arp_ping(self, address, interface) -> bool: + """Check arp ping response traffic stats""" + if not self.connection_check(): + return False + + response = self.path("/ping", return_list=False) + if response is None: + return False + + args = { + "arp-ping": "yes", + "interval": "100ms", + "count": 3, + "interface": interface, + "address": address, + } + self.lock.acquire() + try: + _LOGGER.debug("Ping host query: %s", "/ping") + ping = response("/ping", **args) + except librouteros_custom.exceptions.ConnectionClosed: + self.disconnect() + self.lock.release() + return False + except ( + librouteros_custom.exceptions.TrapError, + librouteros_custom.exceptions.MultiTrapError, + librouteros_custom.exceptions.ProtocolError, + librouteros_custom.exceptions.FatalError, + ssl.SSLError, + BrokenPipeError, + OSError, + ValueError, + ) as api_error: + self.disconnect("arp_ping", api_error) + self.lock.release() + return False + except: + self.disconnect("arp_ping") + self.lock.release() + return False + + try: + ping = list(ping) + except librouteros_custom.exceptions.ConnectionClosed as api_error: + self.disconnect("arp_ping", api_error) + self.lock.release() + return False + except: + self.disconnect("arp_ping") + self.lock.release() + return False + + self.lock.release() + + for tmp in ping: + if tmp["received"] > 0: + return True + + return False