Static typing for PiHole (#51681)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Yuval Aboulafia 2021-06-22 12:50:50 +03:00 committed by GitHub
parent 9cd3ffbd47
commit 39bf304031
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 40 deletions

View file

@ -56,6 +56,7 @@ homeassistant.components.notify.*
homeassistant.components.number.* homeassistant.components.number.*
homeassistant.components.onewire.* homeassistant.components.onewire.*
homeassistant.components.persistent_notification.* homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.proximity.* homeassistant.components.proximity.*
homeassistant.components.recorder.purge homeassistant.components.recorder.purge
homeassistant.components.recorder.repack homeassistant.components.recorder.repack

View file

@ -1,11 +1,13 @@
"""The pi_hole component.""" """The pi_hole component."""
from __future__ import annotations
import logging import logging
from hole import Hole from hole import Hole
from hole.exceptions import HoleError from hole.exceptions import HoleError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_HOST, CONF_HOST,
@ -13,10 +15,12 @@ from homeassistant.const import (
CONF_SSL, CONF_SSL,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.core import callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -60,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema(
) )
async def async_setup(hass, config): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Pi-hole integration.""" """Set up the Pi-hole integration."""
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
@ -77,7 +81,7 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, entry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Pi-hole entry.""" """Set up Pi-hole entry."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
@ -109,7 +113,7 @@ async def async_setup_entry(hass, entry):
_LOGGER.warning("Failed to connect: %s", ex) _LOGGER.warning("Failed to connect: %s", ex)
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
async def async_update_data(): async def async_update_data() -> None:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
try: try:
await api.get_data() await api.get_data()
@ -133,7 +137,7 @@ async def async_setup_entry(hass, entry):
return True return True
async def async_unload_entry(hass, entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Pi-hole entry.""" """Unload Pi-hole entry."""
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
entry, _async_platforms(entry) entry, _async_platforms(entry)
@ -144,7 +148,7 @@ async def async_unload_entry(hass, entry):
@callback @callback
def _async_platforms(entry): def _async_platforms(entry: ConfigEntry) -> list[str]:
"""Return platforms to be loaded / unloaded.""" """Return platforms to be loaded / unloaded."""
platforms = ["sensor"] platforms = ["sensor"]
if not entry.data[CONF_STATISTICS_ONLY]: if not entry.data[CONF_STATISTICS_ONLY]:
@ -157,7 +161,13 @@ def _async_platforms(entry):
class PiHoleEntity(CoordinatorEntity): class PiHoleEntity(CoordinatorEntity):
"""Representation of a Pi-hole entity.""" """Representation of a Pi-hole entity."""
def __init__(self, api, coordinator, name, server_unique_id): def __init__(
self,
api: Hole,
coordinator: DataUpdateCoordinator,
name: str,
server_unique_id: str,
) -> None:
"""Initialize a Pi-hole entity.""" """Initialize a Pi-hole entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.api = api self.api = api
@ -165,12 +175,12 @@ class PiHoleEntity(CoordinatorEntity):
self._server_unique_id = server_unique_id self._server_unique_id = server_unique_id
@property @property
def icon(self): def icon(self) -> str:
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return "mdi:pi-hole" return "mdi:pi-hole"
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device information of the entity.""" """Return the device information of the entity."""
return { return {
"identifiers": {(DOMAIN, self._server_unique_id)}, "identifiers": {(DOMAIN, self._server_unique_id)},

View file

@ -1,12 +1,17 @@
"""Support for getting status from a Pi-hole system.""" """Support for getting status from a Pi-hole system."""
from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PiHoleEntity from . import PiHoleEntity
from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Pi-hole binary sensor.""" """Set up the Pi-hole binary sensor."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@ -25,16 +30,16 @@ class PiHoleBinarySensor(PiHoleEntity, BinarySensorEntity):
"""Representation of a Pi-hole binary sensor.""" """Representation of a Pi-hole binary sensor."""
@property @property
def name(self): def name(self) -> str:
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return self._name
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique id of the sensor.""" """Return the unique id of the sensor."""
return f"{self._server_unique_id}/Status" return f"{self._server_unique_id}/Status"
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return if the service is on.""" """Return if the service is on."""
return self.api.data.get("status") == "enabled" return self.api.data.get("status") == "enabled" # type: ignore[no-any-return]

View file

@ -1,5 +1,8 @@
"""Config flow to configure the Pi-hole integration.""" """Config flow to configure the Pi-hole integration."""
from __future__ import annotations
import logging import logging
from typing import Any
from hole import Hole from hole import Hole
from hole.exceptions import HoleError from hole.exceptions import HoleError
@ -24,6 +27,7 @@ from homeassistant.const import (
CONF_SSL, CONF_SSL,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,19 +38,25 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self): def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self._config = None self._config: dict = {}
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
return await self.async_step_init(user_input) return await self.async_step_init(user_input)
async def async_step_import(self, user_input=None): async def async_step_import(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by import.""" """Handle a flow initiated by import."""
return await self.async_step_init(user_input, is_import=True) return await self.async_step_init(user_input, is_import=True)
async def async_step_init(self, user_input, is_import=False): async def async_step_init(
self, user_input: dict[str, Any] | None, is_import: bool = False
) -> FlowResult:
"""Handle init step of a flow.""" """Handle init step of a flow."""
errors = {} errors = {}
@ -131,7 +141,9 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_api_key(self, user_input=None): async def async_step_api_key(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle step to setup API key.""" """Handle step to setup API key."""
if user_input is not None: if user_input is not None:
return self.async_create_entry( return self.async_create_entry(
@ -147,14 +159,16 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}), data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}),
) )
async def _async_endpoint_existed(self, endpoint): async def _async_endpoint_existed(self, endpoint: str) -> bool:
existing_endpoints = [ existing_endpoints = [
f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}"
for entry in self._async_current_entries() for entry in self._async_current_entries()
] ]
return endpoint in existing_endpoints return endpoint in existing_endpoints
async def _async_try_connect(self, host, location, tls, verify_tls): async def _async_try_connect(
self, host: str, location: str, tls: bool, verify_tls: bool
) -> None:
session = async_get_clientsession(self.hass, verify_tls) session = async_get_clientsession(self.hass, verify_tls)
pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls) pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls)
await pi_hole.get_data() await pi_hole.get_data()

View file

@ -1,7 +1,16 @@
"""Support for getting statistical data from a Pi-hole system.""" """Support for getting statistical data from a Pi-hole system."""
from __future__ import annotations
from typing import Any
from hole import Hole
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import PiHoleEntity from . import PiHoleEntity
from .const import ( from .const import (
@ -14,7 +23,9 @@ from .const import (
) )
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Pi-hole sensor.""" """Set up the Pi-hole sensor."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@ -34,7 +45,14 @@ async def async_setup_entry(hass, entry, async_add_entities):
class PiHoleSensor(PiHoleEntity, SensorEntity): class PiHoleSensor(PiHoleEntity, SensorEntity):
"""Representation of a Pi-hole sensor.""" """Representation of a Pi-hole sensor."""
def __init__(self, api, coordinator, name, sensor_name, server_unique_id): def __init__(
self,
api: Hole,
coordinator: DataUpdateCoordinator,
name: str,
sensor_name: str,
server_unique_id: str,
) -> None:
"""Initialize a Pi-hole sensor.""" """Initialize a Pi-hole sensor."""
super().__init__(api, coordinator, name, server_unique_id) super().__init__(api, coordinator, name, server_unique_id)
@ -46,27 +64,27 @@ class PiHoleSensor(PiHoleEntity, SensorEntity):
self._icon = variable_info[2] self._icon = variable_info[2]
@property @property
def name(self): def name(self) -> str:
"""Return the name of the sensor.""" """Return the name of the sensor."""
return f"{self._name} {self._condition_name}" return f"{self._name} {self._condition_name}"
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique id of the sensor.""" """Return the unique id of the sensor."""
return f"{self._server_unique_id}/{self._condition_name}" return f"{self._server_unique_id}/{self._condition_name}"
@property @property
def icon(self): def icon(self) -> str:
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return self._icon return self._icon
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._unit_of_measurement return self._unit_of_measurement
@property @property
def state(self): def state(self) -> Any:
"""Return the state of the device.""" """Return the state of the device."""
try: try:
return round(self.api.data[self._condition], 2) return round(self.api.data[self._condition], 2)
@ -74,6 +92,6 @@ class PiHoleSensor(PiHoleEntity, SensorEntity):
return self.api.data[self._condition] return self.api.data[self._condition]
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the Pi-hole.""" """Return the state attributes of the Pi-hole."""
return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]}

View file

@ -1,12 +1,18 @@
"""Support for turning on and off Pi-hole system.""" """Support for turning on and off Pi-hole system."""
from __future__ import annotations
import logging import logging
from typing import Any
from hole.exceptions import HoleError from hole.exceptions import HoleError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PiHoleEntity from . import PiHoleEntity
from .const import ( from .const import (
@ -20,7 +26,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Pi-hole switch.""" """Set up the Pi-hole switch."""
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id] hole_data = hass.data[PIHOLE_DOMAIN][entry.entry_id]
@ -51,26 +59,26 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
"""Representation of a Pi-hole switch.""" """Representation of a Pi-hole switch."""
@property @property
def name(self): def name(self) -> str:
"""Return the name of the switch.""" """Return the name of the switch."""
return self._name return self._name
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique id of the switch.""" """Return the unique id of the switch."""
return f"{self._server_unique_id}/Switch" return f"{self._server_unique_id}/Switch"
@property @property
def icon(self): def icon(self) -> str:
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return "mdi:pi-hole" return "mdi:pi-hole"
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return if the service is on.""" """Return if the service is on."""
return self.api.data.get("status") == "enabled" return self.api.data.get("status") == "enabled" # type: ignore[no-any-return]
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the service.""" """Turn on the service."""
try: try:
await self.api.enable() await self.api.enable()
@ -78,11 +86,11 @@ class PiHoleSwitch(PiHoleEntity, SwitchEntity):
except HoleError as err: except HoleError as err:
_LOGGER.error("Unable to enable Pi-hole: %s", err) _LOGGER.error("Unable to enable Pi-hole: %s", err)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the service.""" """Turn off the service."""
await self.async_disable() await self.async_disable()
async def async_disable(self, duration=None): async def async_disable(self, duration: Any = None) -> None:
"""Disable the service for a given duration.""" """Disable the service for a given duration."""
duration_seconds = True # Disable infinitely by default duration_seconds = True # Disable infinitely by default
if duration is not None: if duration is not None:

View file

@ -627,6 +627,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.pi_hole.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.proximity.*] [mypy-homeassistant.components.proximity.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true