mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
provide Squeezebox player sensor for next alarm timestamp (#155788)
This commit is contained in:
@@ -19,9 +19,10 @@ 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"
|
||||
PLAYER_SENSOR_ALARM_SNOOZE = "alarm_snooze"
|
||||
PLAYER_SENSOR_ALARM_UPCOMING = "alarm_upcoming"
|
||||
PLAYER_SENSOR_NEXT_ALARM = "alarm_next"
|
||||
STATUS_QUERY_LIBRARYNAME = "libraryname"
|
||||
STATUS_QUERY_MAC = "mac"
|
||||
STATUS_QUERY_UUID = "uuid"
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
@@ -13,11 +16,15 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import SqueezeboxConfigEntry
|
||||
from .const import (
|
||||
PLAYER_SENSOR_NEXT_ALARM,
|
||||
SIGNAL_PLAYER_DISCOVERED,
|
||||
STATUS_SENSOR_INFO_TOTAL_ALBUMS,
|
||||
STATUS_SENSOR_INFO_TOTAL_ARTISTS,
|
||||
STATUS_SENSOR_INFO_TOTAL_DURATION,
|
||||
@@ -27,12 +34,12 @@ from .const import (
|
||||
STATUS_SENSOR_OTHER_PLAYER_COUNT,
|
||||
STATUS_SENSOR_PLAYER_COUNT,
|
||||
)
|
||||
from .entity import LMSStatusEntity
|
||||
from .entity import LMSStatusEntity, SqueezeboxEntity, SqueezeBoxPlayerUpdateCoordinator
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SERVER_STATUS_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=STATUS_SENSOR_INFO_TOTAL_ALBUMS,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
@@ -71,6 +78,23 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PlayerSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes player sensor entity."""
|
||||
|
||||
value_fn: Callable[[SqueezeboxSensorEntity], datetime | None]
|
||||
|
||||
|
||||
PLAYER_SENSORS: tuple[PlayerSensorEntityDescription, ...] = (
|
||||
PlayerSensorEntityDescription(
|
||||
key=PLAYER_SENSOR_NEXT_ALARM,
|
||||
translation_key=PLAYER_SENSOR_NEXT_ALARM,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda sensor: sensor.coordinator.player.alarm_next,
|
||||
),
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -81,9 +105,30 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Platform setup using common elements."""
|
||||
|
||||
# Add player sensor entities when player discovered
|
||||
async def _player_discovered(
|
||||
player_coordinator: SqueezeBoxPlayerUpdateCoordinator,
|
||||
) -> None:
|
||||
_LOGGER.debug(
|
||||
"Setting up sensor entities for player %s, model %s",
|
||||
player_coordinator.player.name,
|
||||
player_coordinator.player.model,
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
SqueezeboxSensorEntity(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(
|
||||
ServerStatusSensor(entry.runtime_data.coordinator, description)
|
||||
for description in SENSORS
|
||||
for description in SERVER_STATUS_SENSORS
|
||||
)
|
||||
|
||||
|
||||
@@ -94,3 +139,24 @@ class ServerStatusSensor(LMSStatusEntity, SensorEntity):
|
||||
def native_value(self) -> StateType:
|
||||
"""LMS Status directly from coordinator data."""
|
||||
return cast(StateType, self.coordinator.data[self.entity_description.key])
|
||||
|
||||
|
||||
class SqueezeboxSensorEntity(SqueezeboxEntity, SensorEntity):
|
||||
"""Representation of player based sensors."""
|
||||
|
||||
entity_description: PlayerSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SqueezeBoxPlayerUpdateCoordinator,
|
||||
description: PlayerSensorEntityDescription,
|
||||
) -> 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 native_value(self) -> datetime | None:
|
||||
"""Sensor value directly from player coordinator."""
|
||||
return self.entity_description.value_fn(self)
|
||||
|
||||
@@ -82,6 +82,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"alarm_next": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"info_total_albums": {
|
||||
"name": "Total albums",
|
||||
"unit_of_measurement": "albums"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Setup the squeezebox tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -47,6 +49,14 @@ TEST_MAC = ["aa:bb:cc:dd:ee:ff", "de:ad:be:ef:de:ad", "ff:ee:dd:cc:bb:aa"]
|
||||
TEST_PLAYER_NAME = "Test Player"
|
||||
TEST_SERVER_NAME = "Test Server"
|
||||
TEST_ALARM_ID = "1"
|
||||
TEST_ALARM_NEXT_TIME = datetime(
|
||||
year=1985,
|
||||
month=10,
|
||||
day=26,
|
||||
hour=1,
|
||||
minute=21,
|
||||
tzinfo=ZoneInfo("UTC"),
|
||||
)
|
||||
FAKE_VALID_ITEM_ID = "1234"
|
||||
FAKE_INVALID_ITEM_ID = "4321"
|
||||
|
||||
@@ -297,9 +307,10 @@ 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
|
||||
mock_player.alarm_snooze = False
|
||||
mock_player.alarm_upcoming = True
|
||||
mock_player.alarm_next = TEST_ALARM_NEXT_TIME
|
||||
|
||||
return mock_player
|
||||
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
"""Test squeezebox 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.squeezebox.const import PLAYER_UPDATE_INTERVAL
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import FAKE_QUERY_RESPONSE
|
||||
from .conftest import FAKE_QUERY_RESPONSE, TEST_ALARM_NEXT_TIME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_sensor(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Test sensor states and attributes."""
|
||||
@pytest.fixture(autouse=True)
|
||||
def squeezebox_sensor_platform():
|
||||
"""Only set up the sensor platform for these tests."""
|
||||
with patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.SENSOR]):
|
||||
yield
|
||||
|
||||
|
||||
async def test_server_sensor(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that server sensor reports correct player count."""
|
||||
|
||||
# Setup component
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.squeezebox.PLATFORMS",
|
||||
[Platform.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,3 +40,36 @@ async def test_sensor(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non
|
||||
|
||||
assert state is not None
|
||||
assert state.state == "10"
|
||||
|
||||
|
||||
async def test_player_sensor_next_alarm(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
lms: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test player sensor for time of next alarm."""
|
||||
|
||||
# Setup component
|
||||
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)
|
||||
player = (await lms.async_get_players())[0]
|
||||
|
||||
# test alarm time is set from player
|
||||
state = hass.states.get("sensor.none_next_alarm")
|
||||
assert state is not None
|
||||
assert state.state == TEST_ALARM_NEXT_TIME.isoformat()
|
||||
|
||||
# simulate no upcoming alarm
|
||||
player.alarm_next = None
|
||||
freezer.tick(timedelta(seconds=PLAYER_UPDATE_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.none_next_alarm")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
Reference in New Issue
Block a user