Skip to content

Commit

Permalink
Enable strict typing for the Squeezebox integration (#125161)
Browse files Browse the repository at this point in the history
* Strict typing for squeezebox

* Improve unit tests

* Refactor tests to use websockets and services.async_call

* Apply suggestions from code review

* Fix merge conflict
  • Loading branch information
rajlaud authored Sep 3, 2024
1 parent 00533ba commit 8f26cff
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 60 deletions.
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ homeassistant.components.solarlog.*
homeassistant.components.sonarr.*
homeassistant.components.speedtestdotnet.*
homeassistant.components.sql.*
homeassistant.components.squeezebox.*
homeassistant.components.ssdp.*
homeassistant.components.starlink.*
homeassistant.components.statistics.*
Expand Down
45 changes: 31 additions & 14 deletions homeassistant/components/squeezebox/browse_media.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
"""Support for media browsing."""

from __future__ import annotations

import contextlib
from typing import Any

from pysqueezebox import Player

from homeassistant.components import media_source
from homeassistant.components.media_player import (
BrowseError,
BrowseMedia,
MediaClass,
MediaPlayerEntity,
MediaType,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import is_internal_request

LIBRARY = ["Favorites", "Artists", "Albums", "Tracks", "Playlists", "Genres"]
Expand Down Expand Up @@ -36,7 +43,7 @@
"Favorites": "item_id",
}

CONTENT_TYPE_MEDIA_CLASS = {
CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | None]] = {
"Favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
"Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
"Albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
Expand Down Expand Up @@ -66,14 +73,18 @@
BROWSE_LIMIT = 1000


async def build_item_response(entity, player, payload):
async def build_item_response(
entity: MediaPlayerEntity, player: Player, payload: dict[str, str | None]
) -> BrowseMedia:
"""Create response payload for search described by payload."""

internal_request = is_internal_request(entity.hass)

search_id = payload["search_id"]
search_type = payload["search_type"]

assert (
search_type is not None
) # async_browse_media will not call this function if search_type is None
media_class = CONTENT_TYPE_MEDIA_CLASS[search_type]

children = None
Expand All @@ -95,9 +106,9 @@ async def build_item_response(entity, player, payload):
children = []
for item in result["items"]:
item_id = str(item["id"])
item_thumbnail = None
item_thumbnail: str | None = None
if item_type:
child_item_type = item_type
child_item_type: MediaType | str = item_type
child_media_class = CONTENT_TYPE_MEDIA_CLASS[item_type]
can_expand = child_media_class["children"] is not None
can_play = True
Expand All @@ -120,7 +131,7 @@ async def build_item_response(entity, player, payload):
can_expand = False
can_play = True

if artwork_track_id := item.get("artwork_track_id"):
if artwork_track_id := item.get("artwork_track_id") and item_type:
if internal_request:
item_thumbnail = player.generate_image_url_from_track_id(
artwork_track_id
Expand All @@ -132,6 +143,7 @@ async def build_item_response(entity, player, payload):
else:
item_thumbnail = item.get("image_url") # will not be proxied by HA

assert child_media_class["item"] is not None
children.append(
BrowseMedia(
title=item["title"],
Expand All @@ -147,6 +159,9 @@ async def build_item_response(entity, player, payload):
if children is None:
raise BrowseError(f"Media not found: {search_type} / {search_id}")

assert media_class["item"] is not None
if not search_id:
search_id = search_type
return BrowseMedia(
title=result.get("title"),
media_class=media_class["item"],
Expand All @@ -159,9 +174,9 @@ async def build_item_response(entity, player, payload):
)


async def library_payload(hass, player):
async def library_payload(hass: HomeAssistant, player: Player) -> BrowseMedia:
"""Create response payload to describe contents of library."""
library_info = {
library_info: dict[str, Any] = {
"title": "Music Library",
"media_class": MediaClass.DIRECTORY,
"media_content_id": "library",
Expand All @@ -179,6 +194,7 @@ async def library_payload(hass, player):
limit=1,
)
if result is not None and result.get("items") is not None:
assert media_class["children"] is not None
library_info["children"].append(
BrowseMedia(
title=item,
Expand All @@ -191,14 +207,14 @@ async def library_payload(hass, player):
)

with contextlib.suppress(media_source.BrowseError):
item = await media_source.async_browse_media(
browse = await media_source.async_browse_media(
hass, None, content_filter=media_source_content_filter
)
# If domain is None, it's overview of available sources
if item.domain is None:
library_info["children"].extend(item.children)
if browse.domain is None:
library_info["children"].extend(browse.children)
else:
library_info["children"].append(item)
library_info["children"].append(browse)

return BrowseMedia(**library_info)

Expand All @@ -208,7 +224,7 @@ def media_source_content_filter(item: BrowseMedia) -> bool:
return item.media_content_type.startswith("audio/")


async def generate_playlist(player, payload):
async def generate_playlist(player: Player, payload: dict[str, str]) -> list | None:
"""Generate playlist from browsing payload."""
media_type = payload["search_type"]
media_id = payload["search_id"]
Expand All @@ -221,5 +237,6 @@ async def generate_playlist(player, payload):
"titles", limit=BROWSE_LIMIT, browse_id=browse_id
)
if result and "items" in result:
return result["items"]
items: list = result["items"]
return items
raise BrowseError(f"Media not found: {media_type} / {media_id}")
22 changes: 15 additions & 7 deletions homeassistant/components/squeezebox/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Config flow for Squeezebox integration."""

from __future__ import annotations

import asyncio
from http import HTTPStatus
import logging
Expand All @@ -24,9 +26,11 @@
TIMEOUT = 5


def _base_schema(discovery_info=None):
def _base_schema(
discovery_info: dict[str, Any] | None = None,
) -> vol.Schema:
"""Generate base schema."""
base_schema = {}
base_schema: dict[Any, Any] = {}
if discovery_info and CONF_HOST in discovery_info:
base_schema.update(
{
Expand Down Expand Up @@ -71,14 +75,14 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize an instance of the squeezebox config flow."""
self.data_schema = _base_schema()
self.discovery_info = None
self.discovery_info: dict[str, Any] | None = None

async def _discover(self, uuid=None):
async def _discover(self, uuid: str | None = None) -> None:
"""Discover an unconfigured LMS server."""
self.discovery_info = None
discovery_event = asyncio.Event()

def _discovery_callback(server):
def _discovery_callback(server: Server) -> None:
if server.uuid:
# ignore already configured uuids
for entry in self._async_current_entries():
Expand Down Expand Up @@ -156,7 +160,9 @@ async def async_step_user(
errors=errors,
)

async def async_step_edit(self, user_input=None):
async def async_step_edit(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Edit a discovered or manually inputted server."""
errors = {}
if user_input:
Expand All @@ -171,7 +177,9 @@ async def async_step_edit(self, user_input=None):
step_id="edit", data_schema=self.data_schema, errors=errors
)

async def async_step_integration_discovery(self, discovery_info):
async def async_step_integration_discovery(
self, discovery_info: dict[str, Any]
) -> ConfigFlowResult:
"""Handle discovery of a server."""
_LOGGER.debug("Reached server discovery flow with info: %s", discovery_info)
if "uuid" in discovery_info:
Expand Down
Loading

0 comments on commit 8f26cff

Please sign in to comment.