Refactor pihole integration (#25837)

* Adds tests for pi_hole integration

* Refactor pi_hole component to an integration supporting multiple platforms

* Adds mock of Hole dependency

* Aborts platform setup when discovery_info is none

* Removes use of monitored_conditions

* Adds integration setup test

* Removes PlatformNotReady check

* Adds sensor test

* Code review updates

* Refactor tests to assert state through hass

* Reorder imports
This commit is contained in:
John Luetke 2019-09-03 16:18:06 -07:00 committed by Paulus Schoutsen
parent 9035efee10
commit 757482ee85
7 changed files with 259 additions and 111 deletions

View File

@ -1 +1,96 @@
"""The pi_hole component."""
import logging
import voluptuous as vol
from hole import Hole
from hole.exceptions import HoleError
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.util import Throttle
from .const import (
DOMAIN,
CONF_LOCATION,
DEFAULT_HOST,
DEFAULT_LOCATION,
DEFAULT_NAME,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
MIN_TIME_BETWEEN_UPDATES,
)
LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the pi_hole integration."""
conf = config[DOMAIN]
name = conf[CONF_NAME]
host = conf[CONF_HOST]
use_tls = conf[CONF_SSL]
verify_tls = conf[CONF_VERIFY_SSL]
location = conf[CONF_LOCATION]
LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host)
session = async_get_clientsession(hass, True)
pi_hole = PiHoleData(
Hole(
host,
hass.loop,
session,
location=location,
tls=use_tls,
verify_tls=verify_tls,
),
name,
)
await pi_hole.async_update()
hass.data[DOMAIN] = pi_hole
hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config))
return True
class PiHoleData:
"""Get the latest data and update the states."""
def __init__(self, api, name):
"""Initialize the data object."""
self.api = api
self.name = name
self.available = True
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest data from the Pi-hole."""
try:
await self.api.get_data()
self.available = True
except HoleError:
LOGGER.error("Unable to fetch data from Pi-hole")
self.available = False

View File

@ -0,0 +1,43 @@
"""Constants for the pi_hole intergration."""
from datetime import timedelta
DOMAIN = "pi_hole"
CONF_LOCATION = "location"
DEFAULT_HOST = "pi.hole"
DEFAULT_LOCATION = "admin"
DEFAULT_METHOD = "GET"
DEFAULT_NAME = "Pi-Hole"
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
ATTR_BLOCKED_DOMAINS = "domains_blocked"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
SENSOR_DICT = {
"ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"],
"ads_percentage_today": [
"Ads Percentage Blocked Today",
"%",
"mdi:close-octagon-outline",
],
"clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"],
"dns_queries_today": [
"DNS Queries Today",
"queries",
"mdi:comment-question-outline",
],
"domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"],
"queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"],
"queries_forwarded": [
"DNS Queries Forwarded",
"queries",
"mdi:comment-question-outline",
],
"unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"],
"unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"],
}
SENSOR_LIST = list(SENSOR_DICT)

View File

@ -1,100 +1,27 @@
"""Support for getting statistical data from a Pi-hole system."""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_SSL,
CONF_VERIFY_SSL,
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
ATTR_BLOCKED_DOMAINS = "domains_blocked"
ATTR_PERCENTAGE_TODAY = "percentage_today"
ATTR_QUERIES_TODAY = "queries_today"
CONF_LOCATION = "location"
DEFAULT_HOST = "localhost"
DEFAULT_LOCATION = "admin"
DEFAULT_METHOD = "GET"
DEFAULT_NAME = "Pi-Hole"
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
MONITORED_CONDITIONS = {
"ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"],
"ads_percentage_today": [
"Ads Percentage Blocked Today",
"%",
"mdi:close-octagon-outline",
],
"clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"],
"dns_queries_today": [
"DNS Queries Today",
"queries",
"mdi:comment-question-outline",
],
"domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"],
"queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"],
"queries_forwarded": [
"DNS Queries Forwarded",
"queries",
"mdi:comment-question-outline",
],
"unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"],
"unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_MONITORED_CONDITIONS, default=["ads_blocked_today"]): vol.All(
cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]
),
}
from .const import (
DOMAIN as PIHOLE_DOMAIN,
ATTR_BLOCKED_DOMAINS,
SENSOR_LIST,
SENSOR_DICT,
)
LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Pi-hole sensor."""
from hole import Hole
"""Set up the pi-hole sensor."""
if discovery_info is None:
return
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
use_tls = config.get(CONF_SSL)
location = config.get(CONF_LOCATION)
verify_tls = config.get(CONF_VERIFY_SSL)
pi_hole = hass.data[PIHOLE_DOMAIN]
session = async_get_clientsession(hass, verify_tls)
pi_hole = PiHoleData(Hole(host, hass.loop, session, location=location, tls=use_tls))
await pi_hole.async_update()
if pi_hole.api.data is None:
raise PlatformNotReady
sensors = [
PiHoleSensor(pi_hole, name, condition)
for condition in config[CONF_MONITORED_CONDITIONS]
]
sensors = []
sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST]
async_add_entities(sensors, True)
@ -102,13 +29,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
class PiHoleSensor(Entity):
"""Representation of a Pi-hole sensor."""
def __init__(self, pi_hole, name, condition):
def __init__(self, pi_hole, sensor_name):
"""Initialize a Pi-hole sensor."""
self.pi_hole = pi_hole
self._name = name
self._condition = condition
self._name = pi_hole.name
self._condition = sensor_name
variable_info = MONITORED_CONDITIONS[condition]
variable_info = SENSOR_DICT[sensor_name]
self._condition_name = variable_info[0]
self._unit_of_measurement = variable_info[1]
self._icon = variable_info[2]
@ -151,24 +78,3 @@ class PiHoleSensor(Entity):
"""Get the latest data from the Pi-hole API."""
await self.pi_hole.async_update()
self.data = self.pi_hole.api.data
class PiHoleData:
"""Get the latest data and update the states."""
def __init__(self, api):
"""Initialize the data object."""
self.api = api
self.available = True
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest data from the Pi-hole."""
from hole.exceptions import HoleError
try:
await self.api.get_data()
self.available = True
except HoleError:
_LOGGER.error("Unable to fetch data from Pi-hole")
self.available = False

View File

@ -172,6 +172,9 @@ hbmqtt==0.9.4
# homeassistant.components.jewish_calendar
hdate==0.9.0
# homeassistant.components.pi_hole
hole==0.5.0
# homeassistant.components.workday
holidays==0.9.11

View File

@ -86,6 +86,7 @@ TEST_REQUIREMENTS = (
"haversine",
"hbmqtt",
"hdate",
"hole",
"holidays",
"home-assistant-frontend",
"homekit[IP]",

View File

@ -0,0 +1 @@
"""Tests for the pi_hole component."""

View File

@ -0,0 +1,99 @@
"""Test pi_hole component."""
from asynctest import CoroutineMock
from hole import Hole
from homeassistant.components import pi_hole
from tests.common import async_setup_component
from unittest.mock import patch
def mock_pihole_data_call(Hole):
"""Need to override so as to allow mocked data."""
Hole.__init__ = (
lambda self, host, loop, session, location, tls, verify_tls=True, api_token=None: None
)
Hole.data = {
"ads_blocked_today": 0,
"ads_percentage_today": 0,
"clients_ever_seen": 0,
"dns_queries_today": 0,
"domains_being_blocked": 0,
"queries_cached": 0,
"queries_forwarded": 0,
"status": 0,
"unique_clients": 0,
"unique_domains": 0,
}
pass
async def test_setup_no_config(hass):
"""Tests component setup with no config."""
with patch.object(
Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole))
):
assert await async_setup_component(hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {}})
await hass.async_block_till_done()
assert (
hass.states.get("sensor.pi_hole_ads_blocked_today").name
== "Pi-Hole Ads Blocked Today"
)
assert (
hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").name
== "Pi-Hole Ads Percentage Blocked Today"
)
assert (
hass.states.get("sensor.pi_hole_dns_queries_cached").name
== "Pi-Hole DNS Queries Cached"
)
assert (
hass.states.get("sensor.pi_hole_dns_queries_forwarded").name
== "Pi-Hole DNS Queries Forwarded"
)
assert (
hass.states.get("sensor.pi_hole_dns_queries_today").name
== "Pi-Hole DNS Queries Today"
)
assert (
hass.states.get("sensor.pi_hole_dns_unique_clients").name
== "Pi-Hole DNS Unique Clients"
)
assert (
hass.states.get("sensor.pi_hole_dns_unique_domains").name
== "Pi-Hole DNS Unique Domains"
)
assert (
hass.states.get("sensor.pi_hole_domains_blocked").name
== "Pi-Hole Domains Blocked"
)
assert hass.states.get("sensor.pi_hole_seen_clients").name == "Pi-Hole Seen Clients"
assert hass.states.get("sensor.pi_hole_ads_blocked_today").state == "0"
assert hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").state == "0"
assert hass.states.get("sensor.pi_hole_dns_queries_cached").state == "0"
assert hass.states.get("sensor.pi_hole_dns_queries_forwarded").state == "0"
assert hass.states.get("sensor.pi_hole_dns_queries_today").state == "0"
assert hass.states.get("sensor.pi_hole_dns_unique_clients").state == "0"
assert hass.states.get("sensor.pi_hole_dns_unique_domains").state == "0"
assert hass.states.get("sensor.pi_hole_domains_blocked").state == "0"
assert hass.states.get("sensor.pi_hole_seen_clients").state == "0"
async def test_setup_custom_config(hass):
"""Tests component setup with custom config."""
with patch.object(
Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole))
):
assert await async_setup_component(
hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {"name": "Custom"}}
)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.custom_ads_blocked_today").name
== "Custom Ads Blocked Today"
)