mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 07:05:48 +01:00
Add 'stations near me' to radio browser (#150907)
This commit is contained in:
@@ -16,6 +16,7 @@ from homeassistant.components.media_source import (
|
||||
Unresolvable,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util.location import vincenty
|
||||
|
||||
from . import RadioBrowserConfigEntry
|
||||
from .const import DOMAIN
|
||||
@@ -88,6 +89,7 @@ class RadioMediaSource(MediaSource):
|
||||
*await self._async_build_popular(radios, item),
|
||||
*await self._async_build_by_tag(radios, item),
|
||||
*await self._async_build_by_language(radios, item),
|
||||
*await self._async_build_local(radios, item),
|
||||
*await self._async_build_by_country(radios, item),
|
||||
],
|
||||
)
|
||||
@@ -292,3 +294,63 @@ class RadioMediaSource(MediaSource):
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
def _filter_local_stations(
|
||||
self, stations: list[Station], latitude: float, longitude: float
|
||||
) -> list[Station]:
|
||||
return [
|
||||
station
|
||||
for station in stations
|
||||
if station.latitude is not None
|
||||
and station.longitude is not None
|
||||
and (
|
||||
(
|
||||
dist := vincenty(
|
||||
(latitude, longitude),
|
||||
(station.latitude, station.longitude),
|
||||
False,
|
||||
)
|
||||
)
|
||||
is not None
|
||||
)
|
||||
and dist < 100
|
||||
]
|
||||
|
||||
async def _async_build_local(
|
||||
self, radios: RadioBrowser, item: MediaSourceItem
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Handle browsing local radio stations."""
|
||||
|
||||
if item.identifier == "local":
|
||||
country = self.hass.config.country
|
||||
stations = await radios.stations(
|
||||
filter_by=FilterBy.COUNTRY_CODE_EXACT,
|
||||
filter_term=country,
|
||||
hide_broken=True,
|
||||
order=Order.NAME,
|
||||
reverse=False,
|
||||
)
|
||||
|
||||
local_stations = await self.hass.async_add_executor_job(
|
||||
self._filter_local_stations,
|
||||
stations,
|
||||
self.hass.config.latitude,
|
||||
self.hass.config.longitude,
|
||||
)
|
||||
|
||||
return self._async_build_stations(radios, local_stations)
|
||||
|
||||
if not item.identifier:
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier="local",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaType.MUSIC,
|
||||
title="Local stations",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.radio_browser.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -29,3 +30,108 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"homeassistant.components.radio_browser.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Radio Browser integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_countries():
|
||||
"Generate mock countries for the countries method of the radios object."
|
||||
|
||||
class MockCountry:
|
||||
"""Country Object for Radios."""
|
||||
|
||||
def __init__(self, code, name) -> None:
|
||||
"""Initialize a mock country."""
|
||||
self.code = code
|
||||
self.name = name
|
||||
self.favicon = "fake.png"
|
||||
|
||||
return [MockCountry("US", "United States")]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_stations():
|
||||
"Generate mock stations for the stations method of the radios object."
|
||||
|
||||
class MockStation:
|
||||
"""Station object for Radios."""
|
||||
|
||||
def __init__(self, country_code, latitude, longitude, name, uuid) -> None:
|
||||
"""Initialize a mock station."""
|
||||
self.country_code = country_code
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.uuid = uuid
|
||||
self.name = name
|
||||
self.codec = "MP3"
|
||||
self.favicon = "fake.png"
|
||||
|
||||
return [
|
||||
MockStation(
|
||||
country_code="US",
|
||||
latitude=45.52000,
|
||||
longitude=-122.63961,
|
||||
name="Near Station 1",
|
||||
uuid="1",
|
||||
),
|
||||
MockStation(
|
||||
country_code="US",
|
||||
latitude=None,
|
||||
longitude=None,
|
||||
name="Unknown location station",
|
||||
uuid="2",
|
||||
),
|
||||
MockStation(
|
||||
country_code="US",
|
||||
latitude=47.57071,
|
||||
longitude=-122.21148,
|
||||
name="Moderate Far Station",
|
||||
uuid="3",
|
||||
),
|
||||
MockStation(
|
||||
country_code="US",
|
||||
latitude=45.73943,
|
||||
longitude=-121.51859,
|
||||
name="Near Station 2",
|
||||
uuid="4",
|
||||
),
|
||||
MockStation(
|
||||
country_code="US",
|
||||
latitude=44.99026,
|
||||
longitude=-69.27804,
|
||||
name="Really Far Station",
|
||||
uuid="5",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_radios(mock_countries, mock_stations):
|
||||
"""Provide a radios mock object."""
|
||||
radios = MagicMock()
|
||||
radios.countries = AsyncMock(return_value=mock_countries)
|
||||
radios.stations = AsyncMock(return_value=mock_stations)
|
||||
return radios
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patch_radios(monkeypatch: pytest.MonkeyPatch, mock_radios):
|
||||
"""Replace the radios object in the source with the mock object (with mock stations and countries)."""
|
||||
|
||||
def _patch(source):
|
||||
monkeypatch.setattr(type(source), "radios", mock_radios)
|
||||
|
||||
return _patch
|
||||
|
||||
73
tests/components/radio_browser/test_media_source.py
Normal file
73
tests/components/radio_browser/test_media_source.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Tests for radio_browser media_source."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from radios import FilterBy, Order
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.radio_browser.media_source import async_get_media_source
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
DOMAIN = "radio_browser"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_media_source(hass: HomeAssistant) -> None:
|
||||
"""Set up media source."""
|
||||
assert await async_setup_component(hass, "media_source", {})
|
||||
|
||||
|
||||
async def test_browsing_local(
|
||||
hass: HomeAssistant, init_integration: AsyncMock, patch_radios
|
||||
) -> None:
|
||||
"""Test browsing local stations."""
|
||||
|
||||
hass.config.latitude = 45.58539
|
||||
hass.config.longitude = -122.40320
|
||||
hass.config.country = "US"
|
||||
|
||||
source = await async_get_media_source(hass)
|
||||
patch_radios(source)
|
||||
|
||||
item = await media_source.async_browse_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}"
|
||||
)
|
||||
|
||||
assert item is not None
|
||||
assert item.title == "My Radios"
|
||||
assert item.children is not None
|
||||
assert len(item.children) == 5
|
||||
assert item.can_play is False
|
||||
assert item.can_expand is True
|
||||
|
||||
assert item.children[3].title == "Local stations"
|
||||
|
||||
item_child = await media_source.async_browse_media(
|
||||
hass, item.children[3].media_content_id
|
||||
)
|
||||
|
||||
source.radios.stations.assert_awaited_with(
|
||||
filter_by=FilterBy.COUNTRY_CODE_EXACT,
|
||||
filter_term=hass.config.country,
|
||||
hide_broken=True,
|
||||
order=Order.NAME,
|
||||
reverse=False,
|
||||
)
|
||||
|
||||
assert item_child is not None
|
||||
assert item_child.title == "My Radios"
|
||||
assert len(item_child.children) == 2
|
||||
assert item_child.children[0].title == "Near Station 1"
|
||||
assert item_child.children[1].title == "Near Station 2"
|
||||
|
||||
# Test browsing a different category to hit the path where async_build_local
|
||||
# returns []
|
||||
other_browse = await media_source.async_browse_media(
|
||||
hass, f"{media_source.URI_SCHEME}{DOMAIN}/nonexistent"
|
||||
)
|
||||
|
||||
assert other_browse is not None
|
||||
assert other_browse.title == "My Radios"
|
||||
assert len(other_browse.children) == 0
|
||||
Reference in New Issue
Block a user