Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unbound Blocklist Switch #210

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom_components/opnsense/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@

ICON_MEMORY = "mdi:memory"

ATTR_UNBOUND_BLOCKLIST = "unbound_blocklist"

SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = {
# pfstate
"telemetry.pfstate.used": SensorEntityDescription(
Expand Down
4 changes: 4 additions & 0 deletions custom_components/opnsense/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import ATTR_UNBOUND_BLOCKLIST
from .helpers import dict_get
from .pyopnsense import OPNsenseClient

Expand Down Expand Up @@ -144,6 +145,9 @@ async def _async_update_data(self):
self._state["dhcp_leases"] = []
self._state["dhcp_stats"] = {}
self._state["notices"] = await self._get_notices()
self._state[ATTR_UNBOUND_BLOCKLIST] = (
await self._client.get_unbound_blocklist()
)

lease_stats: Mapping[str, int] = {"total": 0, "online": 0, "offline": 0}
for lease in self._state["dhcp_leases"]:
Expand Down
58 changes: 58 additions & 0 deletions custom_components/opnsense/pyopnsense/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,3 +1393,61 @@ async def close_notice(self, id) -> bool:
success = False
_LOGGER.debug(f"[close_notice] success: {success}")
return success

@_log_errors
async def get_unbound_blocklist(self) -> Mapping[str, Any]:
response: Mapping[str, Any] | list = await self._get(
"/api/unbound/settings/get"
)
if response is None or not isinstance(response, Mapping):
_LOGGER.error("Invalid data returned from get_unbound_blocklist")
return {}
# _LOGGER.debug(f"[get_unbound_blocklist] response: {response}")
dnsbl_settings = response.get("unbound", {}).get("dnsbl", {})
# _LOGGER.debug(f"[get_unbound_blocklist] dnsbl_settings: {dnsbl_settings}")
if not isinstance(dnsbl_settings, Mapping):
return {}
dnsbl = {}
for attr in ["enabled", "safesearch", "nxdomain", "address"]:
dnsbl[attr] = dnsbl_settings.get(attr, "")
for attr in ["type", "lists", "whitelists", "blocklists", "wildcards"]:
if isinstance(dnsbl_settings[attr], Mapping):
dnsbl[attr] = ",".join(
[
key
for key, value in dnsbl_settings.get(attr, {}).items()
if isinstance(value, Mapping) and value.get("selected", 0) == 1
]
)
else:
dnsbl[attr] = ""
_LOGGER.debug(f"[get_unbound_blocklist] dnsbl: {dnsbl}")
return dnsbl

async def _set_unbound_blocklist(self, set_state: bool) -> bool:
payload = {}
payload["unbound"] = {}
payload["unbound"]["dnsbl"] = await self.get_unbound_blocklist()
if set_state:
payload["unbound"]["dnsbl"]["enabled"] = "1"
else:
payload["unbound"]["dnsbl"]["enabled"] = "0"
response: Mapping[str, Any] | list = await self._post(
"/api/unbound/settings/set", payload=payload
)
_LOGGER.debug(
f"[set_unbound_blocklist] set_state: {'On' if set_state else 'Off'}, payload: {payload}, response: {response}"
)
return not (
response is None
or not isinstance(response, Mapping)
or response.get("result", "failed") != "saved"
)

@_log_errors
async def enable_unbound_blocklist(self) -> bool:
return await self._set_unbound_blocklist(set_state=True)

@_log_errors
async def disable_unbound_blocklist(self) -> bool:
return await self._set_unbound_blocklist(set_state=False)
84 changes: 77 additions & 7 deletions custom_components/opnsense/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from custom_components.opnsense.pyopnsense import OPNsenseClient

from . import CoordinatorEntityManager, OPNsenseEntity
from .const import COORDINATOR, DOMAIN
from .const import ATTR_UNBOUND_BLOCKLIST, COORDINATOR, DOMAIN
from .coordinator import OPNsenseDataUpdateCoordinator
from .helpers import dict_get

Expand Down Expand Up @@ -192,6 +192,21 @@ def process_entities_callback(hass, config_entry):
),
)
entities.append(entity)

entity = OPNsenseUnboundBlocklistSwitch(
config_entry,
coordinator,
SwitchEntityDescription(
key=f"unbound_blocklist.switch",
name=f"Unbound Blocklist Switch",
# icon=icon,
# entity_category=ENTITY_CATEGORY_CONFIG,
device_class=SwitchDeviceClass.SWITCH,
entity_registry_enabled_default=False,
),
)
entities.append(entity)

return entities

cem = CoordinatorEntityManager(
Expand Down Expand Up @@ -220,13 +235,13 @@ def __init__(
f"{self.opnsense_device_unique_id}_{entity_description.key}"
)

@property
def is_on(self):
return False
# @property
# def is_on(self):
# return False

@property
def extra_state_attributes(self):
return None
# @property
# def extra_state_attributes(self):
# return None


class OPNsenseFilterSwitch(OPNsenseSwitch):
Expand Down Expand Up @@ -421,3 +436,58 @@ def extra_state_attributes(self) -> Mapping[str, Any]:
for attr in ["id", "name"]:
attributes[f"service_{attr}"] = service.get(attr, None)
return attributes


class OPNsenseUnboundBlocklistSwitch(OPNsenseSwitch):

def __init__(
self,
config_entry,
coordinator: OPNsenseDataUpdateCoordinator,
entity_description: SwitchEntityDescription,
) -> None:
super().__init__(
config_entry=config_entry,
coordinator=coordinator,
entity_description=entity_description,
)
self._attr_is_on = STATE_UNKNOWN
self._attr_extra_state_attributes = {}
self._attr_available = False

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
dnsbl = self.coordinator.data.get(ATTR_UNBOUND_BLOCKLIST, {})
if not isinstance(dnsbl, Mapping) or len(dnsbl) == 0:
self._attr_available = False
return
self._attr_available = True
self._attr_is_on = True if dnsbl.get("enabled", "0") == "1" else False
self._attr_extra_state_attributes = {
"Force SafeSearch": (
True if dnsbl.get("safesearch", "0") == "1" else False
),
"Type of DNSBL": dnsbl.get("type", ""),
"URLs of Blocklists": dnsbl.get("lists", ""),
"Whitelist Domains": dnsbl.get("whitelists", ""),
"Blocklist Domains": dnsbl.get("blocklists", ""),
"Wildcard Domains": dnsbl.get("wildcards", ""),
"Destination Address": dnsbl.get("address", ""),
"Return NXDOMAIN": (True if dnsbl.get("nxdomain", "0") == "1" else False),
}
self.async_write_ha_state()

async def async_turn_on(self, **kwargs) -> None:
"""Turn the entity on."""
client: OPNsenseClient = self._get_opnsense_client()
result: bool = await client.enable_unbound_blocklist()
if result:
await self.coordinator.async_refresh()

async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
client: OPNsenseClient = self._get_opnsense_client()
result: bool = await client.disable_unbound_blocklist()
if result:
await self.coordinator.async_refresh()
Loading