mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
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:
parent
9035efee10
commit
757482ee85
@ -1 +1,96 @@
|
|||||||
"""The pi_hole component."""
|
"""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
|
||||||
|
43
homeassistant/components/pi_hole/const.py
Normal file
43
homeassistant/components/pi_hole/const.py
Normal 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)
|
@ -1,100 +1,27 @@
|
|||||||
"""Support for getting statistical data from a Pi-hole system."""
|
"""Support for getting statistical data from a Pi-hole system."""
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
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.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from .const import (
|
||||||
|
DOMAIN as PIHOLE_DOMAIN,
|
||||||
ATTR_BLOCKED_DOMAINS = "domains_blocked"
|
ATTR_BLOCKED_DOMAINS,
|
||||||
ATTR_PERCENTAGE_TODAY = "percentage_today"
|
SENSOR_LIST,
|
||||||
ATTR_QUERIES_TODAY = "queries_today"
|
SENSOR_DICT,
|
||||||
|
|
||||||
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)]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the Pi-hole sensor."""
|
"""Set up the pi-hole sensor."""
|
||||||
from hole import Hole
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
name = config.get(CONF_NAME)
|
pi_hole = hass.data[PIHOLE_DOMAIN]
|
||||||
host = config.get(CONF_HOST)
|
|
||||||
use_tls = config.get(CONF_SSL)
|
|
||||||
location = config.get(CONF_LOCATION)
|
|
||||||
verify_tls = config.get(CONF_VERIFY_SSL)
|
|
||||||
|
|
||||||
session = async_get_clientsession(hass, verify_tls)
|
sensors = []
|
||||||
pi_hole = PiHoleData(Hole(host, hass.loop, session, location=location, tls=use_tls))
|
sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST]
|
||||||
|
|
||||||
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]
|
|
||||||
]
|
|
||||||
|
|
||||||
async_add_entities(sensors, True)
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
@ -102,13 +29,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
class PiHoleSensor(Entity):
|
class PiHoleSensor(Entity):
|
||||||
"""Representation of a Pi-hole sensor."""
|
"""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."""
|
"""Initialize a Pi-hole sensor."""
|
||||||
self.pi_hole = pi_hole
|
self.pi_hole = pi_hole
|
||||||
self._name = name
|
self._name = pi_hole.name
|
||||||
self._condition = condition
|
self._condition = sensor_name
|
||||||
|
|
||||||
variable_info = MONITORED_CONDITIONS[condition]
|
variable_info = SENSOR_DICT[sensor_name]
|
||||||
self._condition_name = variable_info[0]
|
self._condition_name = variable_info[0]
|
||||||
self._unit_of_measurement = variable_info[1]
|
self._unit_of_measurement = variable_info[1]
|
||||||
self._icon = variable_info[2]
|
self._icon = variable_info[2]
|
||||||
@ -151,24 +78,3 @@ class PiHoleSensor(Entity):
|
|||||||
"""Get the latest data from the Pi-hole API."""
|
"""Get the latest data from the Pi-hole API."""
|
||||||
await self.pi_hole.async_update()
|
await self.pi_hole.async_update()
|
||||||
self.data = self.pi_hole.api.data
|
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
|
|
||||||
|
@ -172,6 +172,9 @@ hbmqtt==0.9.4
|
|||||||
# homeassistant.components.jewish_calendar
|
# homeassistant.components.jewish_calendar
|
||||||
hdate==0.9.0
|
hdate==0.9.0
|
||||||
|
|
||||||
|
# homeassistant.components.pi_hole
|
||||||
|
hole==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.workday
|
# homeassistant.components.workday
|
||||||
holidays==0.9.11
|
holidays==0.9.11
|
||||||
|
|
||||||
|
@ -86,6 +86,7 @@ TEST_REQUIREMENTS = (
|
|||||||
"haversine",
|
"haversine",
|
||||||
"hbmqtt",
|
"hbmqtt",
|
||||||
"hdate",
|
"hdate",
|
||||||
|
"hole",
|
||||||
"holidays",
|
"holidays",
|
||||||
"home-assistant-frontend",
|
"home-assistant-frontend",
|
||||||
"homekit[IP]",
|
"homekit[IP]",
|
||||||
|
1
tests/components/pi_hole/__init__.py
Normal file
1
tests/components/pi_hole/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the pi_hole component."""
|
99
tests/components/pi_hole/test_init.py
Normal file
99
tests/components/pi_hole/test_init.py
Normal 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"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user