mirror of
https://github.com/home-assistant/core.git
synced 2025-09-13 16:51:37 +02:00
Add sensor platform to Sleep as Android (#150440)
This commit is contained in:
@@ -15,7 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import ATTR_EVENT, ATTR_VALUE1, ATTR_VALUE2, ATTR_VALUE3, DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.EVENT]
|
||||
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR]
|
||||
|
||||
type SleepAsAndroidConfigEntry = ConfigEntry
|
||||
|
||||
|
@@ -28,3 +28,5 @@ MAP_EVENTS = {
|
||||
"lullaby_stop": "stop",
|
||||
"lullaby_volume_down": "volume_down",
|
||||
}
|
||||
|
||||
ALARM_LABEL_DEFAULT = "alarm"
|
||||
|
@@ -2,8 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from . import SleepAsAndroidConfigEntry
|
||||
@@ -31,3 +34,14 @@ class SleepAsAndroidEntity(Entity):
|
||||
model="Sleep as Android",
|
||||
name=config_entry.title,
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None:
|
||||
"""Handle the Sleep as Android event."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register event callback."""
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event)
|
||||
)
|
||||
|
@@ -11,11 +11,10 @@ from homeassistant.components.event import (
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SleepAsAndroidConfigEntry
|
||||
from .const import ATTR_EVENT, DOMAIN, MAP_EVENTS
|
||||
from .const import ATTR_EVENT, MAP_EVENTS
|
||||
from .entity import SleepAsAndroidEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -152,10 +151,3 @@ class SleepAsAndroidEventEntity(SleepAsAndroidEntity, EventEntity):
|
||||
):
|
||||
self._trigger_event(event)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register event callback."""
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event)
|
||||
)
|
||||
|
96
homeassistant/components/sleep_as_android/sensor.py
Normal file
96
homeassistant/components/sleep_as_android/sensor.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Sensor platform for Sleep as Android integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import SleepAsAndroidConfigEntry
|
||||
from .const import ALARM_LABEL_DEFAULT, ATTR_EVENT, ATTR_VALUE1, ATTR_VALUE2
|
||||
from .entity import SleepAsAndroidEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class SleepAsAndroidSensor(StrEnum):
|
||||
"""Sleep as Android sensors."""
|
||||
|
||||
NEXT_ALARM = "next_alarm"
|
||||
ALARM_LABEL = "alarm_label"
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=SleepAsAndroidSensor.NEXT_ALARM,
|
||||
translation_key=SleepAsAndroidSensor.NEXT_ALARM,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=SleepAsAndroidSensor.ALARM_LABEL,
|
||||
translation_key=SleepAsAndroidSensor.ALARM_LABEL,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SleepAsAndroidConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
|
||||
async_add_entities(
|
||||
SleepAsAndroidSensorEntity(config_entry, description)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class SleepAsAndroidSensorEntity(SleepAsAndroidEntity, RestoreSensor):
|
||||
"""A sensor entity."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None:
|
||||
"""Handle the Sleep as Android event."""
|
||||
|
||||
if webhook_id == self.webhook_id and data[ATTR_EVENT] in (
|
||||
"alarm_snooze_clicked",
|
||||
"alarm_snooze_canceled",
|
||||
"alarm_alert_start",
|
||||
"alarm_alert_dismiss",
|
||||
"alarm_skip_next",
|
||||
"show_skip_next_alarm",
|
||||
"alarm_rescheduled",
|
||||
):
|
||||
if (
|
||||
self.entity_description.key is SleepAsAndroidSensor.NEXT_ALARM
|
||||
and (alarm_time := data.get(ATTR_VALUE1))
|
||||
and alarm_time.isnumeric()
|
||||
):
|
||||
self._attr_native_value = datetime.fromtimestamp(
|
||||
int(alarm_time) / 1000, tz=dt_util.get_default_time_zone()
|
||||
)
|
||||
if self.entity_description.key is SleepAsAndroidSensor.ALARM_LABEL and (
|
||||
label := data.get(ATTR_VALUE2, ALARM_LABEL_DEFAULT)
|
||||
):
|
||||
self._attr_native_value = label
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore entity state."""
|
||||
state = await self.async_get_last_sensor_data()
|
||||
if state:
|
||||
self._attr_native_value = state.native_value
|
||||
|
||||
await super().async_added_to_hass()
|
@@ -118,6 +118,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"next_alarm": {
|
||||
"name": "Next alarm"
|
||||
},
|
||||
"alarm_label": {
|
||||
"name": "Alarm label",
|
||||
"state": {
|
||||
"alarm": "Alarm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
98
tests/components/sleep_as_android/snapshots/test_sensor.ambr
Normal file
98
tests/components/sleep_as_android/snapshots/test_sensor.ambr
Normal file
@@ -0,0 +1,98 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[sensor.sleep_as_android_alarm_label-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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': None,
|
||||
'entity_id': 'sensor.sleep_as_android_alarm_label',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Alarm label',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidSensor.ALARM_LABEL: 'alarm_label'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_alarm_label',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.sleep_as_android_alarm_label-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Sleep as Android Alarm label',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.sleep_as_android_alarm_label',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'label',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.sleep_as_android_next_alarm-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'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': None,
|
||||
'entity_id': 'sensor.sleep_as_android_next_alarm',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Next alarm',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidSensor.NEXT_ALARM: 'next_alarm'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_next_alarm',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.sleep_as_android_next_alarm-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Sleep as Android Next alarm',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.sleep_as_android_next_alarm',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2020-02-26T12:21:00+00:00',
|
||||
})
|
||||
# ---
|
@@ -1,13 +1,15 @@
|
||||
"""Test the Sleep as Android event platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import freeze_time
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -15,6 +17,16 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def event_only() -> Generator[None]:
|
||||
"""Enable only the event platform."""
|
||||
with patch(
|
||||
"homeassistant.components.sleep_as_android.PLATFORMS",
|
||||
[Platform.EVENT],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@freeze_time("2025-01-01T03:30:00.000Z")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
|
124
tests/components/sleep_as_android/test_sensor.py
Normal file
124
tests/components/sleep_as_android/test_sensor.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Test the Sleep as Android sensor platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
mock_restore_cache_with_extra_data,
|
||||
snapshot_platform,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def sensor_only() -> Generator[None]:
|
||||
"""Enable only the sensor platform."""
|
||||
with patch(
|
||||
"homeassistant.components.sleep_as_android.PLATFORMS",
|
||||
[Platform.SENSOR],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Snapshot test states of sensor platform."""
|
||||
|
||||
mock_restore_cache_with_extra_data(
|
||||
hass,
|
||||
(
|
||||
(
|
||||
State(
|
||||
"sensor.sleep_as_android_next_alarm",
|
||||
"",
|
||||
),
|
||||
{
|
||||
"native_value": datetime.fromisoformat("2020-02-26T12:21:00+00:00"),
|
||||
"native_unit_of_measurement": None,
|
||||
},
|
||||
),
|
||||
(
|
||||
State(
|
||||
"sensor.sleep_as_android_alarm_label",
|
||||
"",
|
||||
),
|
||||
{
|
||||
"native_value": "label",
|
||||
"native_unit_of_measurement": None,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event",
|
||||
[
|
||||
"alarm_snooze_clicked",
|
||||
"alarm_snooze_canceled",
|
||||
"alarm_alert_start",
|
||||
"alarm_alert_dismiss",
|
||||
"alarm_skip_next",
|
||||
"show_skip_next_alarm",
|
||||
"alarm_rescheduled",
|
||||
],
|
||||
)
|
||||
async def test_webhook_sensor(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
event: str,
|
||||
) -> None:
|
||||
"""Test webhook updates sensor."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (state := hass.states.get("sensor.sleep_as_android_next_alarm"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
assert (state := hass.states.get("sensor.sleep_as_android_alarm_label"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
response = await client.post(
|
||||
"/api/webhook/webhook_id",
|
||||
json={
|
||||
"event": event,
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
)
|
||||
assert response.status == HTTPStatus.NO_CONTENT
|
||||
|
||||
assert (state := hass.states.get("sensor.sleep_as_android_next_alarm"))
|
||||
assert state.state == "2020-02-26T12:21:00+00:00"
|
||||
|
||||
assert (state := hass.states.get("sensor.sleep_as_android_alarm_label"))
|
||||
assert state.state == "label"
|
Reference in New Issue
Block a user