Add support for Sonos microphone binary_sensor (#63097)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
ec75b0caf0
commit
8a8ffa1c08
5 changed files with 93 additions and 31 deletions
|
@ -14,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import SONOS_CREATE_BATTERY
|
||||
from .const import SONOS_CREATE_BATTERY, SONOS_CREATE_MIC_SENSOR
|
||||
from .entity import SonosEntity
|
||||
from .speaker import SonosSpeaker
|
||||
|
||||
|
@ -30,13 +30,25 @@ async def async_setup_entry(
|
|||
) -> None:
|
||||
"""Set up Sonos from a config entry."""
|
||||
|
||||
async def _async_create_entity(speaker: SonosSpeaker) -> None:
|
||||
async def _async_create_battery_entity(speaker: SonosSpeaker) -> None:
|
||||
_LOGGER.debug("Creating battery binary_sensor on %s", speaker.zone_name)
|
||||
entity = SonosPowerEntity(speaker)
|
||||
async_add_entities([entity])
|
||||
|
||||
async def _async_create_mic_entity(speaker: SonosSpeaker) -> None:
|
||||
_LOGGER.debug("Creating microphone binary_sensor on %s", speaker.zone_name)
|
||||
async_add_entities([SonosMicrophoneSensorEntity(speaker)])
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SONOS_CREATE_BATTERY, _async_create_entity)
|
||||
async_dispatcher_connect(
|
||||
hass, SONOS_CREATE_BATTERY, _async_create_battery_entity
|
||||
)
|
||||
)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, SONOS_CREATE_MIC_SENSOR, _async_create_mic_entity
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@ -72,3 +84,24 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity):
|
|||
def available(self) -> bool:
|
||||
"""Return whether this device is available."""
|
||||
return self.speaker.available and (self.speaker.charging is not None)
|
||||
|
||||
|
||||
class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity):
|
||||
"""Representation of a Sonos microphone sensor entity."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_icon = "mdi:microphone"
|
||||
|
||||
def __init__(self, speaker: SonosSpeaker) -> None:
|
||||
"""Initialize the microphone binary sensor entity."""
|
||||
super().__init__(speaker)
|
||||
self._attr_unique_id = f"{self.soco.uid}-microphone"
|
||||
self._attr_name = f"{self.speaker.zone_name} Microphone"
|
||||
|
||||
async def _async_poll(self) -> None:
|
||||
"""Stub for abstract class implementation. Not a pollable attribute."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the binary sensor."""
|
||||
return self.speaker.mic_enabled
|
||||
|
|
|
@ -146,6 +146,7 @@ SONOS_CHECK_ACTIVITY = "sonos_check_activity"
|
|||
SONOS_CREATE_ALARM = "sonos_create_alarm"
|
||||
SONOS_CREATE_AUDIO_FORMAT_SENSOR = "sonos_create_audio_format_sensor"
|
||||
SONOS_CREATE_BATTERY = "sonos_create_battery"
|
||||
SONOS_CREATE_MIC_SENSOR = "sonos_create_mic_sensor"
|
||||
SONOS_CREATE_SWITCHES = "sonos_create_switches"
|
||||
SONOS_CREATE_LEVELS = "sonos_create_levels"
|
||||
SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player"
|
||||
|
|
|
@ -46,6 +46,7 @@ from .const import (
|
|||
SONOS_CREATE_BATTERY,
|
||||
SONOS_CREATE_LEVELS,
|
||||
SONOS_CREATE_MEDIA_PLAYER,
|
||||
SONOS_CREATE_MIC_SENSOR,
|
||||
SONOS_CREATE_SWITCHES,
|
||||
SONOS_POLL_UPDATE,
|
||||
SONOS_REBOOTED,
|
||||
|
@ -194,6 +195,7 @@ class SonosSpeaker:
|
|||
|
||||
# Misc features
|
||||
self.buttons_enabled: bool | None = None
|
||||
self.mic_enabled: bool | None = None
|
||||
self.status_light: bool | None = None
|
||||
|
||||
# Grouping
|
||||
|
@ -435,21 +437,15 @@ class SonosSpeaker:
|
|||
|
||||
async def async_update_device_properties(self, event: SonosEvent) -> None:
|
||||
"""Update device properties from an event."""
|
||||
if "mic_enabled" in event.variables:
|
||||
mic_exists = self.mic_enabled is not None
|
||||
self.mic_enabled = bool(int(event.variables["mic_enabled"]))
|
||||
if not mic_exists:
|
||||
async_dispatcher_send(self.hass, SONOS_CREATE_MIC_SENSOR, self)
|
||||
|
||||
if more_info := event.variables.get("more_info"):
|
||||
battery_dict = dict(x.split(":") for x in more_info.split(","))
|
||||
for unused in UNUSED_DEVICE_KEYS:
|
||||
battery_dict.pop(unused, None)
|
||||
if not battery_dict:
|
||||
return
|
||||
if "BattChg" not in battery_dict:
|
||||
_LOGGER.debug(
|
||||
"Unknown device properties update for %s (%s), please report an issue: '%s'",
|
||||
self.zone_name,
|
||||
self.model_name,
|
||||
more_info,
|
||||
)
|
||||
return
|
||||
await self.async_update_battery_info(battery_dict)
|
||||
await self.async_update_battery_info(more_info)
|
||||
|
||||
self.async_write_entity_states()
|
||||
|
||||
@callback
|
||||
|
@ -559,8 +555,22 @@ class SonosSpeaker:
|
|||
#
|
||||
# Battery management
|
||||
#
|
||||
async def async_update_battery_info(self, battery_dict: dict[str, Any]) -> None:
|
||||
"""Update battery info using the decoded SonosEvent."""
|
||||
async def async_update_battery_info(self, more_info: str) -> None:
|
||||
"""Update battery info using a SonosEvent payload value."""
|
||||
battery_dict = dict(x.split(":") for x in more_info.split(","))
|
||||
for unused in UNUSED_DEVICE_KEYS:
|
||||
battery_dict.pop(unused, None)
|
||||
if not battery_dict:
|
||||
return
|
||||
if "BattChg" not in battery_dict:
|
||||
_LOGGER.debug(
|
||||
"Unknown device properties update for %s (%s), please report an issue: '%s'",
|
||||
self.zone_name,
|
||||
self.model_name,
|
||||
more_info,
|
||||
)
|
||||
return
|
||||
|
||||
self._last_battery_event = dt_util.utcnow()
|
||||
|
||||
is_charging = EVENT_CHARGING[battery_dict["BattChg"]]
|
||||
|
|
|
@ -222,11 +222,12 @@ def battery_info_fixture():
|
|||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="battery_event")
|
||||
def battery_event_fixture(soco):
|
||||
"""Create battery_event fixture."""
|
||||
@pytest.fixture(name="device_properties_event")
|
||||
def device_properties_event_fixture(soco):
|
||||
"""Create device_properties_event fixture."""
|
||||
variables = {
|
||||
"zone_name": "Zone A",
|
||||
"mic_enabled": "1",
|
||||
"more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25",
|
||||
}
|
||||
return SonosMockEvent(soco, soco.deviceProperties, variables)
|
||||
|
|
|
@ -45,7 +45,7 @@ async def test_battery_attributes(hass, async_autosetup_sonos, soco):
|
|||
)
|
||||
|
||||
|
||||
async def test_battery_on_S1(hass, async_setup_sonos, soco, battery_event):
|
||||
async def test_battery_on_s1(hass, async_setup_sonos, soco, device_properties_event):
|
||||
"""Test battery state updates on a Sonos S1 device."""
|
||||
soco.get_battery_info.return_value = {}
|
||||
|
||||
|
@ -60,7 +60,7 @@ async def test_battery_on_S1(hass, async_setup_sonos, soco, battery_event):
|
|||
assert "binary_sensor.zone_a_power" not in entity_registry.entities
|
||||
|
||||
# Update the speaker with a callback event
|
||||
sub_callback(battery_event)
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
battery = entity_registry.entities["sensor.zone_a_battery"]
|
||||
|
@ -74,7 +74,7 @@ async def test_battery_on_S1(hass, async_setup_sonos, soco, battery_event):
|
|||
|
||||
|
||||
async def test_device_payload_without_battery(
|
||||
hass, async_setup_sonos, soco, battery_event, caplog
|
||||
hass, async_setup_sonos, soco, device_properties_event, caplog
|
||||
):
|
||||
"""Test device properties event update without battery info."""
|
||||
soco.get_battery_info.return_value = None
|
||||
|
@ -85,16 +85,16 @@ async def test_device_payload_without_battery(
|
|||
sub_callback = subscription.callback
|
||||
|
||||
bad_payload = "BadKey:BadValue"
|
||||
battery_event.variables["more_info"] = bad_payload
|
||||
device_properties_event.variables["more_info"] = bad_payload
|
||||
|
||||
sub_callback(battery_event)
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert bad_payload in caplog.text
|
||||
|
||||
|
||||
async def test_device_payload_without_battery_and_ignored_keys(
|
||||
hass, async_setup_sonos, soco, battery_event, caplog
|
||||
hass, async_setup_sonos, soco, device_properties_event, caplog
|
||||
):
|
||||
"""Test device properties event update without battery info and ignored keys."""
|
||||
soco.get_battery_info.return_value = None
|
||||
|
@ -105,18 +105,35 @@ async def test_device_payload_without_battery_and_ignored_keys(
|
|||
sub_callback = subscription.callback
|
||||
|
||||
ignored_payload = "SPID:InCeiling,TargetRoomName:Bouncy House"
|
||||
battery_event.variables["more_info"] = ignored_payload
|
||||
device_properties_event.variables["more_info"] = ignored_payload
|
||||
|
||||
sub_callback(battery_event)
|
||||
sub_callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ignored_payload not in caplog.text
|
||||
|
||||
|
||||
async def test_audio_input_sensor(hass, async_autosetup_sonos, soco):
|
||||
"""Test sonos device with battery state."""
|
||||
"""Test audio input sensor."""
|
||||
entity_registry = ent_reg.async_get(hass)
|
||||
|
||||
audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"]
|
||||
audio_input_state = hass.states.get(audio_input_sensor.entity_id)
|
||||
assert audio_input_state.state == "Dolby 5.1"
|
||||
|
||||
|
||||
async def test_microphone_binary_sensor(
|
||||
hass, async_autosetup_sonos, soco, device_properties_event
|
||||
):
|
||||
"""Test microphone binary sensor."""
|
||||
entity_registry = ent_reg.async_get(hass)
|
||||
assert "binary_sensor.zone_a_microphone" not in entity_registry.entities
|
||||
|
||||
# Update the speaker with a callback event
|
||||
subscription = soco.deviceProperties.subscribe.return_value
|
||||
subscription.callback(device_properties_event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mic_binary_sensor = entity_registry.entities["binary_sensor.zone_a_microphone"]
|
||||
mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id)
|
||||
assert mic_binary_sensor_state.state == STATE_ON
|
||||
|
|
Loading…
Add table
Reference in a new issue