mirror of
https://github.com/home-assistant/core.git
synced 2025-08-29 17:31:41 +02:00
Add Sleep as Android integration (#142569)
This commit is contained in:
@@ -466,6 +466,7 @@ homeassistant.components.simplisafe.*
|
||||
homeassistant.components.siren.*
|
||||
homeassistant.components.skybell.*
|
||||
homeassistant.components.slack.*
|
||||
homeassistant.components.sleep_as_android.*
|
||||
homeassistant.components.sleepiq.*
|
||||
homeassistant.components.smhi.*
|
||||
homeassistant.components.smlight.*
|
||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1415,6 +1415,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/skybell/ @tkdrob
|
||||
/homeassistant/components/slack/ @tkdrob @fletcherau
|
||||
/tests/components/slack/ @tkdrob @fletcherau
|
||||
/homeassistant/components/sleep_as_android/ @tr4nt0r
|
||||
/tests/components/sleep_as_android/ @tr4nt0r
|
||||
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
||||
/tests/components/sleepiq/ @mfugate1 @kbickar
|
||||
/homeassistant/components/slide/ @ualex73
|
||||
|
66
homeassistant/components/sleep_as_android/__init__.py
Normal file
66
homeassistant/components/sleep_as_android/__init__.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""The Sleep as Android integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp.web import Request, Response
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
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]
|
||||
|
||||
type SleepAsAndroidConfigEntry = ConfigEntry
|
||||
|
||||
WEBHOOK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_EVENT): str,
|
||||
vol.Optional(ATTR_VALUE1): str,
|
||||
vol.Optional(ATTR_VALUE2): str,
|
||||
vol.Optional(ATTR_VALUE3): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def handle_webhook(
|
||||
hass: HomeAssistant, webhook_id: str, request: Request
|
||||
) -> Response:
|
||||
"""Handle incoming Sleep as Android webhook request."""
|
||||
|
||||
try:
|
||||
data = WEBHOOK_SCHEMA(await request.json())
|
||||
except vol.MultipleInvalid as error:
|
||||
return Response(
|
||||
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
)
|
||||
|
||||
async_dispatcher_send(hass, DOMAIN, webhook_id, data)
|
||||
return Response(status=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: SleepAsAndroidConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Sleep as Android from a config entry."""
|
||||
|
||||
webhook.async_register(
|
||||
hass, DOMAIN, entry.title, entry.data[CONF_WEBHOOK_ID], handle_webhook
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: SleepAsAndroidConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
14
homeassistant/components/sleep_as_android/config_flow.py
Normal file
14
homeassistant/components/sleep_as_android/config_flow.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Config flow for the Sleep as Android integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
config_entry_flow.register_webhook_flow(
|
||||
DOMAIN,
|
||||
"Sleep as Android",
|
||||
{"docs_url": "https://www.home-assistant.io/integrations/sleep_as_android"},
|
||||
allow_multiple=True,
|
||||
)
|
30
homeassistant/components/sleep_as_android/const.py
Normal file
30
homeassistant/components/sleep_as_android/const.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Constants for the Sleep as Android integration."""
|
||||
|
||||
DOMAIN = "sleep_as_android"
|
||||
|
||||
ATTR_EVENT = "event"
|
||||
ATTR_VALUE1 = "value1"
|
||||
ATTR_VALUE2 = "value2"
|
||||
ATTR_VALUE3 = "value3"
|
||||
|
||||
MAP_EVENTS = {
|
||||
"sleep_tracking_paused": "paused",
|
||||
"sleep_tracking_resumed": "resumed",
|
||||
"sleep_tracking_started": "started",
|
||||
"sleep_tracking_stopped": "stopped",
|
||||
"alarm_alert_dismiss": "alert_dismiss",
|
||||
"alarm_alert_start": "alert_start",
|
||||
"alarm_rescheduled": "rescheduled",
|
||||
"alarm_skip_next": "skip_next",
|
||||
"alarm_snooze_canceled": "snooze_canceled",
|
||||
"alarm_snooze_clicked": "snooze_clicked",
|
||||
"alarm_wake_up_check": "wake_up_check",
|
||||
"sound_event_baby": "baby",
|
||||
"sound_event_cough": "cough",
|
||||
"sound_event_laugh": "laugh",
|
||||
"sound_event_snore": "snore",
|
||||
"sound_event_talk": "talk",
|
||||
"lullaby_start": "start",
|
||||
"lullaby_stop": "stop",
|
||||
"lullaby_volume_down": "volume_down",
|
||||
}
|
33
homeassistant/components/sleep_as_android/entity.py
Normal file
33
homeassistant/components/sleep_as_android/entity.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Base entity for Sleep as Android integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
|
||||
from . import SleepAsAndroidConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class SleepAsAndroidEntity(Entity):
|
||||
"""Base entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: SleepAsAndroidConfigEntry,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{entity_description.key}"
|
||||
self.entity_description = entity_description
|
||||
self.webhook_id = config_entry.data[CONF_WEBHOOK_ID]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Urbandroid",
|
||||
model="Sleep as Android",
|
||||
name=config_entry.title,
|
||||
)
|
161
homeassistant/components/sleep_as_android/event.py
Normal file
161
homeassistant/components/sleep_as_android/event.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Event platform for Sleep as Android integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from homeassistant.components.event import (
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
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 .entity import SleepAsAndroidEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class SleepAsAndroidEventEntityDescription(EventEntityDescription):
|
||||
"""Sleep as Android sensor description."""
|
||||
|
||||
event_types: list[str]
|
||||
|
||||
|
||||
class SleepAsAndroidEvent(StrEnum):
|
||||
"""Sleep as Android events."""
|
||||
|
||||
ALARM_CLOCK = "alarm_clock"
|
||||
USER_NOTIFICATION = "user_notification"
|
||||
SMART_WAKEUP = "smart_wakeup"
|
||||
SLEEP_HEALTH = "sleep_health"
|
||||
LULLABY = "lullaby"
|
||||
SLEEP_PHASE = "sleep_phase"
|
||||
SLEEP_TRACKING = "sleep_tracking"
|
||||
SOUND_EVENT = "sound_event"
|
||||
|
||||
|
||||
EVENT_DESCRIPTIONS: tuple[SleepAsAndroidEventEntityDescription, ...] = (
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.SLEEP_TRACKING,
|
||||
translation_key=SleepAsAndroidEvent.SLEEP_TRACKING,
|
||||
device_class=EventDeviceClass.BUTTON,
|
||||
event_types=[
|
||||
"paused",
|
||||
"resumed",
|
||||
"started",
|
||||
"stopped",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.ALARM_CLOCK,
|
||||
translation_key=SleepAsAndroidEvent.ALARM_CLOCK,
|
||||
event_types=[
|
||||
"alert_dismiss",
|
||||
"alert_start",
|
||||
"rescheduled",
|
||||
"skip_next",
|
||||
"snooze_canceled",
|
||||
"snooze_clicked",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.SMART_WAKEUP,
|
||||
translation_key=SleepAsAndroidEvent.SMART_WAKEUP,
|
||||
event_types=[
|
||||
"before_smart_period",
|
||||
"smart_period",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.USER_NOTIFICATION,
|
||||
translation_key=SleepAsAndroidEvent.USER_NOTIFICATION,
|
||||
event_types=[
|
||||
"wake_up_check",
|
||||
"show_skip_next_alarm",
|
||||
"time_to_bed_alarm_alert",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.SLEEP_PHASE,
|
||||
translation_key=SleepAsAndroidEvent.SLEEP_PHASE,
|
||||
event_types=[
|
||||
"awake",
|
||||
"deep_sleep",
|
||||
"light_sleep",
|
||||
"not_awake",
|
||||
"rem",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.SOUND_EVENT,
|
||||
translation_key=SleepAsAndroidEvent.SOUND_EVENT,
|
||||
event_types=[
|
||||
"baby",
|
||||
"cough",
|
||||
"laugh",
|
||||
"snore",
|
||||
"talk",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.LULLABY,
|
||||
translation_key=SleepAsAndroidEvent.LULLABY,
|
||||
event_types=[
|
||||
"start",
|
||||
"stop",
|
||||
"volume_down",
|
||||
],
|
||||
),
|
||||
SleepAsAndroidEventEntityDescription(
|
||||
key=SleepAsAndroidEvent.SLEEP_HEALTH,
|
||||
translation_key=SleepAsAndroidEvent.SLEEP_HEALTH,
|
||||
event_types=[
|
||||
"antisnoring",
|
||||
"apnea_alarm",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SleepAsAndroidConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the event platform."""
|
||||
|
||||
async_add_entities(
|
||||
SleepAsAndroidEventEntity(config_entry, description)
|
||||
for description in EVENT_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class SleepAsAndroidEventEntity(SleepAsAndroidEntity, EventEntity):
|
||||
"""An event entity."""
|
||||
|
||||
entity_description: SleepAsAndroidEventEntityDescription
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self, webhook_id: str, data: dict[str, str]) -> None:
|
||||
"""Handle the Sleep as Android event."""
|
||||
event = MAP_EVENTS.get(data[ATTR_EVENT], data[ATTR_EVENT])
|
||||
if (
|
||||
webhook_id == self.webhook_id
|
||||
and event in self.entity_description.event_types
|
||||
):
|
||||
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)
|
||||
)
|
30
homeassistant/components/sleep_as_android/icons.json
Normal file
30
homeassistant/components/sleep_as_android/icons.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"entity": {
|
||||
"event": {
|
||||
"alarm_clock": {
|
||||
"default": "mdi:alarm"
|
||||
},
|
||||
"user_notification": {
|
||||
"default": "mdi:cellphone-message"
|
||||
},
|
||||
"smart_wakeup": {
|
||||
"default": "mdi:brain"
|
||||
},
|
||||
"sleep_phase": {
|
||||
"default": "mdi:bed"
|
||||
},
|
||||
"sound_event": {
|
||||
"default": "mdi:chat-sleep-outline"
|
||||
},
|
||||
"sleep_tracking": {
|
||||
"default": "mdi:record-rec"
|
||||
},
|
||||
"lullaby": {
|
||||
"default": "mdi:cradle-outline"
|
||||
},
|
||||
"sleep_health": {
|
||||
"default": "mdi:heart-pulse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
homeassistant/components/sleep_as_android/manifest.json
Normal file
10
homeassistant/components/sleep_as_android/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "sleep_as_android",
|
||||
"name": "Sleep as Android",
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sleep_as_android",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver"
|
||||
}
|
110
homeassistant/components/sleep_as_android/quality_scale.yaml
Normal file
110
homeassistant/components/sleep_as_android/quality_scale.yaml
Normal file
@@ -0,0 +1,110 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: has no actions
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: does not poll
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage:
|
||||
status: done
|
||||
comment: uses webhook flow helper, already covered
|
||||
config-flow: done
|
||||
dependency-transparency:
|
||||
status: exempt
|
||||
comment: no dependencies
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data:
|
||||
status: exempt
|
||||
comment: has no runtime data
|
||||
test-before-configure:
|
||||
status: exempt
|
||||
comment: nothing to test
|
||||
test-before-setup:
|
||||
status: exempt
|
||||
comment: nothing to test
|
||||
unique-config-entry:
|
||||
status: exempt
|
||||
comment: only 1 webhook can be configured per device. It's not possible to prevent different devices from using the same webhook
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: integration has no options
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: exempt
|
||||
comment: only state-less entities
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: exempt
|
||||
comment: only state-less entities
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: no authentication required
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: no discovery
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: cannot be discovered
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: does not poll
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: has no devices
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: has no devices
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: does not raise exceptions
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: webhook config flow helper does not implement reconfigure
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: has no repairs
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: has no stale devices
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: exempt
|
||||
comment: has no external dependencies
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: does not do http requests
|
||||
strict-typing: done
|
123
homeassistant/components/sleep_as_android/strings.json
Normal file
123
homeassistant/components/sleep_as_android/strings.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Sleep as Android",
|
||||
"description": "Are you sure you want to set up the Sleep as Android integration?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]",
|
||||
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to set up a webhook.\n\nOpen Sleep as Android and go to *Settings → Services → Automation → Webhooks*\n\nEnable *Webhooks* and fill in the following webhook in the URL field:\n\n`{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"event": {
|
||||
"sleep_tracking": {
|
||||
"name": "Sleep tracking",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"paused": "[%key:common::state::paused%]",
|
||||
"resumed": "Resumed",
|
||||
"started": "Started",
|
||||
"stopped": "[%key:common::state::stopped%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"alarm_clock": {
|
||||
"name": "Alarm clock",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"alert_dismiss": "Alarm dismissed",
|
||||
"alert_start": "Alarm started",
|
||||
"rescheduled": "Alarm rescheduled",
|
||||
"skip_next": "Alarm skipped",
|
||||
"snooze_canceled": "Snooze canceled",
|
||||
"snooze_clicked": "Snoozing"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"smart_wakeup": {
|
||||
"name": "Smart wake-up",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"before_smart_period": "45min before smart wake-up",
|
||||
"smart_period": "Smart wake-up started"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_notification": {
|
||||
"name": "User notification",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"wake_up_check": "Wake-up check",
|
||||
"show_skip_next_alarm": "Skip next alarm",
|
||||
"time_to_bed_alarm_alert": "Time to bed"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sleep_phase": {
|
||||
"name": "Sleep phase",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"awake": "Woke up",
|
||||
"deep_sleep": "Deep sleep",
|
||||
"light_sleep": "Light sleep",
|
||||
"not_awake": "Fell asleep",
|
||||
"rem": "REM sleep"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sound_event": {
|
||||
"name": "Sound recognition",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"baby": "Baby crying",
|
||||
"cough": "Coughing or sneezing",
|
||||
"laugh": "Laughter",
|
||||
"snore": "Snoring",
|
||||
"talk": "Talking"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"lullaby": {
|
||||
"name": "Lullaby",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"start": "Started",
|
||||
"stop": "[%key:common::state::stopped%]",
|
||||
"volume_down": "Lowering volume"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sleep_health": {
|
||||
"name": "Sleep health",
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"antisnoring": "Anti-snoring triggered",
|
||||
"apnea_alarm": "Sleep apnea detected"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -576,6 +576,7 @@ FLOWS = {
|
||||
"sky_remote",
|
||||
"skybell",
|
||||
"slack",
|
||||
"sleep_as_android",
|
||||
"sleepiq",
|
||||
"slide_local",
|
||||
"slimproto",
|
||||
|
@@ -5994,6 +5994,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"sleep_as_android": {
|
||||
"name": "Sleep as Android",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"sleepiq": {
|
||||
"name": "SleepIQ",
|
||||
"integration_type": "hub",
|
||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@@ -4416,6 +4416,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.sleep_as_android.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.sleepiq.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
1
tests/components/sleep_as_android/__init__.py
Normal file
1
tests/components/sleep_as_android/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Sleep as Android integration."""
|
34
tests/components/sleep_as_android/conftest.py
Normal file
34
tests/components/sleep_as_android/conftest.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Common fixtures for the Sleep as Android tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sleep_as_android.const import DOMAIN
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.sleep_as_android.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock Sleep as Android configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Sleep as Android",
|
||||
data={
|
||||
"cloudhook": False,
|
||||
CONF_WEBHOOK_ID: "webhook_id",
|
||||
},
|
||||
entry_id="01JRD840SAZ55DGXBD78PTQ4EF",
|
||||
)
|
494
tests/components/sleep_as_android/snapshots/test_event.ambr
Normal file
494
tests/components/sleep_as_android/snapshots/test_event.ambr
Normal file
@@ -0,0 +1,494 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[event.sleep_as_android_alarm_clock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'alert_dismiss',
|
||||
'alert_start',
|
||||
'rescheduled',
|
||||
'skip_next',
|
||||
'snooze_canceled',
|
||||
'snooze_clicked',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_alarm_clock',
|
||||
'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 clock',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.ALARM_CLOCK: 'alarm_clock'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_alarm_clock',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_alarm_clock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'alert_dismiss',
|
||||
'alert_start',
|
||||
'rescheduled',
|
||||
'skip_next',
|
||||
'snooze_canceled',
|
||||
'snooze_clicked',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Alarm clock',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_alarm_clock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_lullaby-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'start',
|
||||
'stop',
|
||||
'volume_down',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_lullaby',
|
||||
'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': 'Lullaby',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.LULLABY: 'lullaby'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_lullaby',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_lullaby-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'start',
|
||||
'stop',
|
||||
'volume_down',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Lullaby',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_lullaby',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_health-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'antisnoring',
|
||||
'apnea_alarm',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_sleep_health',
|
||||
'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': 'Sleep health',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.SLEEP_HEALTH: 'sleep_health'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_health',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_health-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'antisnoring',
|
||||
'apnea_alarm',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Sleep health',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_sleep_health',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_phase-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'awake',
|
||||
'deep_sleep',
|
||||
'light_sleep',
|
||||
'not_awake',
|
||||
'rem',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_sleep_phase',
|
||||
'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': 'Sleep phase',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.SLEEP_PHASE: 'sleep_phase'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_phase',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_phase-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'awake',
|
||||
'deep_sleep',
|
||||
'light_sleep',
|
||||
'not_awake',
|
||||
'rem',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Sleep phase',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_sleep_phase',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_tracking-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'paused',
|
||||
'resumed',
|
||||
'started',
|
||||
'stopped',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_sleep_tracking',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Sleep tracking',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.SLEEP_TRACKING: 'sleep_tracking'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sleep_tracking',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sleep_tracking-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'paused',
|
||||
'resumed',
|
||||
'started',
|
||||
'stopped',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Sleep tracking',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_sleep_tracking',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_smart_wake_up-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'before_smart_period',
|
||||
'smart_period',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_smart_wake_up',
|
||||
'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': 'Smart wake-up',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.SMART_WAKEUP: 'smart_wakeup'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_smart_wakeup',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_smart_wake_up-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'before_smart_period',
|
||||
'smart_period',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Smart wake-up',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_smart_wake_up',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sound_recognition-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'baby',
|
||||
'cough',
|
||||
'laugh',
|
||||
'snore',
|
||||
'talk',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_sound_recognition',
|
||||
'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': 'Sound recognition',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.SOUND_EVENT: 'sound_event'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_sound_event',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_sound_recognition-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'baby',
|
||||
'cough',
|
||||
'laugh',
|
||||
'snore',
|
||||
'talk',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android Sound recognition',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_sound_recognition',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_user_notification-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'wake_up_check',
|
||||
'show_skip_next_alarm',
|
||||
'time_to_bed_alarm_alert',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.sleep_as_android_user_notification',
|
||||
'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': 'User notification',
|
||||
'platform': 'sleep_as_android',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <SleepAsAndroidEvent.USER_NOTIFICATION: 'user_notification'>,
|
||||
'unique_id': '01JRD840SAZ55DGXBD78PTQ4EF_user_notification',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[event.sleep_as_android_user_notification-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'wake_up_check',
|
||||
'show_skip_next_alarm',
|
||||
'time_to_bed_alarm_alert',
|
||||
]),
|
||||
'friendly_name': 'Sleep as Android User notification',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.sleep_as_android_user_notification',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
38
tests/components/sleep_as_android/test_config_flow.py
Normal file
38
tests/components/sleep_as_android/test_config_flow.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Test the Sleep as Android config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from homeassistant.components.sleep_as_android.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.webhook.async_generate_id",
|
||||
return_value="webhook_id",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.webhook.async_generate_url",
|
||||
return_value="http://example.com:8123",
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Sleep as Android"
|
||||
assert result["data"] == {
|
||||
"cloudhook": False,
|
||||
CONF_WEBHOOK_ID: "webhook_id",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
161
tests/components/sleep_as_android/test_event.py
Normal file
161
tests/components/sleep_as_android/test_event.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Test the Sleep as Android event platform."""
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@freeze_time("2025-01-01T03:30:00.000Z")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Snapshot test states of event platform."""
|
||||
|
||||
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(
|
||||
("entity", "payload"),
|
||||
[
|
||||
("sleep_tracking", {"event": "sleep_tracking_paused"}),
|
||||
("sleep_tracking", {"event": "sleep_tracking_resumed"}),
|
||||
("sleep_tracking", {"event": "sleep_tracking_started"}),
|
||||
("sleep_tracking", {"event": "sleep_tracking_stopped"}),
|
||||
(
|
||||
"alarm_clock",
|
||||
{
|
||||
"event": "alarm_alert_dismiss",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
(
|
||||
"alarm_clock",
|
||||
{
|
||||
"event": "alarm_alert_start",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
("alarm_clock", {"event": "alarm_rescheduled"}),
|
||||
(
|
||||
"alarm_clock",
|
||||
{"event": "alarm_skip_next", "value1": "1582719660934", "value2": "label"},
|
||||
),
|
||||
(
|
||||
"alarm_clock",
|
||||
{
|
||||
"event": "alarm_snooze_canceled",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
(
|
||||
"alarm_clock",
|
||||
{
|
||||
"event": "alarm_snooze_clicked",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
("smart_wake_up", {"event": "before_smart_period", "value1": "label"}),
|
||||
("smart_wake_up", {"event": "smart_period"}),
|
||||
("sleep_health", {"event": "antisnoring"}),
|
||||
("sleep_health", {"event": "apnea_alarm"}),
|
||||
("lullaby", {"event": "lullaby_start"}),
|
||||
("lullaby", {"event": "lullaby_stop"}),
|
||||
("lullaby", {"event": "lullaby_volume_down"}),
|
||||
("sleep_phase", {"event": "awake"}),
|
||||
("sleep_phase", {"event": "deep_sleep"}),
|
||||
("sleep_phase", {"event": "light_sleep"}),
|
||||
("sleep_phase", {"event": "not_awake"}),
|
||||
("sleep_phase", {"event": "rem"}),
|
||||
("sound_recognition", {"event": "sound_event_baby"}),
|
||||
("sound_recognition", {"event": "sound_event_cough"}),
|
||||
("sound_recognition", {"event": "sound_event_laugh"}),
|
||||
("sound_recognition", {"event": "sound_event_snore"}),
|
||||
("sound_recognition", {"event": "sound_event_talk"}),
|
||||
("user_notification", {"event": "alarm_wake_up_check"}),
|
||||
(
|
||||
"user_notification",
|
||||
{
|
||||
"event": "show_skip_next_alarm",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
(
|
||||
"user_notification",
|
||||
{
|
||||
"event": "time_to_bed_alarm_alert",
|
||||
"value1": "1582719660934",
|
||||
"value2": "label",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
@freeze_time("2025-01-01T03:30:00.000+00:00")
|
||||
async def test_webhook_event(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
entity: str,
|
||||
payload: dict[str, str],
|
||||
) -> None:
|
||||
"""Test webhook events."""
|
||||
|
||||
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(f"event.sleep_as_android_{entity}"))
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
response = await client.post("/api/webhook/webhook_id", json=payload)
|
||||
assert response.status == HTTPStatus.NO_CONTENT
|
||||
|
||||
assert (state := hass.states.get(f"event.sleep_as_android_{entity}"))
|
||||
assert state.state == "2025-01-01T03:30:00.000+00:00"
|
||||
|
||||
|
||||
async def test_webhook_invalid(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
) -> None:
|
||||
"""Test webhook event call with invalid data."""
|
||||
|
||||
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
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
|
||||
response = await client.post("/api/webhook/webhook_id", json={})
|
||||
|
||||
assert response.status == HTTPStatus.UNPROCESSABLE_ENTITY
|
22
tests/components/sleep_as_android/test_init.py
Normal file
22
tests/components/sleep_as_android/test_init.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Test the Sleep as Android integration setup."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_entry_setup_unload(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test integration setup and unload."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
Reference in New Issue
Block a user