From a16ef5b7ffabe4607a148f1d7384d6abd2c4ab47 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Tue, 10 Sep 2024 16:17:26 +0100 Subject: [PATCH] Add squeezebox service sensors (#125349) * Add server sensors * Fix Platforms order * Fix spelling * Fix translations * Add sensor test * Case changes * refactor to use native_value attr override * Fix typing * Fix cast to type * add cast * use update platform for LMS versions * Fix translation * remove update entity * remove possible update entites * Fix and clarify * update to icon trans remove update plaform entitiy supporting items * add UOM to sensors * correct criptic prettier fail * reword other players * Apply suggestions from code review --------- Co-authored-by: Joost Lekkerkerker --- .../components/squeezebox/__init__.py | 6 +- .../components/squeezebox/coordinator.py | 13 +++ homeassistant/components/squeezebox/entity.py | 2 +- .../components/squeezebox/icons.json | 22 +++++ homeassistant/components/squeezebox/sensor.py | 98 +++++++++++++++++++ .../components/squeezebox/strings.json | 26 +++++ tests/components/squeezebox/__init__.py | 16 +++ .../squeezebox/test_binary_sensor.py | 3 +- tests/components/squeezebox/test_sensor.py | 29 ++++++ 9 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/squeezebox/sensor.py create mode 100644 tests/components/squeezebox/test_sensor.py diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index be8c92b18df..c0a5b906474 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -40,7 +40,11 @@ from .coordinator import LMSStatusDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.MEDIA_PLAYER, + Platform.SENSOR, +] @dataclass diff --git a/homeassistant/components/squeezebox/coordinator.py b/homeassistant/components/squeezebox/coordinator.py index 71c55452004..0d958399bcb 100644 --- a/homeassistant/components/squeezebox/coordinator.py +++ b/homeassistant/components/squeezebox/coordinator.py @@ -3,15 +3,18 @@ from asyncio import timeout from datetime import timedelta import logging +import re from pysqueezebox import Server from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import ( SENSOR_UPDATE_INTERVAL, STATUS_API_TIMEOUT, + STATUS_SENSOR_LASTSCAN, STATUS_SENSOR_NEEDSRESTART, STATUS_SENSOR_RESCAN, ) @@ -32,6 +35,7 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator): always_update=False, ) self.lms = lms + self.newversion_regex = re.compile("<.*$") async def _async_update_data(self) -> dict: """Fetch data fromn LMS status call. @@ -50,10 +54,19 @@ class LMSStatusDataUpdateCoordinator(DataUpdateCoordinator): def _prepare_status_data(self, data: dict) -> dict: """Sensors that need the data changing for HA presentation.""" + # Binary sensors # rescan bool are we rescanning alter poll not present if false data[STATUS_SENSOR_RESCAN] = STATUS_SENSOR_RESCAN in data # needsrestart bool pending lms plugin updates not present if false data[STATUS_SENSOR_NEEDSRESTART] = STATUS_SENSOR_NEEDSRESTART in data + # Sensors that need special handling + # 'lastscan': '1718431678', epoc -> ISO 8601 not always present + data[STATUS_SENSOR_LASTSCAN] = ( + dt_util.utc_from_timestamp(int(data[STATUS_SENSOR_LASTSCAN])) + if STATUS_SENSOR_LASTSCAN in data + else None + ) + _LOGGER.debug("Processed serverstatus %s=%s", self.lms.name, data) return data diff --git a/homeassistant/components/squeezebox/entity.py b/homeassistant/components/squeezebox/entity.py index 8ac80265369..027ca68edc6 100644 --- a/homeassistant/components/squeezebox/entity.py +++ b/homeassistant/components/squeezebox/entity.py @@ -21,7 +21,7 @@ class LMSStatusEntity(CoordinatorEntity[LMSStatusDataUpdateCoordinator]): """Initialize status sensor entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_translation_key = description.key + self._attr_translation_key = description.key.replace(" ", "_") self._attr_unique_id = ( f"{coordinator.data[STATUS_QUERY_UUID]}_{description.key}" ) diff --git a/homeassistant/components/squeezebox/icons.json b/homeassistant/components/squeezebox/icons.json index b11311e1292..e86016329f5 100644 --- a/homeassistant/components/squeezebox/icons.json +++ b/homeassistant/components/squeezebox/icons.json @@ -1,4 +1,26 @@ { + "entity": { + "sensor": { + "info_total_albums": { + "default": "mdi:album" + }, + "info_total_artists": { + "default": "mdi:account-music" + }, + "info_total_genres": { + "default": "mdi:drama-masks" + }, + "info_total_songs": { + "default": "mdi:file-music" + }, + "player_count": { + "default": "mdi:folder-play" + }, + "other_player_count": { + "default": "mdi:folder-play-outline" + } + } + }, "services": { "call_method": { "service": "mdi:console" diff --git a/homeassistant/components/squeezebox/sensor.py b/homeassistant/components/squeezebox/sensor.py new file mode 100644 index 00000000000..ff9f86ccf1f --- /dev/null +++ b/homeassistant/components/squeezebox/sensor.py @@ -0,0 +1,98 @@ +"""Platform for sensor integration for squeezebox.""" + +from __future__ import annotations + +import logging +from typing import cast + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import SqueezeboxConfigEntry +from .const import ( + STATUS_SENSOR_INFO_TOTAL_ALBUMS, + STATUS_SENSOR_INFO_TOTAL_ARTISTS, + STATUS_SENSOR_INFO_TOTAL_DURATION, + STATUS_SENSOR_INFO_TOTAL_GENRES, + STATUS_SENSOR_INFO_TOTAL_SONGS, + STATUS_SENSOR_LASTSCAN, + STATUS_SENSOR_OTHER_PLAYER_COUNT, + STATUS_SENSOR_PLAYER_COUNT, +) +from .entity import LMSStatusEntity + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=STATUS_SENSOR_INFO_TOTAL_ALBUMS, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="albums", + ), + SensorEntityDescription( + key=STATUS_SENSOR_INFO_TOTAL_ARTISTS, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="artists", + ), + SensorEntityDescription( + key=STATUS_SENSOR_INFO_TOTAL_DURATION, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=UnitOfTime.SECONDS, + ), + SensorEntityDescription( + key=STATUS_SENSOR_INFO_TOTAL_GENRES, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="genres", + ), + SensorEntityDescription( + key=STATUS_SENSOR_INFO_TOTAL_SONGS, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="songs", + ), + SensorEntityDescription( + key=STATUS_SENSOR_LASTSCAN, + device_class=SensorDeviceClass.TIMESTAMP, + ), + SensorEntityDescription( + key=STATUS_SENSOR_PLAYER_COUNT, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement="players", + ), + SensorEntityDescription( + key=STATUS_SENSOR_OTHER_PLAYER_COUNT, + state_class=SensorStateClass.TOTAL, + entity_registry_visible_default=False, + native_unit_of_measurement="players", + ), +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SqueezeboxConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Platform setup using common elements.""" + + async_add_entities( + ServerStatusSensor(entry.runtime_data.coordinator, description) + for description in SENSORS + ) + + +class ServerStatusSensor(LMSStatusEntity, SensorEntity): + """LMS Status based sensor from LMS via cooridnatior.""" + + @property + def native_value(self) -> StateType: + """LMS Status directly from coordinator data.""" + return cast(StateType, self.coordinator.data[self.entity_description.key]) diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index 89302951146..1a120ee0567 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -84,6 +84,32 @@ "needsrestart": { "name": "Needs restart" } + }, + "sensor": { + "lastscan": { + "name": "Last scan" + }, + "info_total_albums": { + "name": "Total albums" + }, + "info_total_artists": { + "name": "Total artists" + }, + "info_total_duration": { + "name": "Total duration" + }, + "info_total_genres": { + "name": "Total genres" + }, + "info_total_songs": { + "name": "Total songs" + }, + "player_count": { + "name": "Player count" + }, + "other_player_count": { + "name": "Player count off service" + } } } } diff --git a/tests/components/squeezebox/__init__.py b/tests/components/squeezebox/__init__.py index d5faabba32e..3b7a57db459 100644 --- a/tests/components/squeezebox/__init__.py +++ b/tests/components/squeezebox/__init__.py @@ -6,6 +6,14 @@ from homeassistant.components.squeezebox.const import ( STATUS_QUERY_MAC, STATUS_QUERY_UUID, STATUS_QUERY_VERSION, + STATUS_SENSOR_INFO_TOTAL_ALBUMS, + STATUS_SENSOR_INFO_TOTAL_ARTISTS, + STATUS_SENSOR_INFO_TOTAL_DURATION, + STATUS_SENSOR_INFO_TOTAL_GENRES, + STATUS_SENSOR_INFO_TOTAL_SONGS, + STATUS_SENSOR_LASTSCAN, + STATUS_SENSOR_OTHER_PLAYER_COUNT, + STATUS_SENSOR_PLAYER_COUNT, STATUS_SENSOR_RESCAN, ) from homeassistant.const import CONF_HOST, CONF_PORT @@ -25,7 +33,15 @@ FAKE_QUERY_RESPONSE = { STATUS_QUERY_MAC: FAKE_MAC, STATUS_QUERY_VERSION: FAKE_VERSION, STATUS_SENSOR_RESCAN: 1, + STATUS_SENSOR_LASTSCAN: 0, STATUS_QUERY_LIBRARYNAME: "FakeLib", + STATUS_SENSOR_INFO_TOTAL_ALBUMS: 4, + STATUS_SENSOR_INFO_TOTAL_ARTISTS: 2, + STATUS_SENSOR_INFO_TOTAL_DURATION: 500, + STATUS_SENSOR_INFO_TOTAL_GENRES: 1, + STATUS_SENSOR_INFO_TOTAL_SONGS: 42, + STATUS_SENSOR_PLAYER_COUNT: 10, + STATUS_SENSOR_OTHER_PLAYER_COUNT: 0, "players_loop": [ { "isplaying": 0, diff --git a/tests/components/squeezebox/test_binary_sensor.py b/tests/components/squeezebox/test_binary_sensor.py index a2de0cbf95e..450d16a709c 100644 --- a/tests/components/squeezebox/test_binary_sensor.py +++ b/tests/components/squeezebox/test_binary_sensor.py @@ -1,5 +1,6 @@ """Test squeezebox binary sensors.""" +import copy from unittest.mock import patch from homeassistant.const import Platform @@ -23,7 +24,7 @@ async def test_binary_sensor( ), patch( "homeassistant.components.squeezebox.Server.async_query", - return_value=FAKE_QUERY_RESPONSE, + return_value=copy.deepcopy(FAKE_QUERY_RESPONSE), ), ): await setup_mocked_integration(hass) diff --git a/tests/components/squeezebox/test_sensor.py b/tests/components/squeezebox/test_sensor.py new file mode 100644 index 00000000000..b9e9802568c --- /dev/null +++ b/tests/components/squeezebox/test_sensor.py @@ -0,0 +1,29 @@ +"""Test squeezebox sensors.""" + +from unittest.mock import patch + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import FAKE_QUERY_RESPONSE, setup_mocked_integration + + +async def test_sensor(hass: HomeAssistant) -> None: + """Test binary sensor states and attributes.""" + + # Setup component + with ( + patch( + "homeassistant.components.squeezebox.PLATFORMS", + [Platform.SENSOR], + ), + patch( + "homeassistant.components.squeezebox.Server.async_query", + return_value=FAKE_QUERY_RESPONSE, + ), + ): + await setup_mocked_integration(hass) + state = hass.states.get("sensor.fakelib_player_count") + + assert state is not None + assert state.state == "10"