forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds AdGuard Home integration (home-assistant#24219)
* Adds AdGuard Home integration * 👕 Addresses linting warnings * 🚑 Fixes typehint in async_setup_entry * 👕 Take advantage of Python's coalescing operators * 👕 Use adguard instance from outer scope directly in service calls * 👕 Use more sensible scan_interval default for sensors * 👕 Adds specific files to .coveragerc * ☔ Added tests and small changes to improve coverage * 🔨 Import adguardhome dependencies at the top * 🚑 Converted service handlers to be async * 🔥 Removed init step from config flow
- Loading branch information
Showing
17 changed files
with
1,095 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"config": { | ||
"title": "AdGuard Home", | ||
"step": { | ||
"user": { | ||
"title": "Link your AdGuard Home.", | ||
"description": "Set up your AdGuard Home instance to allow monitoring and control.", | ||
"data": { | ||
"host": "Host", | ||
"password": "Password", | ||
"port": "Port", | ||
"username": "Username", | ||
"ssl": "AdGuard Home uses a SSL certificate", | ||
"verify_ssl": "AdGuard Home uses a proper certificate" | ||
} | ||
}, | ||
"hassio_confirm": { | ||
"title": "AdGuard Home via Hass.io add-on", | ||
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?" | ||
} | ||
}, | ||
"error": { | ||
"connection_error": "Failed to connect." | ||
}, | ||
"abort": { | ||
"single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
"""Support for AdGuard Home.""" | ||
import logging | ||
from typing import Any, Dict | ||
|
||
from adguardhome import AdGuardHome, AdGuardHomeError | ||
import voluptuous as vol | ||
|
||
from homeassistant.components.adguard.const import ( | ||
CONF_FORCE, DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, | ||
SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, SERVICE_REFRESH, | ||
SERVICE_REMOVE_URL) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import ( | ||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_URL, | ||
CONF_USERNAME, CONF_VERIFY_SSL) | ||
from homeassistant.helpers import config_validation as cv | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.entity import Entity | ||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) | ||
SERVICE_ADD_URL_SCHEMA = vol.Schema( | ||
{vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url} | ||
) | ||
SERVICE_REFRESH_SCHEMA = vol.Schema( | ||
{vol.Optional(CONF_FORCE, default=False): cv.boolean} | ||
) | ||
|
||
|
||
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: | ||
"""Set up the AdGuard Home components.""" | ||
return True | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistantType, entry: ConfigEntry | ||
) -> bool: | ||
"""Set up AdGuard Home from a config entry.""" | ||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]) | ||
adguard = AdGuardHome( | ||
entry.data[CONF_HOST], | ||
port=entry.data[CONF_PORT], | ||
username=entry.data[CONF_USERNAME], | ||
password=entry.data[CONF_PASSWORD], | ||
tls=entry.data[CONF_SSL], | ||
verify_ssl=entry.data[CONF_VERIFY_SSL], | ||
loop=hass.loop, | ||
session=session, | ||
) | ||
|
||
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard | ||
|
||
for component in 'sensor', 'switch': | ||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(entry, component) | ||
) | ||
|
||
async def add_url(call) -> None: | ||
"""Service call to add a new filter subscription to AdGuard Home.""" | ||
await adguard.filtering.add_url( | ||
call.data.get(CONF_NAME), call.data.get(CONF_URL) | ||
) | ||
|
||
async def remove_url(call) -> None: | ||
"""Service call to remove a filter subscription from AdGuard Home.""" | ||
await adguard.filtering.remove_url(call.data.get(CONF_URL)) | ||
|
||
async def enable_url(call) -> None: | ||
"""Service call to enable a filter subscription in AdGuard Home.""" | ||
await adguard.filtering.enable_url(call.data.get(CONF_URL)) | ||
|
||
async def disable_url(call) -> None: | ||
"""Service call to disable a filter subscription in AdGuard Home.""" | ||
await adguard.filtering.disable_url(call.data.get(CONF_URL)) | ||
|
||
async def refresh(call) -> None: | ||
"""Service call to refresh the filter subscriptions in AdGuard Home.""" | ||
await adguard.filtering.refresh(call.data.get(CONF_FORCE)) | ||
|
||
hass.services.async_register( | ||
DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA | ||
) | ||
hass.services.async_register( | ||
DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA | ||
) | ||
hass.services.async_register( | ||
DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA | ||
) | ||
hass.services.async_register( | ||
DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA | ||
) | ||
hass.services.async_register( | ||
DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry( | ||
hass: HomeAssistantType, entry: ConfigType | ||
) -> bool: | ||
"""Unload AdGuard Home config entry.""" | ||
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) | ||
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) | ||
hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) | ||
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) | ||
hass.services.async_remove(DOMAIN, SERVICE_REFRESH) | ||
|
||
for component in 'sensor', 'switch': | ||
await hass.config_entries.async_forward_entry_unload(entry, component) | ||
|
||
del hass.data[DOMAIN] | ||
|
||
return True | ||
|
||
|
||
class AdGuardHomeEntity(Entity): | ||
"""Defines a base AdGuard Home entity.""" | ||
|
||
def __init__(self, adguard, name: str, icon: str) -> None: | ||
"""Initialize the AdGuard Home entity.""" | ||
self._name = name | ||
self._icon = icon | ||
self._available = True | ||
self.adguard = adguard | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Return the name of the entity.""" | ||
return self._name | ||
|
||
@property | ||
def icon(self) -> str: | ||
"""Return the mdi icon of the entity.""" | ||
return self._icon | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return True if entity is available.""" | ||
return self._available | ||
|
||
async def async_update(self) -> None: | ||
"""Update AdGuard Home entity.""" | ||
try: | ||
await self._adguard_update() | ||
self._available = True | ||
except AdGuardHomeError: | ||
if self._available: | ||
_LOGGER.debug( | ||
"An error occurred while updating AdGuard Home sensor.", | ||
exc_info=True, | ||
) | ||
self._available = False | ||
|
||
async def _adguard_update(self) -> None: | ||
"""Update AdGuard Home entity.""" | ||
raise NotImplementedError() | ||
|
||
|
||
class AdGuardHomeDeviceEntity(AdGuardHomeEntity): | ||
"""Defines a AdGuard Home device entity.""" | ||
|
||
@property | ||
def device_info(self) -> Dict[str, Any]: | ||
"""Return device information about this AdGuard Home instance.""" | ||
return { | ||
'identifiers': { | ||
( | ||
DOMAIN, | ||
self.adguard.host, | ||
self.adguard.port, | ||
self.adguard.base_path, | ||
) | ||
}, | ||
'name': 'AdGuard Home', | ||
'manufacturer': 'AdGuard Team', | ||
'sw_version': self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
"""Config flow to configure the AdGuard Home integration.""" | ||
import logging | ||
|
||
from adguardhome import AdGuardHome, AdGuardHomeConnectionError | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.components.adguard.const import DOMAIN | ||
from homeassistant.config_entries import ConfigFlow | ||
from homeassistant.const import ( | ||
CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, | ||
CONF_VERIFY_SSL) | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
@config_entries.HANDLERS.register(DOMAIN) | ||
class AdGuardHomeFlowHandler(ConfigFlow): | ||
"""Handle a AdGuard Home config flow.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL | ||
|
||
_hassio_discovery = None | ||
|
||
def __init__(self): | ||
"""Initialize AgGuard Home flow.""" | ||
pass | ||
|
||
async def _show_setup_form(self, errors=None): | ||
"""Show the setup form to the user.""" | ||
return self.async_show_form( | ||
step_id='user', | ||
data_schema=vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST): str, | ||
vol.Required(CONF_PORT, default=3000): vol.Coerce(int), | ||
vol.Optional(CONF_USERNAME): str, | ||
vol.Optional(CONF_PASSWORD): str, | ||
vol.Required(CONF_SSL, default=True): bool, | ||
vol.Required(CONF_VERIFY_SSL, default=True): bool, | ||
} | ||
), | ||
errors=errors or {}, | ||
) | ||
|
||
async def _show_hassio_form(self, errors=None): | ||
"""Show the Hass.io confirmation form to the user.""" | ||
return self.async_show_form( | ||
step_id='hassio_confirm', | ||
description_placeholders={ | ||
'addon': self._hassio_discovery['addon'] | ||
}, | ||
data_schema=vol.Schema({}), | ||
errors=errors or {}, | ||
) | ||
|
||
async def async_step_user(self, user_input=None): | ||
"""Handle a flow initiated by the user.""" | ||
if self._async_current_entries(): | ||
return self.async_abort(reason='single_instance_allowed') | ||
|
||
if user_input is None: | ||
return await self._show_setup_form(user_input) | ||
|
||
errors = {} | ||
|
||
session = async_get_clientsession( | ||
self.hass, user_input[CONF_VERIFY_SSL] | ||
) | ||
|
||
adguard = AdGuardHome( | ||
user_input[CONF_HOST], | ||
port=user_input[CONF_PORT], | ||
username=user_input.get(CONF_USERNAME), | ||
password=user_input.get(CONF_PASSWORD), | ||
tls=user_input[CONF_SSL], | ||
verify_ssl=user_input[CONF_VERIFY_SSL], | ||
loop=self.hass.loop, | ||
session=session, | ||
) | ||
|
||
try: | ||
await adguard.version() | ||
except AdGuardHomeConnectionError: | ||
errors['base'] = 'connection_error' | ||
return await self._show_setup_form(errors) | ||
|
||
return self.async_create_entry( | ||
title=user_input[CONF_HOST], | ||
data={ | ||
CONF_HOST: user_input[CONF_HOST], | ||
CONF_PASSWORD: user_input.get(CONF_PASSWORD), | ||
CONF_PORT: user_input[CONF_PORT], | ||
CONF_SSL: user_input[CONF_SSL], | ||
CONF_USERNAME: user_input.get(CONF_USERNAME), | ||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], | ||
}, | ||
) | ||
|
||
async def async_step_hassio(self, user_input=None): | ||
"""Prepare configuration for a Hass.io AdGuard Home add-on. | ||
This flow is triggered by the discovery component. | ||
""" | ||
if self._async_current_entries(): | ||
return self.async_abort(reason='single_instance_allowed') | ||
|
||
self._hassio_discovery = user_input | ||
|
||
return await self.async_step_hassio_confirm() | ||
|
||
async def async_step_hassio_confirm(self, user_input=None): | ||
"""Confirm Hass.io discovery.""" | ||
if user_input is None: | ||
return await self._show_hassio_form() | ||
|
||
errors = {} | ||
|
||
session = async_get_clientsession(self.hass, False) | ||
|
||
adguard = AdGuardHome( | ||
self._hassio_discovery[CONF_HOST], | ||
port=self._hassio_discovery[CONF_PORT], | ||
tls=False, | ||
loop=self.hass.loop, | ||
session=session, | ||
) | ||
|
||
try: | ||
await adguard.version() | ||
except AdGuardHomeConnectionError: | ||
errors['base'] = 'connection_error' | ||
return await self._show_hassio_form(errors) | ||
|
||
return self.async_create_entry( | ||
title=self._hassio_discovery['addon'], | ||
data={ | ||
CONF_HOST: self._hassio_discovery[CONF_HOST], | ||
CONF_PORT: self._hassio_discovery[CONF_PORT], | ||
CONF_PASSWORD: None, | ||
CONF_SSL: False, | ||
CONF_USERNAME: None, | ||
CONF_VERIFY_SSL: True, | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
"""Constants for the AdGuard Home integration.""" | ||
|
||
DOMAIN = 'adguard' | ||
|
||
DATA_ADGUARD_CLIENT = 'adguard_client' | ||
DATA_ADGUARD_VERION = 'adguard_version' | ||
|
||
CONF_FORCE = 'force' | ||
|
||
SERVICE_ADD_URL = 'add_url' | ||
SERVICE_DISABLE_URL = 'disable_url' | ||
SERVICE_ENABLE_URL = 'enable_url' | ||
SERVICE_REFRESH = 'refresh' | ||
SERVICE_REMOVE_URL = 'remove_url' |
Oops, something went wrong.