Add sensors to Yoto
Add the sensor platform: battery, card slot and day mode, plus power source, SSID and Wi-Fi RSSI as diagnostics (RSSI disabled by default). Move the online check to the base entity so all entities (including the media player) report unavailable when the player is offline.
This commit is contained in:
@@ -14,7 +14,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
from .const import DOMAIN
|
||||
from .coordinator import YotoConfigEntry, YotoDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER]
|
||||
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: YotoConfigEntry) -> bool:
|
||||
|
||||
@@ -128,10 +128,9 @@ class YotoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, YotoPlayer]]):
|
||||
"""Ask each player to push a fresh status snapshot over MQTT."""
|
||||
if not self.client.is_mqtt_connected:
|
||||
return
|
||||
# Fire-and-forget: the data/status response lands via the on_update
|
||||
# callback later, which already triggers async_set_updated_data.
|
||||
for device_id in list(self.client.players):
|
||||
await self.client.request_player_status(device_id)
|
||||
await self.client.request_player_extended_status(device_id)
|
||||
|
||||
def _mqtt_event(self, _player: YotoPlayer) -> None:
|
||||
"""Handle a real-time update pushed by the Yoto MQTT broker."""
|
||||
|
||||
@@ -43,4 +43,8 @@ class YotoEntity(CoordinatorEntity[YotoDataUpdateCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return super().available and self._player_id in self.coordinator.data
|
||||
return (
|
||||
super().available
|
||||
and self._player_id in self.coordinator.data
|
||||
and bool(self.player.is_online)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"card_insertion_state": {
|
||||
"default": "mdi:card-bulleted-outline",
|
||||
"state": {
|
||||
"none": "mdi:card-bulleted-off-outline",
|
||||
"physical": "mdi:card-bulleted",
|
||||
"remote": "mdi:cast-audio",
|
||||
"streaming": "mdi:radio-tower"
|
||||
}
|
||||
},
|
||||
"day_mode": {
|
||||
"default": "mdi:theme-light-dark",
|
||||
"state": {
|
||||
"day": "mdi:weather-sunny",
|
||||
"night": "mdi:weather-night"
|
||||
}
|
||||
},
|
||||
"power_source": {
|
||||
"default": "mdi:power-plug"
|
||||
},
|
||||
"ssid": {
|
||||
"default": "mdi:router-wireless"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,11 +82,6 @@ class YotoMediaPlayer(YotoEntity, MediaPlayerEntity):
|
||||
super().__init__(coordinator, player)
|
||||
self._attr_unique_id = player.id
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return whether the player is reachable through the Yoto cloud."""
|
||||
return super().available and bool(self.player.is_online)
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the playback state."""
|
||||
|
||||
@@ -57,20 +57,12 @@ rules:
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: Only the media_player entity ships in this PR; no diagnostic entities yet.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Only the media_player entity ships in this PR; no entities are disabled by default.
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: The media_player uses the device name; no translatable strings yet.
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No custom icon translations are needed yet.
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: Authorization is the only configuration; reauth covers re-linking the account.
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Sensor platform for the Yoto integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from yoto_api import CardInsertionState, DayMode, PowerSource, YotoPlayer
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import YotoConfigEntry, YotoDataUpdateCoordinator
|
||||
from .entity import YotoEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _enum_state(value: CardInsertionState | PowerSource | None) -> str | None:
|
||||
"""Return an enum member as a lowercase string, or None if unset."""
|
||||
return value.name.lower() if value is not None else None
|
||||
|
||||
|
||||
def _day_mode_state(value: DayMode | None) -> str | None:
|
||||
"""Return day/night, treating the firmware's UNKNOWN as unset."""
|
||||
if value is None or value is DayMode.UNKNOWN:
|
||||
return None
|
||||
return value.name.lower()
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class YotoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes a Yoto sensor entity."""
|
||||
|
||||
value_fn: Callable[[YotoPlayer], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[YotoSensorEntityDescription, ...] = (
|
||||
YotoSensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda player: player.status.battery_level_percentage,
|
||||
),
|
||||
YotoSensorEntityDescription(
|
||||
key="card_insertion_state",
|
||||
translation_key="card_insertion_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[state.name.lower() for state in CardInsertionState],
|
||||
value_fn=lambda player: _enum_state(player.status.card_insertion_state),
|
||||
),
|
||||
YotoSensorEntityDescription(
|
||||
key="day_mode",
|
||||
translation_key="day_mode",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["day", "night"],
|
||||
value_fn=lambda player: _day_mode_state(player.status.day_mode),
|
||||
),
|
||||
YotoSensorEntityDescription(
|
||||
key="power_source",
|
||||
translation_key="power_source",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[source.name.lower() for source in PowerSource],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda player: _enum_state(player.extended_status.power_source),
|
||||
),
|
||||
YotoSensorEntityDescription(
|
||||
key="ssid",
|
||||
translation_key="ssid",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda player: player.extended_status.network_ssid,
|
||||
),
|
||||
YotoSensorEntityDescription(
|
||||
key="wifi_rssi",
|
||||
translation_key="wifi_rssi",
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda player: player.extended_status.wifi_strength,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: YotoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Yoto sensor platform."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
YotoSensor(coordinator, player, description)
|
||||
for player in coordinator.client.players.values()
|
||||
for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class YotoSensor(YotoEntity, SensorEntity):
|
||||
"""Representation of a Yoto player sensor."""
|
||||
|
||||
entity_description: YotoSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: YotoDataUpdateCoordinator,
|
||||
player: YotoPlayer,
|
||||
description: YotoSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, player)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{player.id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the sensor value."""
|
||||
return self.entity_description.value_fn(self.player)
|
||||
@@ -30,6 +30,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"card_insertion_state": {
|
||||
"name": "Card slot",
|
||||
"state": {
|
||||
"none": "Empty",
|
||||
"physical": "Physical card",
|
||||
"remote": "Remote",
|
||||
"streaming": "Streaming"
|
||||
}
|
||||
},
|
||||
"day_mode": {
|
||||
"name": "Day mode",
|
||||
"state": {
|
||||
"day": "Day",
|
||||
"night": "Night"
|
||||
}
|
||||
},
|
||||
"power_source": {
|
||||
"name": "Power source",
|
||||
"state": {
|
||||
"battery": "Battery",
|
||||
"qi_dock": "Wireless dock",
|
||||
"usb_c": "USB-C",
|
||||
"v2_dock": "Dock"
|
||||
}
|
||||
},
|
||||
"ssid": {
|
||||
"name": "SSID"
|
||||
},
|
||||
"wifi_rssi": {
|
||||
"name": "Wi-Fi RSSI"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"card_detail_failed": {
|
||||
"message": "Could not load Yoto card details: {error}"
|
||||
|
||||
@@ -9,12 +9,17 @@ import jwt
|
||||
import pytest
|
||||
from yoto_api import (
|
||||
Card,
|
||||
CardInsertionState,
|
||||
Chapter,
|
||||
DayMode,
|
||||
Device,
|
||||
Group,
|
||||
PlaybackEvent,
|
||||
PlaybackStatus,
|
||||
PlayerExtendedStatus,
|
||||
PlayerInfo,
|
||||
PlayerStatus,
|
||||
PowerSource,
|
||||
Track,
|
||||
YotoPlayer,
|
||||
)
|
||||
@@ -88,6 +93,16 @@ def _build_player() -> YotoPlayer:
|
||||
firmware_version="v2.17.5",
|
||||
mac="aa:bb:cc:dd:ee:ff",
|
||||
)
|
||||
player.status = PlayerStatus(
|
||||
battery_level_percentage=75,
|
||||
card_insertion_state=CardInsertionState.PHYSICAL,
|
||||
day_mode=DayMode.DAY,
|
||||
)
|
||||
player.extended_status = PlayerExtendedStatus(
|
||||
power_source=PowerSource.USB_C,
|
||||
network_ssid="HomeNet",
|
||||
wifi_strength=-55,
|
||||
)
|
||||
player.last_event = PlaybackEvent(
|
||||
player_id=PLAYER_ID,
|
||||
playback_status=PlaybackStatus.PLAYING,
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.nursery_yoto_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.nursery_yoto_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Battery',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Battery',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'player-test_battery_level',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_battery-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'battery',
|
||||
'friendly_name': 'Nursery Yoto Battery',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_battery',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '75',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_card_slot-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'none',
|
||||
'physical',
|
||||
'remote',
|
||||
'streaming',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.nursery_yoto_card_slot',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Card slot',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Card slot',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'card_insertion_state',
|
||||
'unique_id': 'player-test_card_insertion_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_card_slot-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Nursery Yoto Card slot',
|
||||
'options': list([
|
||||
'none',
|
||||
'physical',
|
||||
'remote',
|
||||
'streaming',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_card_slot',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'physical',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_day_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'day',
|
||||
'night',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.nursery_yoto_day_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Day mode',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Day mode',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'day_mode',
|
||||
'unique_id': 'player-test_day_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_day_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Nursery Yoto Day mode',
|
||||
'options': list([
|
||||
'day',
|
||||
'night',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_day_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'day',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_power_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'battery',
|
||||
'v2_dock',
|
||||
'usb_c',
|
||||
'qi_dock',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.nursery_yoto_power_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Power source',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power source',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'power_source',
|
||||
'unique_id': 'player-test_power_source',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_power_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Nursery Yoto Power source',
|
||||
'options': list([
|
||||
'battery',
|
||||
'v2_dock',
|
||||
'usb_c',
|
||||
'qi_dock',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_power_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'usb_c',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_ssid-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.nursery_yoto_ssid',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'SSID',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'SSID',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ssid',
|
||||
'unique_id': 'player-test_ssid',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_ssid-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Nursery Yoto SSID',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_ssid',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'HomeNet',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_wi_fi_rssi-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.nursery_yoto_wi_fi_rssi',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Wi-Fi RSSI',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Wi-Fi RSSI',
|
||||
'platform': 'yoto',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'wifi_rssi',
|
||||
'unique_id': 'player-test_wifi_rssi',
|
||||
'unit_of_measurement': 'dBm',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.nursery_yoto_wi_fi_rssi-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'signal_strength',
|
||||
'friendly_name': 'Nursery Yoto Wi-Fi RSSI',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'dBm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.nursery_yoto_wi_fi_rssi',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-55',
|
||||
})
|
||||
# ---
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for the Yoto media player platform."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -22,7 +22,7 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_VOLUME_SET,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -68,7 +68,8 @@ async def test_entity_state(
|
||||
) -> None:
|
||||
"""Snapshot the media player entity state."""
|
||||
freezer.move_to("2026-05-08T12:00:00+00:00")
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
with patch("homeassistant.components.yoto.PLATFORMS", [Platform.MEDIA_PLAYER]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Tests for the Yoto sensor platform."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("setup_credentials")
|
||||
|
||||
ENTITY_ID = "sensor.nursery_yoto_battery"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_yoto_client", "entity_registry_enabled_by_default")
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Snapshot every Yoto sensor entity."""
|
||||
with patch("homeassistant.components.yoto.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_sensor_unavailable_when_offline(
|
||||
hass: HomeAssistant,
|
||||
mock_yoto_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Sensors are unavailable while the player is offline."""
|
||||
player = next(iter(mock_yoto_client.players.values()))
|
||||
player.is_online = False
|
||||
|
||||
with patch("homeassistant.components.yoto.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
Reference in New Issue
Block a user