mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Add enum sensor for Sonos Power Source (#147449)
* feat: add power source sensor * fix: translations * fix:cleanup * fix: simpify * fix: improve coverage * fix: improve coverage * fix: add missing test * fix: call it charging_base * fix: disable entity by default * update snapshots * Update homeassistant/components/sonos/strings.json Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * fix: update test --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
1286b5d9d8
commit
345ec97dd5
@ -24,6 +24,20 @@ from .speaker import SonosSpeaker
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SONOS_POWER_SOURCE_BATTERY = "BATTERY"
|
||||||
|
SONOS_POWER_SOURCE_CHARGING_RING = "SONOS_CHARGING_RING"
|
||||||
|
SONOS_POWER_SOURCE_USB = "USB_POWER"
|
||||||
|
|
||||||
|
HA_POWER_SOURCE_BATTERY = "battery"
|
||||||
|
HA_POWER_SOURCE_CHARGING_BASE = "charging_base"
|
||||||
|
HA_POWER_SOURCE_USB = "usb"
|
||||||
|
|
||||||
|
power_source_map = {
|
||||||
|
SONOS_POWER_SOURCE_BATTERY: HA_POWER_SOURCE_BATTERY,
|
||||||
|
SONOS_POWER_SOURCE_CHARGING_RING: HA_POWER_SOURCE_CHARGING_BASE,
|
||||||
|
SONOS_POWER_SOURCE_USB: HA_POWER_SOURCE_USB,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -42,9 +56,15 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_create_battery_sensor(speaker: SonosSpeaker) -> None:
|
def _async_create_battery_sensor(speaker: SonosSpeaker) -> None:
|
||||||
_LOGGER.debug("Creating battery level sensor on %s", speaker.zone_name)
|
_LOGGER.debug(
|
||||||
entity = SonosBatteryEntity(speaker, config_entry)
|
"Creating battery level and power source sensor on %s", speaker.zone_name
|
||||||
async_add_entities([entity])
|
)
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
SonosBatteryEntity(speaker, config_entry),
|
||||||
|
SonosPowerSourceEntity(speaker, config_entry),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_create_favorites_sensor(favorites: SonosFavorites) -> None:
|
def _async_create_favorites_sensor(favorites: SonosFavorites) -> None:
|
||||||
@ -101,6 +121,48 @@ class SonosBatteryEntity(SonosEntity, SensorEntity):
|
|||||||
return self.speaker.available and self.speaker.power_source is not None
|
return self.speaker.available and self.speaker.power_source is not None
|
||||||
|
|
||||||
|
|
||||||
|
class SonosPowerSourceEntity(SonosEntity, SensorEntity):
|
||||||
|
"""Representation of a Sonos Power Source entity."""
|
||||||
|
|
||||||
|
_attr_device_class = SensorDeviceClass.ENUM
|
||||||
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_attr_entity_registry_enabled_default = False
|
||||||
|
_attr_options = [
|
||||||
|
HA_POWER_SOURCE_BATTERY,
|
||||||
|
HA_POWER_SOURCE_CHARGING_BASE,
|
||||||
|
HA_POWER_SOURCE_USB,
|
||||||
|
]
|
||||||
|
_attr_translation_key = "power_source"
|
||||||
|
|
||||||
|
def __init__(self, speaker: SonosSpeaker, config_entry: SonosConfigEntry) -> None:
|
||||||
|
"""Initialize the power source sensor."""
|
||||||
|
super().__init__(speaker, config_entry)
|
||||||
|
self._attr_unique_id = f"{self.soco.uid}-power_source"
|
||||||
|
|
||||||
|
async def _async_fallback_poll(self) -> None:
|
||||||
|
"""Poll the device for the current state."""
|
||||||
|
await self.speaker.async_poll_battery()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> str | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
if not (power_source := self.speaker.power_source):
|
||||||
|
return None
|
||||||
|
if not (value := power_source_map.get(power_source)):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unknown power source '%s' for speaker %s",
|
||||||
|
power_source,
|
||||||
|
self.speaker.zone_name,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return whether this entity is available."""
|
||||||
|
return self.speaker.available and self.speaker.power_source is not None
|
||||||
|
|
||||||
|
|
||||||
class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
|
class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
|
||||||
"""Representation of a Sonos audio import format sensor entity."""
|
"""Representation of a Sonos audio import format sensor entity."""
|
||||||
|
|
||||||
|
@ -53,6 +53,14 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"audio_input_format": {
|
"audio_input_format": {
|
||||||
"name": "Audio input format"
|
"name": "Audio input format"
|
||||||
|
},
|
||||||
|
"power_source": {
|
||||||
|
"name": "Power source",
|
||||||
|
"state": {
|
||||||
|
"battery": "Battery",
|
||||||
|
"charging_base": "Charging base",
|
||||||
|
"usb": "USB"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@ -1,20 +1,35 @@
|
|||||||
"""Tests for the Sonos battery sensor platform."""
|
"""Tests for the Sonos battery sensor platform."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import PropertyMock, patch
|
from unittest.mock import PropertyMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from soco.exceptions import NotSupportedException
|
from soco.exceptions import NotSupportedException
|
||||||
|
|
||||||
from homeassistant.components.sensor import SCAN_INTERVAL
|
from homeassistant.components.sensor import SCAN_INTERVAL
|
||||||
|
from homeassistant.components.sonos import DOMAIN
|
||||||
from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE
|
from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE
|
||||||
|
from homeassistant.components.sonos.sensor import (
|
||||||
|
HA_POWER_SOURCE_BATTERY,
|
||||||
|
HA_POWER_SOURCE_CHARGING_BASE,
|
||||||
|
HA_POWER_SOURCE_USB,
|
||||||
|
SensorDeviceClass,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import (
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er, translation
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .conftest import SonosMockEvent
|
from .conftest import MockSoCo, SonosMockEvent
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
@ -42,8 +57,10 @@ async def test_entity_registry_supported(
|
|||||||
assert "media_player.zone_a" in entity_registry.entities
|
assert "media_player.zone_a" in entity_registry.entities
|
||||||
assert "sensor.zone_a_battery" in entity_registry.entities
|
assert "sensor.zone_a_battery" in entity_registry.entities
|
||||||
assert "binary_sensor.zone_a_charging" in entity_registry.entities
|
assert "binary_sensor.zone_a_charging" in entity_registry.entities
|
||||||
|
assert "sensor.zone_a_power_source" in entity_registry.entities
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
async def test_battery_attributes(
|
async def test_battery_attributes(
|
||||||
hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -60,6 +77,71 @@ async def test_battery_attributes(
|
|||||||
power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING"
|
power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
power_source = entity_registry.entities["sensor.zone_a_power_source"]
|
||||||
|
power_source_state = hass.states.get(power_source.entity_id)
|
||||||
|
assert power_source_state.state == HA_POWER_SOURCE_CHARGING_BASE
|
||||||
|
assert power_source_state.attributes.get("device_class") == SensorDeviceClass.ENUM
|
||||||
|
assert power_source_state.attributes.get("options") == [
|
||||||
|
HA_POWER_SOURCE_BATTERY,
|
||||||
|
HA_POWER_SOURCE_CHARGING_BASE,
|
||||||
|
HA_POWER_SOURCE_USB,
|
||||||
|
]
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass,
|
||||||
|
power_source_state.state,
|
||||||
|
Platform.SENSOR,
|
||||||
|
DOMAIN,
|
||||||
|
power_source.translation_key,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert result == "Charging base"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_power_source_unknown_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
async_setup_sonos: Callable[[], Coroutine[Any, Any, None]],
|
||||||
|
soco: MockSoCo,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test bad value for power source."""
|
||||||
|
soco.get_battery_info.return_value = {
|
||||||
|
"Level": 100,
|
||||||
|
"PowerSource": "BAD_POWER_SOURCE",
|
||||||
|
}
|
||||||
|
|
||||||
|
with caplog.at_level("WARNING"):
|
||||||
|
await async_setup_sonos()
|
||||||
|
assert "Unknown power source" in caplog.text
|
||||||
|
assert "BAD_POWER_SOURCE" in caplog.text
|
||||||
|
assert "Zone A" in caplog.text
|
||||||
|
|
||||||
|
power_source = entity_registry.entities["sensor.zone_a_power_source"]
|
||||||
|
power_source_state = hass.states.get(power_source.entity_id)
|
||||||
|
assert power_source_state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_power_source_none(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
async_setup_sonos: Callable[[], Coroutine[Any, Any, None]],
|
||||||
|
soco: MockSoCo,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test none value for power source."""
|
||||||
|
soco.get_battery_info.return_value = {
|
||||||
|
"Level": 100,
|
||||||
|
"PowerSource": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
await async_setup_sonos()
|
||||||
|
|
||||||
|
power_source = entity_registry.entities["sensor.zone_a_power_source"]
|
||||||
|
power_source_state = hass.states.get(power_source.entity_id)
|
||||||
|
assert power_source_state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
async def test_battery_on_s1(
|
async def test_battery_on_s1(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user