Improve typing in DuneHD integration (#51025)

* Improve typing

* One more typing fix

* Run hassfest

* Fix test

* Fix return from constructor

* Add missing Final

* Improve long string format

* Use bool for mute

* Remove unnecessary str type

* Fix host type

* Add missing Final

* Increase test coverage

* Suggested change

Co-authored-by: Ruslan Sayfutdinov <ruslan@sayfutdinov.com>

Co-authored-by: Ruslan Sayfutdinov <ruslan@sayfutdinov.com>
This commit is contained in:
Maciej Bieniek 2021-05-24 21:09:57 +02:00 committed by GitHub
parent 394e044c66
commit 12e2c59a4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 116 additions and 68 deletions

View file

@ -23,6 +23,7 @@ homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.device_tracker.* homeassistant.components.device_tracker.*
homeassistant.components.dunehd.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.fitbit.* homeassistant.components.fitbit.*
homeassistant.components.fritzbox.* homeassistant.components.fritzbox.*

View file

@ -1,16 +1,22 @@
"""The Dune HD component.""" """The Dune HD component."""
from __future__ import annotations
from typing import Final
from pdunehd import DuneHDPlayer from pdunehd import DuneHDPlayer
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from .const import DOMAIN from .const import DOMAIN
PLATFORMS = ["media_player"] PLATFORMS: Final[list[str]] = ["media_player"]
async def async_setup_entry(hass, entry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
host = entry.data[CONF_HOST] host: str = entry.data[CONF_HOST]
player = DuneHDPlayer(host) player = DuneHDPlayer(host)
@ -22,7 +28,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 a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok: if unload_ok:

View file

@ -1,29 +1,34 @@
"""Adds config flow for Dune HD integration.""" """Adds config flow for Dune HD integration."""
from __future__ import annotations
import ipaddress import ipaddress
import logging import logging
import re import re
from typing import Any, Final
from pdunehd import DuneHDPlayer from pdunehd import DuneHDPlayer
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
def host_valid(host): def host_valid(host: str) -> bool:
"""Return True if hostname or IP address is valid.""" """Return True if hostname or IP address is valid."""
try: try:
if ipaddress.ip_address(host).version in [4, 6]: if ipaddress.ip_address(host).version in [4, 6]:
return True return True
except ValueError: except ValueError:
if len(host) > 253: pass
return False if len(host) > 253:
allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(?<!-)$", re.IGNORECASE) return False
return all(allowed.match(x) for x in host.split(".")) allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(?<!-)$", re.IGNORECASE)
return all(allowed.match(x) for x in host.split("."))
class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -31,35 +36,33 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
def __init__(self): async def init_device(self, host: str) -> None:
"""Initialize."""
self.host = None
async def init_device(self, host):
"""Initialize Dune HD player.""" """Initialize Dune HD player."""
player = DuneHDPlayer(host) player = DuneHDPlayer(host)
state = await self.hass.async_add_executor_job(player.update_state) state = await self.hass.async_add_executor_job(player.update_state)
if not state: if not state:
raise CannotConnect() raise CannotConnect()
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
if host_valid(user_input[CONF_HOST]): if host_valid(user_input[CONF_HOST]):
self.host = user_input[CONF_HOST] host: str = user_input[CONF_HOST]
try: try:
if self.host_already_configured(self.host): if self.host_already_configured(host):
raise AlreadyConfigured() raise AlreadyConfigured()
await self.init_device(self.host) await self.init_device(host)
except CannotConnect: except CannotConnect:
errors[CONF_HOST] = "cannot_connect" errors[CONF_HOST] = "cannot_connect"
except AlreadyConfigured: except AlreadyConfigured:
errors[CONF_HOST] = "already_configured" errors[CONF_HOST] = "already_configured"
else: else:
return self.async_create_entry(title=self.host, data=user_input) return self.async_create_entry(title=host, data=user_input)
else: else:
errors[CONF_HOST] = "invalid_host" errors[CONF_HOST] = "invalid_host"
@ -69,21 +72,24 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_import(self, user_input=None): async def async_step_import(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle configuration by yaml file.""" """Handle configuration by yaml file."""
self.host = user_input[CONF_HOST] assert user_input is not None
host: str = user_input[CONF_HOST]
self._async_abort_entries_match({CONF_HOST: self.host}) self._async_abort_entries_match({CONF_HOST: host})
try: try:
await self.init_device(self.host) await self.init_device(host)
except CannotConnect: except CannotConnect:
_LOGGER.error("Import aborted, cannot connect to %s", self.host) _LOGGER.error("Import aborted, cannot connect to %s", host)
return self.async_abort(reason="cannot_connect") return self.async_abort(reason="cannot_connect")
else: else:
return self.async_create_entry(title=self.host, data=user_input) return self.async_create_entry(title=host, data=user_input)
def host_already_configured(self, host): def host_already_configured(self, host: str) -> bool:
"""See if we already have a dunehd entry matching user input configured.""" """See if we already have a dunehd entry matching user input configured."""
existing_hosts = { existing_hosts = {
entry.data[CONF_HOST] for entry in self._async_current_entries() entry.data[CONF_HOST] for entry in self._async_current_entries()

View file

@ -1,4 +1,8 @@
"""Constants for Dune HD integration.""" """Constants for Dune HD integration."""
ATTR_MANUFACTURER = "Dune" from __future__ import annotations
DOMAIN = "dunehd"
DEFAULT_NAME = "Dune HD" from typing import Final
ATTR_MANUFACTURER: Final = "Dune"
DOMAIN: Final = "dunehd"
DEFAULT_NAME: Final = "Dune HD"

View file

@ -1,7 +1,15 @@
"""Dune HD implementation of the media player.""" """Dune HD implementation of the media player."""
from __future__ import annotations
from typing import Any, Final
from pdunehd import DuneHDPlayer
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player import (
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PAUSE,
@ -10,7 +18,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_TURN_OFF, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_TURN_ON,
) )
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
@ -19,13 +27,17 @@ from homeassistant.const import (
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, STATE_PLAYING,
) )
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN
CONF_SOURCES = "sources" CONF_SOURCES: Final = "sources"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}),
@ -33,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
} }
) )
DUNEHD_PLAYER_SUPPORT = ( DUNEHD_PLAYER_SUPPORT: Final[int] = (
SUPPORT_PAUSE SUPPORT_PAUSE
| SUPPORT_TURN_ON | SUPPORT_TURN_ON
| SUPPORT_TURN_OFF | SUPPORT_TURN_OFF
@ -43,9 +55,14 @@ DUNEHD_PLAYER_SUPPORT = (
) )
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dune HD media player platform.""" """Set up the Dune HD media player platform."""
host = config.get(CONF_HOST) host: str = config[CONF_HOST]
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( hass.config_entries.flow.async_init(
@ -54,11 +71,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
) )
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add Dune HD entities from a config_entry.""" """Add Dune HD entities from a config_entry."""
unique_id = config_entry.entry_id unique_id = entry.entry_id
player = hass.data[DOMAIN][config_entry.entry_id] player: str = hass.data[DOMAIN][entry.entry_id]
async_add_entities([DuneHDPlayerEntity(player, DEFAULT_NAME, unique_id)], True) async_add_entities([DuneHDPlayerEntity(player, DEFAULT_NAME, unique_id)], True)
@ -66,22 +85,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DuneHDPlayerEntity(MediaPlayerEntity): class DuneHDPlayerEntity(MediaPlayerEntity):
"""Implementation of the Dune HD player.""" """Implementation of the Dune HD player."""
def __init__(self, player, name, unique_id): def __init__(self, player: DuneHDPlayer, name: str, unique_id: str) -> None:
"""Initialize entity to control Dune HD.""" """Initialize entity to control Dune HD."""
self._player = player self._player = player
self._name = name self._name = name
self._media_title = None self._media_title: str | None = None
self._state = None self._state: dict[str, Any] = {}
self._unique_id = unique_id self._unique_id = unique_id
def update(self): def update(self) -> bool:
"""Update internal status of the entity.""" """Update internal status of the entity."""
self._state = self._player.update_state() self._state = self._player.update_state()
self.__update_title() self.__update_title()
return True return True
@property @property
def state(self): def state(self) -> StateType:
"""Return player state.""" """Return player state."""
state = STATE_OFF state = STATE_OFF
if "playback_position" in self._state: if "playback_position" in self._state:
@ -95,22 +114,22 @@ class DuneHDPlayerEntity(MediaPlayerEntity):
return state return state
@property @property
def name(self): def name(self) -> str:
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._name
@property @property
def available(self): def available(self) -> bool:
"""Return True if entity is available.""" """Return True if entity is available."""
return bool(self._state) return len(self._state) > 0
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return a unique_id for this entity.""" """Return a unique_id for this entity."""
return self._unique_id return self._unique_id
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self._unique_id)}, "identifiers": {(DOMAIN, self._unique_id)},
@ -119,57 +138,58 @@ class DuneHDPlayerEntity(MediaPlayerEntity):
} }
@property @property
def volume_level(self): def volume_level(self) -> float:
"""Return the volume level of the media player (0..1).""" """Return the volume level of the media player (0..1)."""
return int(self._state.get("playback_volume", 0)) / 100 return int(self._state.get("playback_volume", 0)) / 100
@property @property
def is_volume_muted(self): def is_volume_muted(self) -> bool:
"""Return a boolean if volume is currently muted.""" """Return a boolean if volume is currently muted."""
return int(self._state.get("playback_mute", 0)) == 1 return int(self._state.get("playback_mute", 0)) == 1
@property @property
def supported_features(self): def supported_features(self) -> int:
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
return DUNEHD_PLAYER_SUPPORT return DUNEHD_PLAYER_SUPPORT
def volume_up(self): def volume_up(self) -> None:
"""Volume up media player.""" """Volume up media player."""
self._state = self._player.volume_up() self._state = self._player.volume_up()
def volume_down(self): def volume_down(self) -> None:
"""Volume down media player.""" """Volume down media player."""
self._state = self._player.volume_down() self._state = self._player.volume_down()
def mute_volume(self, mute): def mute_volume(self, mute: bool) -> None:
"""Mute/unmute player volume.""" """Mute/unmute player volume."""
self._state = self._player.mute(mute) self._state = self._player.mute(mute)
def turn_off(self): def turn_off(self) -> None:
"""Turn off media player.""" """Turn off media player."""
self._media_title = None self._media_title = None
self._state = self._player.turn_off() self._state = self._player.turn_off()
def turn_on(self): def turn_on(self) -> None:
"""Turn off media player.""" """Turn off media player."""
self._state = self._player.turn_on() self._state = self._player.turn_on()
def media_play(self): def media_play(self) -> None:
"""Play media player.""" """Play media player."""
self._state = self._player.play() self._state = self._player.play()
def media_pause(self): def media_pause(self) -> None:
"""Pause media player.""" """Pause media player."""
self._state = self._player.pause() self._state = self._player.pause()
@property @property
def media_title(self): def media_title(self) -> str | None:
"""Return the current media source.""" """Return the current media source."""
self.__update_title() self.__update_title()
if self._media_title: if self._media_title:
return self._media_title return self._media_title
return None
def __update_title(self): def __update_title(self) -> None:
if self._state.get("player_state") == "bluray_playback": if self._state.get("player_state") == "bluray_playback":
self._media_title = "Blu-Ray" self._media_title = "Blu-Ray"
elif self._state.get("player_state") == "photo_viewer": elif self._state.get("player_state") == "photo_viewer":
@ -179,10 +199,10 @@ class DuneHDPlayerEntity(MediaPlayerEntity):
else: else:
self._media_title = None self._media_title = None
def media_previous_track(self): def media_previous_track(self) -> None:
"""Send previous track command.""" """Send previous track command."""
self._state = self._player.previous_track() self._state = self._player.previous_track()
def media_next_track(self): def media_next_track(self) -> None:
"""Send next track command.""" """Send next track command."""
self._state = self._player.next_track() self._state = self._player.next_track()

View file

@ -264,6 +264,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.dunehd.*]
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.elgato.*] [mypy-homeassistant.components.elgato.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View file

@ -67,10 +67,10 @@ async def test_user_invalid_host(hass):
async def test_user_very_long_host(hass): async def test_user_very_long_host(hass):
"""Test that errors are shown when the host is longer than 253 chars.""" """Test that errors are shown when the host is longer than 253 chars."""
long_host = ( long_host = (
"very_long_host_very_long_host_very_long_host_very_long_host_very_long" "very_long_host_very_long_host_very_long_host_very_long_host_very_long_"
"host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_ho" "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_"
"st_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_" "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_"
"very_long_host_very_long_host" "host_very_long_host_very_long_host"
) )
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: long_host} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: long_host}