1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add Squeezebox binary sensors for player alarm status (#154491)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
wollew
2025-12-05 11:43:19 +01:00
committed by GitHub
parent afea571c2c
commit 4cd460351d
6 changed files with 200 additions and 21 deletions
@@ -10,17 +10,26 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import SqueezeboxConfigEntry
from .const import STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_RESCAN
from .entity import LMSStatusEntity
from . import SqueezeboxConfigEntry, SqueezeBoxPlayerUpdateCoordinator
from .const import (
PLAYER_SENSOR_ALARM_ACTIVE,
PLAYER_SENSOR_ALARM_SNOOZE,
PLAYER_SENSOR_ALARM_UPCOMING,
SIGNAL_PLAYER_DISCOVERED,
STATUS_SENSOR_NEEDSRESTART,
STATUS_SENSOR_RESCAN,
)
from .entity import LMSStatusEntity, SqueezeboxEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
SENSORS: tuple[BinarySensorEntityDescription, ...] = (
SERVER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key=STATUS_SENSOR_RESCAN,
device_class=BinarySensorDeviceClass.RUNNING,
@@ -32,6 +41,23 @@ SENSORS: tuple[BinarySensorEntityDescription, ...] = (
),
)
PLAYER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_UPCOMING,
translation_key=PLAYER_SENSOR_ALARM_UPCOMING,
),
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_ACTIVE,
translation_key=PLAYER_SENSOR_ALARM_ACTIVE,
device_class=BinarySensorDeviceClass.RUNNING,
),
BinarySensorEntityDescription(
key=PLAYER_SENSOR_ALARM_SNOOZE,
translation_key=PLAYER_SENSOR_ALARM_SNOOZE,
device_class=BinarySensorDeviceClass.RUNNING,
),
)
_LOGGER = logging.getLogger(__name__)
@@ -42,9 +68,29 @@ async def async_setup_entry(
) -> None:
"""Platform setup using common elements."""
@callback
def _player_discovered(
player_coordinator: SqueezeBoxPlayerUpdateCoordinator,
) -> None:
_LOGGER.debug(
"Setting up binary sensor entities for player %s, model %s",
player_coordinator.player.name,
player_coordinator.player.model,
)
async_add_entities(
SqueezeboxBinarySensorEntity(player_coordinator, description)
for description in PLAYER_SENSORS
)
entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_PLAYER_DISCOVERED}{entry.entry_id}", _player_discovered
)
)
async_add_entities(
ServerStatusBinarySensor(entry.runtime_data.coordinator, description)
for description in SENSORS
for description in SERVER_SENSORS
)
@@ -55,3 +101,24 @@ class ServerStatusBinarySensor(LMSStatusEntity, BinarySensorEntity):
def is_on(self) -> bool:
"""LMS Status directly from coordinator data."""
return bool(self.coordinator.data[self.entity_description.key])
class SqueezeboxBinarySensorEntity(SqueezeboxEntity, BinarySensorEntity):
"""Representation of player based binary sensors."""
description: BinarySensorEntityDescription
def __init__(
self,
coordinator: SqueezeBoxPlayerUpdateCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the SqueezeBox sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{format_mac(self._player.player_id)}_{description.key}"
@property
def is_on(self) -> bool | None:
"""Return the state of the binary sensor."""
return getattr(self.coordinator.player, self.entity_description.key, None)
@@ -19,6 +19,9 @@ STATUS_SENSOR_INFO_TOTAL_GENRES = "info total genres"
STATUS_SENSOR_INFO_TOTAL_SONGS = "info total songs"
STATUS_SENSOR_PLAYER_COUNT = "player count"
STATUS_SENSOR_OTHER_PLAYER_COUNT = "other player count"
PLAYER_SENSOR_ALARM_UPCOMING = "alarm_upcoming"
PLAYER_SENSOR_ALARM_SNOOZE = "alarm_snooze"
PLAYER_SENSOR_ALARM_ACTIVE = "alarm_active"
STATUS_QUERY_LIBRARYNAME = "libraryname"
STATUS_QUERY_MAC = "mac"
STATUS_QUERY_UUID = "uuid"
@@ -41,6 +41,15 @@
},
"entity": {
"binary_sensor": {
"alarm_active": {
"name": "Alarm active"
},
"alarm_snooze": {
"name": "Alarm snoozed"
},
"alarm_upcoming": {
"name": "Alarm upcoming"
},
"needsrestart": {
"name": "Needs restart"
},
+3
View File
@@ -297,6 +297,9 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
mock_player.model_type = None
mock_player.firmware = None
mock_player.alarms_enabled = True
mock_player.alarm_upcoming = True
mock_player.alarm_snooze = False
mock_player.alarm_active = False
return mock_player
+111 -14
View File
@@ -1,30 +1,39 @@
"""Test squeezebox binary sensors."""
from copy import deepcopy
from unittest.mock import patch
from datetime import timedelta
from unittest.mock import MagicMock, patch
from homeassistant.const import Platform
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.squeezebox.const import PLAYER_UPDATE_INTERVAL
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from .conftest import FAKE_QUERY_RESPONSE
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_binary_sensor(
@pytest.fixture(autouse=True)
def squeezebox_binary_sensor_platform():
"""Only set up the binary_sensor platform for squeezebox tests."""
with patch(
"homeassistant.components.squeezebox.PLATFORMS", [Platform.BINARY_SENSOR]
):
yield
async def test_binary_server_sensor(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test binary sensor states and attributes."""
with (
patch(
"homeassistant.components.squeezebox.PLATFORMS",
[Platform.BINARY_SENSOR],
),
patch(
"homeassistant.components.squeezebox.Server.async_query",
return_value=deepcopy(FAKE_QUERY_RESPONSE),
),
with patch(
"homeassistant.components.squeezebox.Server.async_query",
return_value=deepcopy(FAKE_QUERY_RESPONSE),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
@@ -32,4 +41,92 @@ async def test_binary_sensor(
state = hass.states.get("binary_sensor.fakelib_needs_restart")
assert state is not None
assert state.state == "off"
assert state.state == STATE_OFF
@pytest.fixture
async def mock_player(
hass: HomeAssistant,
config_entry: MockConfigEntry,
lms: MagicMock,
) -> MagicMock:
"""Set up the squeezebox integration and return the mocked player."""
# Mock server status data for coordinator update
# called on update, return something != None to not raise
lms.async_prepared_status.return_value = {
"dummy": False,
}
with patch("homeassistant.components.squeezebox.Server", return_value=lms):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
# Return the player mock
return (await lms.async_get_players())[0]
async def test_player_alarm_sensors_device_class(
hass: HomeAssistant,
mock_player: MagicMock,
) -> None:
"""Test player alarm binary sensors have correct device class."""
# Test alarm upcoming sensor device class
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
assert upcoming_state is not None
assert upcoming_state.attributes.get("device_class") is None
# Test alarm active sensor device class
active_state = hass.states.get("binary_sensor.none_alarm_active")
assert active_state is not None
assert (
active_state.attributes.get("device_class") == BinarySensorDeviceClass.RUNNING
)
# Test alarm snooze sensor device class
snooze_state = hass.states.get("binary_sensor.none_alarm_snoozed")
assert snooze_state is not None
assert (
snooze_state.attributes.get("device_class") == BinarySensorDeviceClass.RUNNING
)
async def test_player_alarm_sensors_state(
hass: HomeAssistant,
mock_player: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test player alarm binary sensors with default states."""
player = mock_player
# Test alarm upcoming sensor
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
assert upcoming_state is not None
assert upcoming_state.state == STATE_ON
# Test alarm active sensor
active_state = hass.states.get("binary_sensor.none_alarm_active")
assert active_state is not None
assert active_state.state == STATE_OFF
# Test alarm snooze sensor
snooze_state = hass.states.get("binary_sensor.none_alarm_snoozed")
assert snooze_state is not None
assert snooze_state.state == STATE_OFF
# Toggle alarm states and verify sensors update
player.alarm_upcoming = False
player.alarm_active = True
player.alarm_snooze = True
freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
assert upcoming_state is not None
assert upcoming_state.state == STATE_OFF
active_state = hass.states.get("binary_sensor.none_alarm_active")
assert active_state is not None
assert active_state.state == STATE_ON
+1 -1
View File
@@ -25,7 +25,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
@pytest.fixture(autouse=True)
def squeezebox_alarm_platform():
"""Only set up the media_player platform for squeezebox tests."""
"""Only set up the switch platform for squeezebox tests."""
with patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.SWITCH]):
yield