Skip to content

Commit

Permalink
Adds AdGuard Home integration (home-assistant#24219)
Browse files Browse the repository at this point in the history
* 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
frenck authored and balloob committed Jun 2, 2019
1 parent 7be7d3f commit 9220270
Show file tree
Hide file tree
Showing 17 changed files with 1,095 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ omit =
homeassistant/components/abode/*
homeassistant/components/acer_projector/switch.py
homeassistant/components/actiontec/device_tracker.py
homeassistant/components/adguard/__init__.py
homeassistant/components/adguard/const.py
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aftership/sensor.py
homeassistant/components/airvisual/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ virtualization/Docker/* @home-assistant/docker
homeassistant/scripts/check_config.py @kellerza

# Integrations
homeassistant/components/adguard/* @frenck
homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alpha_vantage/* @fabaff
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/adguard/.translations/en.json
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."
}
}
}
180 changes: 180 additions & 0 deletions homeassistant/components/adguard/__init__.py
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),
}
147 changes: 147 additions & 0 deletions homeassistant/components/adguard/config_flow.py
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,
},
)
14 changes: 14 additions & 0 deletions homeassistant/components/adguard/const.py
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'
Loading

0 comments on commit 9220270

Please sign in to comment.