mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +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."""
|
||||
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."""
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -86,6 +86,7 @@ TEST_REQUIREMENTS = (
|
||||
"haversine",
|
||||
"hbmqtt",
|
||||
"hdate",
|
||||
"hole",
|
||||
"holidays",
|
||||
"home-assistant-frontend",
|
||||
"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