From cdccf6d00943db12bdc0d15d2fe8ae2545a7fd86 Mon Sep 17 00:00:00 2001 From: sudoxnym <76703581+sudoxnym@users.noreply.github.com> Date: Sun, 5 May 2024 20:39:37 -0600 Subject: [PATCH] Add files via upload --- custom_components/saas/__init__.py | 32 +++ custom_components/saas/config_flow.py | 65 +++++ custom_components/saas/const.py | 131 +++++++++ custom_components/saas/manifest.json | 13 + custom_components/saas/sensor.py | 377 ++++++++++++++++++++++++++ 5 files changed, 618 insertions(+) create mode 100644 custom_components/saas/__init__.py create mode 100644 custom_components/saas/config_flow.py create mode 100644 custom_components/saas/const.py create mode 100644 custom_components/saas/manifest.json create mode 100644 custom_components/saas/sensor.py diff --git a/custom_components/saas/__init__.py b/custom_components/saas/__init__.py new file mode 100644 index 0000000..875d876 --- /dev/null +++ b/custom_components/saas/__init__.py @@ -0,0 +1,32 @@ +"""The SAAS - Sleep As Android Stats integration.""" +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +from .const import DOMAIN + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the SAAS - Sleep As Android Status component.""" + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up SAAS - Sleep As Android Status from a config entry.""" + hass.data[DOMAIN] = entry.data + discovery.load_platform(hass, "sensor", DOMAIN, {}, entry.data) + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + # Remove the sensor platform + await hass.config_entries.async_forward_entry_unload(entry, "sensor") + + # Remove the entry from the domain data + hass.data[DOMAIN].pop(entry.entry_id) + + return True + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up SAAS - Sleep As Android Status from a config entry.""" + hass.data[DOMAIN] = entry.data + hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, "sensor")) + return True \ No newline at end of file diff --git a/custom_components/saas/config_flow.py b/custom_components/saas/config_flow.py new file mode 100644 index 0000000..144b007 --- /dev/null +++ b/custom_components/saas/config_flow.py @@ -0,0 +1,65 @@ +from .const import DOMAIN, CONF_NAME, CONF_TOPIC, CONF_QOS, STATE_MAPPING, CONF_AWAKE_DURATION, CONF_SLEEP_DURATION, CONF_AWAKE_STATES, CONF_SLEEP_STATES, DEFAULT_AWAKE_DURATION, DEFAULT_SLEEP_DURATION, DEFAULT_AWAKE_STATES, DEFAULT_SLEEP_STATES +from homeassistant import config_entries +from homeassistant.core import callback +from voluptuous import Schema, Required, In +from homeassistant.helpers import config_validation as cv + +class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=Schema( + { + Required(CONF_NAME): str, + Required(CONF_TOPIC): str, + Required(CONF_QOS, default=0): In([0, 1, 2]), + Required(CONF_AWAKE_DURATION, default=DEFAULT_AWAKE_DURATION): int, + Required(CONF_SLEEP_DURATION, default=DEFAULT_SLEEP_DURATION): int, + Required(CONF_AWAKE_STATES, default=DEFAULT_AWAKE_STATES): cv.multi_select(STATE_MAPPING), + Required(CONF_SLEEP_STATES, default=DEFAULT_SLEEP_STATES): cv.multi_select(STATE_MAPPING), + } + ), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title=user_input.get(CONF_NAME, ""), data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=Schema( + { + Required(CONF_NAME, default=self.config_entry.options.get(CONF_NAME)): str, + Required(CONF_TOPIC, default=self.config_entry.options.get(CONF_TOPIC)): str, + Required(CONF_QOS, default=self.config_entry.options.get(CONF_QOS, 0)): In([0, 1, 2]), + Required(CONF_AWAKE_DURATION, default=self.config_entry.options.get(CONF_AWAKE_DURATION, DEFAULT_AWAKE_DURATION)): int, + Required(CONF_SLEEP_DURATION, default=self.config_entry.options.get(CONF_SLEEP_DURATION, DEFAULT_SLEEP_DURATION)): int, + Required(CONF_AWAKE_STATES, default=self.config_entry.options.get(CONF_AWAKE_STATES, DEFAULT_AWAKE_STATES)): cv.multi_select(STATE_MAPPING), + Required(CONF_SLEEP_STATES, default=self.config_entry.options.get(CONF_SLEEP_STATES, DEFAULT_SLEEP_STATES)): cv.multi_select(STATE_MAPPING), + } + ), + ) \ No newline at end of file diff --git a/custom_components/saas/const.py b/custom_components/saas/const.py new file mode 100644 index 0000000..053263b --- /dev/null +++ b/custom_components/saas/const.py @@ -0,0 +1,131 @@ +"""Constants for the SAAS - Sleep As Android Stats integration.""" + +DOMAIN = "saas" + +INTEGRATION_NAME = "SAAS - Sleep As Android Stats" +MODEL = "SAAS - Version 0.0.1" + +CONF_NAME = "name" # Name of the Integration +CONF_TOPIC = "topic_template" # MQTT Topic for Sleep As Android Events +CONF_QOS = "qos" # Quality of Service +CONF_AWAKE_DURATION = "awake_duration" # Awake Duration +CONF_SLEEP_DURATION = "sleep_duration" # Sleep Duration + +CONF_AWAKE_STATES = "awake_states" # Awake States +CONF_SLEEP_STATES = "sleep_states" # Sleep States + +DEFAULT_AWAKE_DURATION = 10 # Default Awake Duration +DEFAULT_SLEEP_DURATION = 10 # Default Sleep Duration +DEFAULT_AWAKE_STATES = ['{"event":"awake"}', '{"event":"sleep_tracking_stopped"}'] # Default Awake States +DEFAULT_SLEEP_STATES = ['{"event":"not_awake"}', '{"event":"rem"}', '{"event":"light_sleep"}', '{"event":"deep_sleep"}', '{"event":"sleep_tracking_started"}'] # Default Sleep States + +SENSOR_TYPES = { + "state": {"name": "State", "device_class": None}, + "awake": {"name": "Awake", "device_class": "motion"}, +} + +AVAILABLE_STATES = [ + 'unknown', + '{"event":"alarm_alert_dismiss"}', + '{"event":"alarm_alert_start"}', + '{"event":"alarm_rescheduled"}', + '{"event":"alarm_skip_next"}', + '{"event":"alarm_snooze_canceled"}', + '{"event":"alarm_snooze_clicked"}', + '{"event":"antisnoring"}', + '{"event":"apnea_alarm"}', + '{"event":"awake"}', + '{"event":"before_alarm"}', + '{"event":"before_smart_period"}', + '{"event":"deep_sleep"}', + '{"event":"light_sleep"}', + '{"event":"lullaby_start"}', + '{"event":"lullaby_stop"}', + '{"event":"lullaby_volume_down"}', + '{"event":"not_awake"}', + '{"event":"rem"}', + '{"event":"show_skip_next_alarm"}', + '{"event":"sleep_tracking_paused"}', + '{"event":"sleep_tracking_resumed"}', + '{"event":"sleep_tracking_started"}', + '{"event":"sleep_tracking_stopped"}', + '{"event":"smart_period"}', + '{"event":"sound_event_baby"}', + '{"event":"sound_event_cough"}', + '{"event":"sound_event_laugh"}', + '{"event":"sound_event_snore"}', + '{"event":"sound_event_talk"}', + '{"event":"time_to_bed_alarm_alert"}' +] +# State mapping +STATE_MAPPING = { + "unknown": "Unknown", + '{"event":"sleep_tracking_started"}': "Sleep Tracking Started", + '{"event":"sleep_tracking_stopped"}': "Sleep Tracking Stopped", + '{"event":"sleep_tracking_paused"}': "Sleep Tracking Paused", + '{"event":"sleep_tracking_resumed"}': "Sleep Tracking Resumed", + '{"event":"alarm_snooze_clicked"}': "Alarm Snoozed", + '{"event":"alarm_snooze_canceled"}': "Snooze Canceled", + '{"event":"time_to_bed_alarm_alert"}': "Time To Bed Alarm Alert", + '{"event":"alarm_alert_start"}': "Alarm Alert Started", + '{"event":"alarm_alert_dismiss"}': "Alarm Dismissed", + '{"event":"alarm_skip_next"}': "Skip Next Alarm", + '{"event":"show_skip_next_alarm"}': "Show Skip Next Alarm", + '{"event":"rem"}': "REM", + '{"event":"smart_period"}': "Smart Period", + '{"event":"before_smart_period"}': "Before Smart Period", + '{"event":"lullaby_start"}': "Lullaby Start", + '{"event":"lullaby_stop"}': "Lullaby Stop", + '{"event":"lullaby_volume_down"}': "Lullaby Volume Down", + '{"event":"deep_sleep"}': "Deep Sleep", + '{"event":"light_sleep"}': "Light Sleep", + '{"event":"awake"}': "Awake", + '{"event":"not_awake"}': "Not Awake", + '{"event":"apnea_alarm"}': "Apnea Alarm", + '{"event":"antisnoring"}': "Antisnoring", + '{"event":"before_alarm"}': "Before Alarm", + '{"event":"sound_event_snore"}': "Snore Detected", + '{"event":"sound_event_talk"}': "Talk Detected", + '{"event":"sound_event_cough"}': "Cough Detected", + '{"event":"sound_event_baby"}': "Baby Cry Detected", + '{"event":"sound_event_laugh"}': "Laugh Detected", + '{"event":"alarm_rescheduled"}': "Alarm Rescheduled" +} + +# Sound mapping +SOUND_MAPPING = { + '{"event":"sound_event_snore"}': "Snore Detected", + '{"event":"sound_event_talk"}': "Talk Detected", + '{"event":"sound_event_cough"}': "Cough Detected", + '{"event":"sound_event_baby"}': "Baby Cry Detected", + '{"event":"sound_event_laugh"}': "Laugh Detected", +} + +# Disturbance mapping +DISTURBANCE_MAPPING = { + '{"event":"apnea_alarm"}': "Apnea Alarm", + '{"event":"antisnoring"}': "Antisnoring", +} + +# Alarm event mapping +ALARM_EVENT_MAPPING = { + '{"event":"before_alarm"}': "Before Alarm", + '{"event":"alarm_snooze_clicked"}': "Alarm Snoozed", + '{"event":"alarm_snooze_canceled"}': "Snooze Canceled", + '{"event":"time_to_bed_alarm_alert"}': "Time To Bed Alarm Alert", + '{"event":"alarm_alert_start"}': "Alarm Alert Started", + '{"event":"alarm_alert_dismiss"}': "Alarm Dismissed", + '{"event":"alarm_skip_next"}': "Skip Next Alarm", + '{"event":"show_skip_next_alarm"}': "Show Skip Next Alarm", + '{"event":"rem"}': "REM", + '{"event":"smart_period"}': "Smart Period", + '{"event":"before_smart_period"}': "Before Smart Period", +} + +# Sleep tracking event mapping +SLEEP_TRACKING_MAPPING = { + '{"event":"sleep_tracking_started"}': "Sleep Tracking Started", + '{"event":"sleep_tracking_stopped"}': "Sleep Tracking Stopped", + '{"event":"sleep_tracking_paused"}': "Sleep Tracking Paused", + '{"event":"sleep_tracking_resumed"}': "Sleep Tracking Resumed", +} \ No newline at end of file diff --git a/custom_components/saas/manifest.json b/custom_components/saas/manifest.json new file mode 100644 index 0000000..d0e07af --- /dev/null +++ b/custom_components/saas/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "saas", + "name": "SAAS - Sleep As Android Status", + "codeowners": ["@sudoxnym"], + "config_flow": true, + "dependencies": ["mqtt"], + "documentation": "https://www.github.com/sudoxnym/saas", + "iot_class": "local_push", + "issue_tracker": "", + "quality_scale": "silver", + "requirements": ["pyhaversion", "paho-mqtt"], + "version": "0.0.1" +} \ No newline at end of file diff --git a/custom_components/saas/sensor.py b/custom_components/saas/sensor.py new file mode 100644 index 0000000..3ad2a9c --- /dev/null +++ b/custom_components/saas/sensor.py @@ -0,0 +1,377 @@ +import asyncio +import json +from datetime import timedelta +from collections import deque +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.components.mqtt import async_subscribe +from homeassistant.helpers.entity import Entity +from .const import DOMAIN, CONF_NAME, CONF_TOPIC, CONF_AWAKE_STATES, CONF_SLEEP_STATES, CONF_AWAKE_DURATION, CONF_SLEEP_DURATION, INTEGRATION_NAME, MODEL + + +# Define the state mapping directly in the file +state_mapping = { + 'unknown': "Unknown", + '{"event":"sleep_tracking_started"}': "Sleep Tracking Started", + '{"event":"sleep_tracking_stopped"}': "Sleep Tracking Stopped", + '{"event":"sleep_tracking_paused"}': "Sleep Tracking Paused", + '{"event":"sleep_tracking_resumed"}': "Sleep Tracking Resumed", + '{"event":"alarm_snooze_clicked"}': "Alarm Snoozed", + '{"event":"alarm_snooze_canceled"}': "Snooze Canceled", + '{"event":"time_to_bed_alarm_alert"}': "Time To Bed Alarm Alert", + 'alarm_alert_start': "Alarm Alert Started", + 'alarm_alert_dismiss': "Alarm Dismissed", + '{"event":"alarm_skip_next"}': "Skip Next Alarm", + '{"event":"show_skip_next_alarm"}': "Show Skip Next Alarm", + '{"event":"rem"}': "REM", + '{"event":"smart_period"}': "Smart Period", + '{"event":"before_smart_period"}': "Before Smart Period", + '{"event":"lullaby_start"}': "Lullaby Start", + '{"event":"lullaby_stop"}': "Lullaby Stop", + '{"event":"lullaby_volume_down"}': "Lullaby Volume Down", + '{"event":"deep_sleep"}': "Deep Sleep", + '{"event":"light_sleep"}': "Light Sleep", + '{"event":"awake"}': "Awake", + '{"event":"not_awake"}': "Not Awake", + '{"event":"apnea_alarm"}': "Apnea Alarm", + '{"event":"antisnoring"}': "Antisnoring", + '{"event":"before_alarm"}': "Before Alarm", + '{"event":"sound_event_snore"}': "Snore Detected", + '{"event":"sound_event_talk"}': "Talk Detected", + '{"event":"sound_event_cough"}': "Cough Detected", + '{"event":"sound_event_baby"}': "Baby Cry Detected", + '{"event":"sound_event_laugh"}': "Laugh Detected", + '{"event":"alarm_rescheduled"}': "Alarm Rescheduled" +} + +# Define the sound mapping directly in the file +sound_mapping = { + '{"event":"sound_event_snore"}': "Snore Detected", + '{"event":"sound_event_talk"}': "Talk Detected", + '{"event":"sound_event_cough"}': "Cough Detected", + '{"event":"sound_event_baby"}': "Baby Cry Detected", + '{"event":"sound_event_laugh"}': "Laugh Detected", +} + +disturbance_mapping = { + '{"event":"apnea_alarm"}': "Apnea Alarm", + '{"event":"antisnoring"}': "Antisnoring", +} + +# Define the alarm event mapping directly in the file +alarm_event_mapping = { + '{"event":"before_alarm"}': "Before Alarm", + '{"event":"alarm_snooze_clicked"}': "Alarm Snoozed", + '{"event":"alarm_snooze_canceled"}': "Snooze Canceled", + '{"event":"time_to_bed_alarm_alert"}': "Time To Bed Alarm Alert", + '{"event":"alarm_alert_start"}': "Alarm Alert Started", + '{"event":"alarm_alert_dismiss"}': "Alarm Dismissed", + '{"event":"alarm_skip_next"}': "Skip Next Alarm", + '{"event":"show_skip_next_alarm"}': "Show Skip Next Alarm", + '{"event":"rem"}': "REM", + '{"event":"smart_period"}': "Smart Period", + '{"event":"before_smart_period"}': "Before Smart Period", +} + +# Define the sleep tracking event mapping directly in the file +sleep_tracking_mapping = { + '{"event":"sleep_tracking_started"}': "Sleep Tracking Started", + '{"event":"sleep_tracking_stopped"}': "Sleep Tracking Stopped", + '{"event":"sleep_tracking_paused"}': "Sleep Tracking Paused", + '{"event":"sleep_tracking_resumed"}': "Sleep Tracking Resumed", +} + +# Define the wake status event mapping directly in the file +wake_status_mapping = { + '{"event":"awake"}': "Awake", + '{"event":"not_awake"}': "Not Awake", +} + +class SAASSensor(Entity): + async def async_setup(self): + """Run when the entity is about to be added.""" + pass + """Representation of a SAAS - Sleep As Android Stats sensor.""" + def __init__(self, hass, name, mapping): + """Initialize the sensor.""" + self._state = None + self._name = name + self._hass = hass + self._mapping = mapping + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_sensor_{self._name}" + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} State" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_info(self): + """Return information about the device.""" + return { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + async def message_received(msg): + """Handle new MQTT messages.""" + # Use the mapping to convert the MQTT payload to the corresponding state + new_state = self._mapping.get(msg.payload) + if new_state is not None: + self._state = new_state + self.async_schedule_update_ha_state() + + await async_subscribe(self._hass, self._hass.data[DOMAIN]["topic_template"], message_received) + +class SAASSoundSensor(SAASSensor): + """Representation of a SAAS - Sleep As Android Stats sound sensor.""" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_sound_sensor_{self._name}" + + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} Sound" + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + async def message_received(msg): + """Handle new MQTT messages.""" + # Use the mapping to convert the MQTT payload to the corresponding state + new_state = self._mapping.get(msg.payload, "None") + self._state = new_state + self.async_schedule_update_ha_state() + + await async_subscribe(self._hass, self._hass.data[DOMAIN]["topic_template"], message_received) + +class SAASDisturbanceSensor(SAASSensor): + """Representation of a SAAS - Sleep As Android Stats disturbance sensor.""" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_disturbance_sensor_{self._name}" + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} Disturbance" + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + async def message_received(msg): + """Handle new MQTT messages.""" + # Use the mapping to convert the MQTT payload to the corresponding state + new_state = self._mapping.get(msg.payload, "None") + self._state = new_state + self.async_schedule_update_ha_state() + + await async_subscribe(self._hass, self._hass.data[DOMAIN]["topic_template"], message_received) + +class SAASAlarmEventSensor(SAASSensor): + """Representation of a SAAS - Sleep As Android Stats alarm event sensor.""" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_alarm_event_sensor_{self._name}" + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} Alarm Event" + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + async def message_received(msg): + """Handle new MQTT messages.""" + # Try to parse the payload as JSON + try: + payload = json.loads(msg.payload) + except json.JSONDecodeError: + # If it's not JSON, use the raw payload + payload = msg.payload + + # Use the mapping to convert the MQTT payload to the corresponding state + new_state = self._mapping.get(payload) + if new_state is not None: + self._state = new_state + self.async_schedule_update_ha_state() + + await async_subscribe(self._hass, self._hass.data[DOMAIN]["topic_template"], message_received) + +class SAASSleepTrackingSensor(SAASSensor): + """Representation of a SAAS - Sleep As Android Stats sleep tracking sensor.""" + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_sleep_tracking_sensor_{self._name}" + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} Sleep Tracking" + + async def async_added_to_hass(self): + """Run when entity about to be added.""" + await super().async_added_to_hass() + + async def message_received(msg): + """Handle new MQTT messages.""" + # Use the mapping to convert the MQTT payload to the corresponding state + new_state = self._mapping.get(msg.payload) + if new_state is not None: + self._state = new_state + self.async_schedule_update_ha_state() + + await async_subscribe(self._hass, self._hass.data[DOMAIN]["topic_template"], message_received) + +class SAASWakeStatusSensor(Entity): + def __init__(self, hass, name, topic, awake_states, sleep_states, awake_duration, sleep_duration): + self._state = None + self._name = name + self._topic = topic + self._awake_states = awake_states + self._sleep_states = sleep_states + self._awake_duration = timedelta(minutes=awake_duration) + self._sleep_duration = timedelta(minutes=sleep_duration) + self._message_timestamps = deque() + self._message_states = deque() + + async_track_time_interval(hass, self._async_update_state, timedelta(seconds=10)) + + async def some_async_function(self): + await async_subscribe(self.hass, self._topic, self._message_received) + + async_subscribe(hass, self._topic, self._message_received) + + + + @property + def device_info(self): + """Return information about the device.""" + return { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"saas_wake_status_sensor_{self._name}" + + @property + def name(self): + """Return the name of the sensor.""" + return f"SAAS {self._name} Wake Status" + + @property + def state(self): + return self._state + + async def _message_received(self, msg): + now = datetime.now() + self._message_timestamps.append(now) + self._message_states.append(msg) + + # Remove messages older than sleep_duration + while self._message_timestamps and now - self._message_timestamps[0] > self._sleep_duration: + self._message_timestamps.popleft() + self._message_states.popleft() + + + + async def _async_update_state(self, now): + # Check if the only messages in the last awake_duration are awake_states + for timestamp, state in zip(self._message_timestamps, self._message_states): + if now - timestamp <= self._awake_duration and state not in self._awake_states: + break + else: + self._state = "Awake" + return + + # Check if the only messages in the last sleep_duration are sleep_states + for state in self._message_states: + if state not in self._sleep_states: + break + else: + self._state = "Asleep" + return + + self._state = "Unknown" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the SAAS sensor platform.""" + name = hass.data[DOMAIN].get(CONF_NAME, "Default Name") + topic = hass.data[DOMAIN].get(CONF_TOPIC) + awake_states = hass.data[DOMAIN].get(CONF_AWAKE_STATES) + sleep_states = hass.data[DOMAIN].get(CONF_SLEEP_STATES) + awake_duration = hass.data[DOMAIN].get(CONF_AWAKE_DURATION) + sleep_duration = hass.data[DOMAIN].get(CONF_SLEEP_DURATION) + + entities = [ + SAASSensor(hass, name, state_mapping), + SAASSoundSensor(hass, name, sound_mapping), + SAASDisturbanceSensor(hass, name, disturbance_mapping), + SAASAlarmEventSensor(hass, name, alarm_event_mapping), + SAASSleepTrackingSensor(hass, name, sleep_tracking_mapping), + SAASWakeStatusSensor(hass, name, topic, awake_states, sleep_states, awake_duration, sleep_duration) + ] + + for entity in entities: + if hasattr(entity, "async_setup"): + await entity.async_setup() + + async_add_entities(entities) + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the SAAS sensor platform from a config entry.""" + name = entry.data.get(CONF_NAME, "Default Name") + topic = entry.data.get(CONF_TOPIC) + awake_states = entry.data.get(CONF_AWAKE_STATES) + sleep_states = entry.data.get(CONF_SLEEP_STATES) + awake_duration = entry.data.get(CONF_AWAKE_DURATION) + sleep_duration = entry.data.get(CONF_SLEEP_DURATION) + + entities = [ + SAASSensor(hass, name, state_mapping), + SAASSoundSensor(hass, name, sound_mapping), + SAASDisturbanceSensor(hass, name, disturbance_mapping), + SAASAlarmEventSensor(hass, name, alarm_event_mapping), + SAASSleepTrackingSensor(hass, name, sleep_tracking_mapping), + SAASWakeStatusSensor(hass, name, topic, awake_states, sleep_states, awake_duration, sleep_duration) + ] + + for entity in entities: + if hasattr(entity, "async_setup"): + await entity.async_setup() + + async_add_entities(entities) \ No newline at end of file