Skip to content

Commit 4cd4603

Browse files
wollewCopilotMartinHjelmare
authored
Add Squeezebox binary sensors for player alarm status (home-assistant#154491)
Co-authored-by: Copilot <[email protected]> Co-authored-by: Martin Hjelmare <[email protected]>
1 parent afea571 commit 4cd4603

File tree

6 files changed

+200
-21
lines changed

6 files changed

+200
-21
lines changed

homeassistant/components/squeezebox/binary_sensor.py

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,26 @@
1010
BinarySensorEntityDescription,
1111
)
1212
from homeassistant.const import EntityCategory
13-
from homeassistant.core import HomeAssistant
13+
from homeassistant.core import HomeAssistant, callback
14+
from homeassistant.helpers.device_registry import format_mac
15+
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1416
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
1517

16-
from . import SqueezeboxConfigEntry
17-
from .const import STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_RESCAN
18-
from .entity import LMSStatusEntity
18+
from . import SqueezeboxConfigEntry, SqueezeBoxPlayerUpdateCoordinator
19+
from .const import (
20+
PLAYER_SENSOR_ALARM_ACTIVE,
21+
PLAYER_SENSOR_ALARM_SNOOZE,
22+
PLAYER_SENSOR_ALARM_UPCOMING,
23+
SIGNAL_PLAYER_DISCOVERED,
24+
STATUS_SENSOR_NEEDSRESTART,
25+
STATUS_SENSOR_RESCAN,
26+
)
27+
from .entity import LMSStatusEntity, SqueezeboxEntity
1928

2029
# Coordinator is used to centralize the data updates
2130
PARALLEL_UPDATES = 0
2231

23-
SENSORS: tuple[BinarySensorEntityDescription, ...] = (
32+
SERVER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
2433
BinarySensorEntityDescription(
2534
key=STATUS_SENSOR_RESCAN,
2635
device_class=BinarySensorDeviceClass.RUNNING,
@@ -32,6 +41,23 @@
3241
),
3342
)
3443

44+
PLAYER_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
45+
BinarySensorEntityDescription(
46+
key=PLAYER_SENSOR_ALARM_UPCOMING,
47+
translation_key=PLAYER_SENSOR_ALARM_UPCOMING,
48+
),
49+
BinarySensorEntityDescription(
50+
key=PLAYER_SENSOR_ALARM_ACTIVE,
51+
translation_key=PLAYER_SENSOR_ALARM_ACTIVE,
52+
device_class=BinarySensorDeviceClass.RUNNING,
53+
),
54+
BinarySensorEntityDescription(
55+
key=PLAYER_SENSOR_ALARM_SNOOZE,
56+
translation_key=PLAYER_SENSOR_ALARM_SNOOZE,
57+
device_class=BinarySensorDeviceClass.RUNNING,
58+
),
59+
)
60+
3561
_LOGGER = logging.getLogger(__name__)
3662

3763

@@ -42,9 +68,29 @@ async def async_setup_entry(
4268
) -> None:
4369
"""Platform setup using common elements."""
4470

71+
@callback
72+
def _player_discovered(
73+
player_coordinator: SqueezeBoxPlayerUpdateCoordinator,
74+
) -> None:
75+
_LOGGER.debug(
76+
"Setting up binary sensor entities for player %s, model %s",
77+
player_coordinator.player.name,
78+
player_coordinator.player.model,
79+
)
80+
81+
async_add_entities(
82+
SqueezeboxBinarySensorEntity(player_coordinator, description)
83+
for description in PLAYER_SENSORS
84+
)
85+
86+
entry.async_on_unload(
87+
async_dispatcher_connect(
88+
hass, f"{SIGNAL_PLAYER_DISCOVERED}{entry.entry_id}", _player_discovered
89+
)
90+
)
4591
async_add_entities(
4692
ServerStatusBinarySensor(entry.runtime_data.coordinator, description)
47-
for description in SENSORS
93+
for description in SERVER_SENSORS
4894
)
4995

5096

@@ -55,3 +101,24 @@ class ServerStatusBinarySensor(LMSStatusEntity, BinarySensorEntity):
55101
def is_on(self) -> bool:
56102
"""LMS Status directly from coordinator data."""
57103
return bool(self.coordinator.data[self.entity_description.key])
104+
105+
106+
class SqueezeboxBinarySensorEntity(SqueezeboxEntity, BinarySensorEntity):
107+
"""Representation of player based binary sensors."""
108+
109+
description: BinarySensorEntityDescription
110+
111+
def __init__(
112+
self,
113+
coordinator: SqueezeBoxPlayerUpdateCoordinator,
114+
description: BinarySensorEntityDescription,
115+
) -> None:
116+
"""Initialize the SqueezeBox sensor."""
117+
super().__init__(coordinator)
118+
self.entity_description = description
119+
self._attr_unique_id = f"{format_mac(self._player.player_id)}_{description.key}"
120+
121+
@property
122+
def is_on(self) -> bool | None:
123+
"""Return the state of the binary sensor."""
124+
return getattr(self.coordinator.player, self.entity_description.key, None)

homeassistant/components/squeezebox/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
STATUS_SENSOR_INFO_TOTAL_SONGS = "info total songs"
2020
STATUS_SENSOR_PLAYER_COUNT = "player count"
2121
STATUS_SENSOR_OTHER_PLAYER_COUNT = "other player count"
22+
PLAYER_SENSOR_ALARM_UPCOMING = "alarm_upcoming"
23+
PLAYER_SENSOR_ALARM_SNOOZE = "alarm_snooze"
24+
PLAYER_SENSOR_ALARM_ACTIVE = "alarm_active"
2225
STATUS_QUERY_LIBRARYNAME = "libraryname"
2326
STATUS_QUERY_MAC = "mac"
2427
STATUS_QUERY_UUID = "uuid"

homeassistant/components/squeezebox/strings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@
4141
},
4242
"entity": {
4343
"binary_sensor": {
44+
"alarm_active": {
45+
"name": "Alarm active"
46+
},
47+
"alarm_snooze": {
48+
"name": "Alarm snoozed"
49+
},
50+
"alarm_upcoming": {
51+
"name": "Alarm upcoming"
52+
},
4453
"needsrestart": {
4554
"name": "Needs restart"
4655
},

tests/components/squeezebox/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
297297
mock_player.model_type = None
298298
mock_player.firmware = None
299299
mock_player.alarms_enabled = True
300+
mock_player.alarm_upcoming = True
301+
mock_player.alarm_snooze = False
302+
mock_player.alarm_active = False
300303

301304
return mock_player
302305

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,132 @@
11
"""Test squeezebox binary sensors."""
22

33
from copy import deepcopy
4-
from unittest.mock import patch
4+
from datetime import timedelta
5+
from unittest.mock import MagicMock, patch
56

6-
from homeassistant.const import Platform
7+
from freezegun.api import FrozenDateTimeFactory
8+
import pytest
9+
10+
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
11+
from homeassistant.components.squeezebox.const import PLAYER_UPDATE_INTERVAL
12+
from homeassistant.const import STATE_OFF, STATE_ON, Platform
713
from homeassistant.core import HomeAssistant
814

915
from .conftest import FAKE_QUERY_RESPONSE
1016

11-
from tests.common import MockConfigEntry
17+
from tests.common import MockConfigEntry, async_fire_time_changed
18+
19+
20+
@pytest.fixture(autouse=True)
21+
def squeezebox_binary_sensor_platform():
22+
"""Only set up the binary_sensor platform for squeezebox tests."""
23+
with patch(
24+
"homeassistant.components.squeezebox.PLATFORMS", [Platform.BINARY_SENSOR]
25+
):
26+
yield
1227

1328

14-
async def test_binary_sensor(
29+
async def test_binary_server_sensor(
1530
hass: HomeAssistant,
1631
config_entry: MockConfigEntry,
1732
) -> None:
1833
"""Test binary sensor states and attributes."""
19-
with (
20-
patch(
21-
"homeassistant.components.squeezebox.PLATFORMS",
22-
[Platform.BINARY_SENSOR],
23-
),
24-
patch(
25-
"homeassistant.components.squeezebox.Server.async_query",
26-
return_value=deepcopy(FAKE_QUERY_RESPONSE),
27-
),
34+
with patch(
35+
"homeassistant.components.squeezebox.Server.async_query",
36+
return_value=deepcopy(FAKE_QUERY_RESPONSE),
2837
):
2938
await hass.config_entries.async_setup(config_entry.entry_id)
3039
await hass.async_block_till_done(wait_background_tasks=True)
3140

3241
state = hass.states.get("binary_sensor.fakelib_needs_restart")
3342

3443
assert state is not None
35-
assert state.state == "off"
44+
assert state.state == STATE_OFF
45+
46+
47+
@pytest.fixture
48+
async def mock_player(
49+
hass: HomeAssistant,
50+
config_entry: MockConfigEntry,
51+
lms: MagicMock,
52+
) -> MagicMock:
53+
"""Set up the squeezebox integration and return the mocked player."""
54+
55+
# Mock server status data for coordinator update
56+
# called on update, return something != None to not raise
57+
lms.async_prepared_status.return_value = {
58+
"dummy": False,
59+
}
60+
with patch("homeassistant.components.squeezebox.Server", return_value=lms):
61+
await hass.config_entries.async_setup(config_entry.entry_id)
62+
await hass.async_block_till_done(wait_background_tasks=True)
63+
64+
# Return the player mock
65+
return (await lms.async_get_players())[0]
66+
67+
68+
async def test_player_alarm_sensors_device_class(
69+
hass: HomeAssistant,
70+
mock_player: MagicMock,
71+
) -> None:
72+
"""Test player alarm binary sensors have correct device class."""
73+
74+
# Test alarm upcoming sensor device class
75+
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
76+
assert upcoming_state is not None
77+
assert upcoming_state.attributes.get("device_class") is None
78+
79+
# Test alarm active sensor device class
80+
active_state = hass.states.get("binary_sensor.none_alarm_active")
81+
assert active_state is not None
82+
assert (
83+
active_state.attributes.get("device_class") == BinarySensorDeviceClass.RUNNING
84+
)
85+
86+
# Test alarm snooze sensor device class
87+
snooze_state = hass.states.get("binary_sensor.none_alarm_snoozed")
88+
assert snooze_state is not None
89+
assert (
90+
snooze_state.attributes.get("device_class") == BinarySensorDeviceClass.RUNNING
91+
)
92+
93+
94+
async def test_player_alarm_sensors_state(
95+
hass: HomeAssistant,
96+
mock_player: MagicMock,
97+
freezer: FrozenDateTimeFactory,
98+
) -> None:
99+
"""Test player alarm binary sensors with default states."""
100+
101+
player = mock_player
102+
103+
# Test alarm upcoming sensor
104+
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
105+
assert upcoming_state is not None
106+
assert upcoming_state.state == STATE_ON
107+
108+
# Test alarm active sensor
109+
active_state = hass.states.get("binary_sensor.none_alarm_active")
110+
assert active_state is not None
111+
assert active_state.state == STATE_OFF
112+
113+
# Test alarm snooze sensor
114+
snooze_state = hass.states.get("binary_sensor.none_alarm_snoozed")
115+
assert snooze_state is not None
116+
assert snooze_state.state == STATE_OFF
117+
118+
# Toggle alarm states and verify sensors update
119+
player.alarm_upcoming = False
120+
player.alarm_active = True
121+
player.alarm_snooze = True
122+
freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
123+
async_fire_time_changed(hass)
124+
await hass.async_block_till_done()
125+
126+
upcoming_state = hass.states.get("binary_sensor.none_alarm_upcoming")
127+
assert upcoming_state is not None
128+
assert upcoming_state.state == STATE_OFF
129+
130+
active_state = hass.states.get("binary_sensor.none_alarm_active")
131+
assert active_state is not None
132+
assert active_state.state == STATE_ON

tests/components/squeezebox/test_switch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
@pytest.fixture(autouse=True)
2727
def squeezebox_alarm_platform():
28-
"""Only set up the media_player platform for squeezebox tests."""
28+
"""Only set up the switch platform for squeezebox tests."""
2929
with patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.SWITCH]):
3030
yield
3131

0 commit comments

Comments
 (0)