diff --git a/custom_components/mikrotik_router/config_flow.py b/custom_components/mikrotik_router/config_flow.py index 4e89583..1a0b0e2 100644 --- a/custom_components/mikrotik_router/config_flow.py +++ b/custom_components/mikrotik_router/config_flow.py @@ -39,6 +39,8 @@ DEFAULT_SENSOR_NAT, CONF_SENSOR_MANGLE, DEFAULT_SENSOR_MANGLE, + CONF_SENSOR_FILTER, + DEFAULT_SENSOR_FILTER, CONF_SENSOR_KIDCONTROL, DEFAULT_SENSOR_KIDCONTROL, CONF_SENSOR_PPP, @@ -261,6 +263,12 @@ async def async_step_sensor_select(self, user_input=None): CONF_SENSOR_MANGLE, DEFAULT_SENSOR_MANGLE ), ): bool, + vol.Optional( + CONF_SENSOR_FILTER, + default=self.config_entry.options.get( + CONF_SENSOR_FILTER, DEFAULT_SENSOR_FILTER + ), + ): bool, vol.Optional( CONF_SENSOR_KIDCONTROL, default=self.config_entry.options.get( diff --git a/custom_components/mikrotik_router/mikrotik_controller.py b/custom_components/mikrotik_router/mikrotik_controller.py index 1c88f6a..95c8c1e 100644 --- a/custom_components/mikrotik_router/mikrotik_controller.py +++ b/custom_components/mikrotik_router/mikrotik_controller.py @@ -134,6 +134,7 @@ def __init__(self, hass, config_entry): self.nat_removed = {} self.mangle_removed = {} + self.filter_removed = {} self.host_hass_recovered = False self.host_tracking_initialized = False @@ -225,6 +226,14 @@ def option_sensor_mangle(self): """Config entry option to not track ARP.""" return self.config_entry.options.get(CONF_SENSOR_MANGLE, DEFAULT_SENSOR_MANGLE) + # --------------------------- + # option_sensor_filter + # --------------------------- + @property + def option_sensor_filter(self): + """Config entry option to not track ARP.""" + return self.config_entry.options.get(CONF_SENSOR_FILTER, DEFAULT_SENSOR_FILTER) + # --------------------------- # option_sensor_kidcontrol # --------------------------- @@ -565,6 +574,9 @@ async def async_update(self): if self.api.connected() and self.option_sensor_mangle: await self.hass.async_add_executor_job(self.get_mangle) + if self.api.connected() and self.option_sensor_filter: + await self.hass.async_add_executor_job(self.get_filter) + if self.api.connected() and self.support_ppp and self.option_sensor_ppp: await self.hass.async_add_executor_job(self.get_ppp) @@ -955,6 +967,101 @@ def get_mangle(self): del self.data["mangle"][uid] + # --------------------------- + # get_filter + # --------------------------- + def get_filter(self): + """Get Filter data from Mikrotik""" + self.data["filter"] = parse_api( + data=self.data["filter"], + source=self.api.path("/ip/firewall/filter"), + key=".id", + vals=[ + {"name": ".id"}, + {"name": "chain"}, + {"name": "action"}, + {"name": "comment"}, + {"name": "address-list"}, + {"name": "protocol", "default": "any"}, + {"name": "in-interface", "default": "any"}, + {"name": "out-interface", "default": "any"}, + {"name": "src-address", "default": "any"}, + {"name": "src-port", "default": "any"}, + {"name": "dst-address", "default": "any"}, + {"name": "dst-port", "default": "any"}, + {"name": "layer7-protocol", "default": "any"}, + {"name": "connection-state", "default": "any"}, + {"name": "tcp-flags", "default": "any"}, + { + "name": "enabled", + "source": "disabled", + "type": "bool", + "reverse": True, + "default": True, + }, + ], + val_proc=[ + [ + {"name": "uniq-id"}, + {"action": "combine"}, + {"key": "chain"}, + {"text": ","}, + {"key": "action"}, + {"text": ","}, + {"key": "protocol"}, + {"text": ","}, + {"key": "layer7-protocol"}, + {"text": ","}, + {"key": "in-interface"}, + {"text": ":"}, + {"key": "src-address"}, + {"text": ":"}, + {"key": "src-port"}, + {"text": "-"}, + {"key": "out-interface"}, + {"text": ":"}, + {"key": "dst-address"}, + {"text": ":"}, + {"key": "dst-port"}, + ], + [ + {"name": "name"}, + {"action": "combine"}, + {"key": "action"}, + {"text": ","}, + {"key": "protocol"}, + {"text": ":"}, + {"key": "dst-port"}, + ], + ], + skip=[ + {"name": "dynamic", "value": True}, + {"name": "action", "value": "jump"}, + ], + ) + + # Remove duplicate filter entries to prevent crash + filter_uniq = {} + filter_del = {} + for uid in self.data["filter"]: + tmp_name = self.data["filter"][uid]["uniq-id"] + if tmp_name not in filter_uniq: + filter_uniq[tmp_name] = uid + else: + filter_del[uid] = 1 + filter_del[filter_uniq[tmp_name]] = 1 + + for uid in filter_del: + if self.data["filter"][uid]["uniq-id"] not in self.filter_removed: + self.filter_removed[self.data["filter"][uid]["uniq-id"]] = 1 + _LOGGER.error( + "Mikrotik %s duplicate Filter rule %s, entity will be unavailable.", + self.host, + self.data["filter"][uid]["name"], + ) + + del self.data["filter"][uid] + # --------------------------- # get_kidcontrol # --------------------------- diff --git a/custom_components/mikrotik_router/switch.py b/custom_components/mikrotik_router/switch.py index bae2fdb..396b5fc 100644 --- a/custom_components/mikrotik_router/switch.py +++ b/custom_components/mikrotik_router/switch.py @@ -52,6 +52,23 @@ "comment", ] +DEVICE_ATTRIBUTES_FILTER = [ + "chain", + "action", + "address-list", + "protocol", + "layer7-protocol", + "tcp-flags", + "connection-state", + "in-interface", + "src-address", + "src-port", + "out-interface", + "dst-address", + "dst-port", + "comment", +] + DEVICE_ATTRIBUTES_PPP_SECRET = [ "connected", "service", @@ -143,18 +160,37 @@ def update_items(inst, mikrotik_controller, async_add_entities, switches): # Add switches for sid, sid_uid, sid_name, sid_ref, sid_attr, sid_func in zip( # Data point name - ["interface", "nat", "mangle", "ppp_secret", "script", "queue", "kid-control"], + [ + "interface", + "nat", + "mangle", + "filter", + "ppp_secret", + "script", + "queue", + "kid-control", + ], # Data point unique id - ["name", "uniq-id", "uniq-id", "name", "name", "name", "name"], + ["name", "uniq-id", "uniq-id", "uniq-id", "name", "name", "name", "name"], # Entry Name - ["name", "name", "name", "name", "name", "name", "name"], + ["name", "name", "name", "name", "name", "name", "name", "name"], # Entry Unique id - ["port-mac-address", "uniq-id", "uniq-id", "name", "name", "name", "name"], + [ + "port-mac-address", + "uniq-id", + "uniq-id", + "uniq-id", + "name", + "name", + "name", + "name", + ], # Attr [ DEVICE_ATTRIBUTES_IFACE, DEVICE_ATTRIBUTES_NAT, DEVICE_ATTRIBUTES_MANGLE, + DEVICE_ATTRIBUTES_FILTER, DEVICE_ATTRIBUTES_PPP_SECRET, DEVICE_ATTRIBUTES_SCRIPT, DEVICE_ATTRIBUTES_QUEUE, @@ -165,6 +201,7 @@ def update_items(inst, mikrotik_controller, async_add_entities, switches): MikrotikControllerPortSwitch, MikrotikControllerNATSwitch, MikrotikControllerMangleSwitch, + MikrotikControllerFilterSwitch, MikrotikControllerPPPSecretSwitch, MikrotikControllerScriptSwitch, MikrotikControllerQueueSwitch, @@ -526,6 +563,95 @@ async def async_turn_off(self) -> None: await self._ctrl.async_update() +# --------------------------- +# MikrotikControllerFilterSwitch +# --------------------------- +class MikrotikControllerFilterSwitch(MikrotikControllerSwitch): + """Representation of a Filter switch.""" + + def __init__(self, inst, uid, mikrotik_controller, sid_data): + """Initialize.""" + super().__init__(inst, uid, mikrotik_controller, sid_data) + + @property + def name(self) -> str: + """Return the name.""" + if self._data["comment"]: + return f"{self._inst} Filter {self._data['comment']}" + + return f"{self._inst} Filter {self._data['name']}" + + @property + def unique_id(self) -> str: + """Return a unique id for this entity.""" + return f"{self._inst.lower()}-enable_filter-{self._data['uniq-id']}" + + @property + def icon(self) -> str: + """Return the icon.""" + if not self._data["enabled"]: + icon = "mdi:filter-variant-remove" + else: + icon = "mdi:filter-variant" + + return icon + + @property + def device_info(self) -> Dict[str, Any]: + """Return a description for device registry.""" + info = { + "identifiers": { + ( + DOMAIN, + "serial-number", + self._ctrl.data["routerboard"]["serial-number"], + "switch", + "Filter", + ) + }, + "manufacturer": self._ctrl.data["resource"]["platform"], + "model": self._ctrl.data["resource"]["board-name"], + "name": f"{self._inst} Filter", + } + return info + + async def async_turn_on(self) -> None: + """Turn on the switch.""" + path = "/ip/firewall/filter" + param = ".id" + value = None + for uid in self._ctrl.data["filter"]: + if self._ctrl.data["filter"][uid]["uniq-id"] == ( + f"{self._data['chain']},{self._data['action']},{self._data['protocol']},{self._data['layer7-protocol']}," + f"{self._data['in-interface']}:{self._data['src-address']}:{self._data['src-port']}-" + f"{self._data['out-interface']}:{self._data['dst-address']}:{self._data['dst-port']}" + ): + value = self._ctrl.data["filter"][uid][".id"] + + mod_param = "disabled" + mod_value = False + self._ctrl.set_value(path, param, value, mod_param, mod_value) + await self._ctrl.force_update() + + async def async_turn_off(self) -> None: + """Turn off the switch.""" + path = "/ip/firewall/filter" + param = ".id" + value = None + for uid in self._ctrl.data["filter"]: + if self._ctrl.data["filter"][uid]["uniq-id"] == ( + f"{self._data['chain']},{self._data['action']},{self._data['protocol']},{self._data['layer7-protocol']}," + f"{self._data['in-interface']}:{self._data['src-address']}:{self._data['src-port']}-" + f"{self._data['out-interface']}:{self._data['dst-address']}:{self._data['dst-port']}" + ): + value = self._ctrl.data["filter"][uid][".id"] + + mod_param = "disabled" + mod_value = True + self._ctrl.set_value(path, param, value, mod_param, mod_value) + await self._ctrl.async_update() + + # --------------------------- # MikrotikControllerPPPSecretSwitch # ---------------------------