Compare commits

..

18 commits
0.1.1 ... main

Author SHA1 Message Date
sudoxnym
e2e35f8f20 deprecate in favor of sleepd 2025-12-07 08:05:27 -06:00
sudoxnym
0cbe7ba589
Merge pull request #12 from sudoxnym/codex/update-readme-with-new-sensor
Update README with next alarm sensor
2025-06-28 22:45:28 -06:00
sudoxnym
e4b0fa0375 docs: document next alarm sensor 2025-06-28 22:43:57 -06:00
sudoxnym
45767967a4
Add files via upload 2025-06-28 22:28:11 -06:00
sudoxnym
7c359a4b6e
Merge pull request #10 from sudoxnym/codex/add-sensor-entity-with-alarm-and-label
Add next alarm sensor
2025-06-28 21:43:30 -06:00
sudoxnym
23f5d5dd18 Add next alarm sensor 2025-06-28 21:40:21 -06:00
sudoxnym
5f3a7e78f5
Update README.md 2025-05-27 15:57:09 -06:00
sudoxnym
359dd530fe
Update README.md 2025-05-05 09:03:04 -06:00
sudoxnym
6de713d0c6
Update README.md 2025-05-03 14:50:35 -06:00
sudoxnym
6dc8fa9afa
Update manifest.json 2025-05-03 14:47:06 -06:00
sudoxnym
f5161159d7
Add files via upload 2025-05-03 14:39:55 -06:00
sudoxnym
ccbdd154f2
Update README.md 2025-05-03 14:39:19 -06:00
sudoxnym
3f7c73ab46
Update manifest.json 2025-05-03 10:27:47 -06:00
sudoxnym
3a41318279
Update manifest.json 2025-04-25 23:01:16 -06:00
sudoxnym
1f52692d07
Update README.md 2025-04-15 12:37:36 -06:00
sudoxnym
e94fe3a72b
BREAKING CHANGES!!!!!
BREAKING CHANGES!!!!!
YOU WILL HAVE TO REMOVE YOUR ORIGINAL ENTRIES AND READD THEM
2025-04-14 18:43:09 -06:00
sudoxnym
f8f8c7927d
Add files via upload 2025-04-14 18:17:05 -06:00
sudoxnym
71d8c6dd2b
Add files via upload
WOO Fixed the ghost error!
2025-04-14 17:42:58 -06:00
8 changed files with 521 additions and 275 deletions

232
README.md
View file

@ -1,55 +1,78 @@
<p align="center">
<img src="https://img.shields.io/badge/⚠_DEPRECATED-red?style=for-the-badge" alt="deprecated">
</p>
# ⚠️ THIS PROJECT HAS BEEN DEPRECATED
**this project has been rebranded and moved to [sleepd](https://github.com/sudoxnym/sleepd)**
please use the new repository for future updates and support.
[![go to sleepd](https://img.shields.io/badge/GO_TO_SLEEPD-blue?style=for-the-badge&logo=github)](https://github.com/sudoxnym/sleepd)
---
<details>
<summary>original readme (archived)</summary>
<p align="center"> <p align="center">
<a href="https://hacs.xyz/docs/faq/custom_repositories"> <a href="https://hacs.xyz/docs/faq/custom_repositories">
<img src="https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge&logo=home%20assistant&labelColor=202020&color=41BDF5" alt="Add to HACS"> <img src="https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge&logo=home%20assistant&labelColor=202020&color=41BDF5" alt="add to hacs">
</a> </a>
</p> </p>
<h1>🌙 SAAS - Sleep As Android Status</h1> <h1>🌙 saas - Sleep As Android status</h1>
<h2>📖 Description:</h2> <h2>🚨 0.2.0 breaking changes 🚨</h2>
due to changes in **Home Assistant** 2025.12, you **must** remove your existing **SAAS** integration entries and re-add them after updating to this version.
---
<h2>📖 description:</h2>
<p> <p>
Sleep As Android Status is my solution for wake/sleep state within HA. It listens for the Sleep As Android MQTT Messages, so it does require being on the same network. As of 0.0.4 Buttons that link with the Companion app have been added. sleep as android status is my solution for wake/sleep state within HA. it listens for the Sleep As Android MQTT messages, so it does require being on the same network. as of 0.0.4, buttons that link with the companion app have been added.
</p> </p>
<h4>💡 This integration works best with a Xioami MiBand (7 or older) mixed with the Notify app and Sleep As Android configured.</h4> <h3>🧱 this integration will create:</h3>
<h3>🧱 This integration will create:</h3>
<ul> <ul>
<li> <li>
<details> <details>
<summary><strong>📡 8 Sensors</strong></summary> <summary><strong>📡 9 sensors</strong></summary>
<ul>
<li>Message Received *State</li> | Sensor | Description |
<li>Wake Status</li> | ------ | ----------- |
<li>Sound</li> | message received | shows the last raw MQTT event payload |
<li>Disturbance</li> | wake status | indicates awake or asleep based on your sleep stage |
<li>Alarm</li> | sound | snore, talk, cough, and other sound events |
<li>Lullaby</li> | disturbance | reports apnea and antisnoring events |
<li>Sleep Tracking</li> | **next alarm** | upcoming alarm time and label; stores the last ten alarms in attributes |
<li>Sleep Stage</li> | alarm | alarm related events such as snooze or dismiss |
</ul> | lullaby | lullaby status |
<p>This should intelligently and dynamically allow for state changes in the Wake Status Sensor.</p> | sleep tracking | whether sleep tracking is active or paused |
| sleep stage | current sleep stage from Sleep As Android |
<p>the wake status sensor adjusts automatically based on the defined awake and asleep states.</p>
</details> </details>
</li> </li>
<li> <li>
<details> <details>
<summary><strong>🎛️ 8 Buttons</strong></summary> <summary><strong>🎛️ 8 buttons *only if mobile_app selected</strong></summary>
<ul> <ul>
<li>Alarm Dismiss</li> <li>alarm dismiss</li>
<li>Alarm Snooze</li> <li>alarm snooze</li>
<li>Lullaby Stop</li> <li>lullaby stop</li>
<li>Sleep Tracking Pause</li> <li>sleep tracking pause</li>
<li>Sleep Tracking Resume</li> <li>sleep tracking resume</li>
<li>Sleep Tracking Start</li> <li>sleep tracking start</li>
<li>Sleep Tracking Start with Optimal Alarm</li> <li>sleep tracking start with optimal alarm</li>
<li>Sleep Tracking Stop</li> <li>sleep tracking stop</li>
</ul> </ul>
</details> </details>
</li> </li>
<li> <li>
<details> <details>
<summary><strong>🛠️ 1 Service</strong></summary> <summary><strong>🛠️ 1 service</strong></summary>
<pre> <pre>
service: saas.saas_example_alarm_set service: saas.saas_example_alarm_set
data: data:
@ -62,90 +85,89 @@ data:
</li> </li>
<li> <li>
<details> <details>
<summary><strong>🔗 1 Device per user</strong></summary> <summary><strong>🔗 1 device per user</strong></summary>
<p>One HA device is created per configured user instance to link sensors, services, and buttons.</p> <p>one HA device is created per configured user instance to link sensors, services, and buttons.</p>
</details> </details>
</li> </li>
</ul> </ul>
<details> <details>
<summary><strong>Known working</strong></summary> <summary><strong>known working</strong></summary>
<ul> <ul>
<li>📟 Xioami Mi Band 7</li> <li>📟 **Xiaomi Mi Band 7**</li>
<li>📟 Xioami Mi Band 8 and 9 may work, but they have a different OS that jumps through hoops to work.</li> <li>📟 **Xiaomi Mi Band 8** and **Mi Band 9** may work, but they have a different os that jumps through hoops to work.</li>
<li>⌚ Garmin Fenix 7X with Garmin Alternative, <b>NOT</b> the free one.</li> <li>⌚ **Garmin Fenix 7X** with garmin alternative, **not** the free one.</li>
<li>⌚ Xioami Amazfit GTR Mini — may require root. I am rooted so I just did what's in this guide, but there may be alternative ways to get the key.</li> <li>⌚ **Xiaomi Amazfit GTR3 Pro** — may require root. i am rooted so i just did what's in this guide, but there may be alternative ways to get the key.</li>
</ul> </ul>
</details> </details>
<h2>🧪 Installation:</h2> <h2>🧪 installation:</h2>
<ul> <ul>
<li>Add https://www.github.com/sudoxnym/saas to your Custom Repositories in HACS</li> <li>add <code>https://www.github.com/sudoxnym/saas</code> to your custom repositories in HACS</li>
<li>Search and Download SAAS - Sleep As Android Status</li> <li>search and download **SAAS - Sleep As Android status**</li>
<li>Restart Home Assistant</li> <li>restart Home Assistant</li>
<li> <li>
<a href="https://my.home-assistant.io/redirect/config_flow_start/?domain=saas"> <a href="https://my.home-assistant.io/redirect/config_flow_start/?domain=saas">
<img src="https://my.home-assistant.io/badges/config_flow_start.svg" alt="Add to HA"> <img src="https://my.home-assistant.io/badges/config_flow_start.svg" alt="add to ha">
</a> </a>
</li> </li>
<li>Add Integration: SAAS - Sleep As Android Status</li> <li>add integration: **SAAS - Sleep As Android status**</li>
</ul> </ul>
<h2>⚙️ Configuration:</h2> <h2>⚙️ configuration:</h2>
<ul> <ul>
<li>Name: Name of user</li> <li>name: name of user</li>
<li>Topic: MQTT Topic from Sleep As Android</li> <li>topic: MQTT topic from Sleep As Android *MUST MATCH*</li>
<li>QoS: Quality of Service</li> <li>awake duration: time in seconds in which awake states = true to indicate awake. <b>fixed</b></li>
<li>Awake Duration: Time in seconds in which awake states = true to indicate awake. <s>Sensor usually updates within 30 seconds or so after the duration, not entirely sure why the delay.</s> <b>FIXED</b></li> <li>asleep duration: time in seconds in which sleep states = true to indicate asleep. <b>fixed</b></li>
<li>Asleep Duration: Time in seconds in which sleep states = true to indicate asleep. <s>Sensor usually updates within 30 seconds or so after the duration, not entirely sure why the delay.</s> <b>FIXED</b></li> <li>awake states: states to indicate being awake</li>
<li>Awake States: States to indicate being awake</li> <li>asleep states: states to indicate being asleep</li>
<li>Asleep States: States to indicate being asleep</li> <li>mobile app: target for buttons <b>requires companion app *OPTIONAL: REQUIRES COMPANION APP*</b></li>
<li>Mobile App: Target for buttons <b>REQUIRES COMPANION APP</b></li>
</ul> </ul>
<details> <details>
<summary><strong>📲 Set Up Notify for Mi Band 7</strong></summary> <summary><strong>📲 set up Notify for Mi Band 7</strong></summary>
<ol> <ol>
<li>Pair MiBand 7 as you normally would with <a href="https://play.google.com/store/apps/details?id=com.xiaomi.wearable&hl=en_US">Mi Fitness</a></li> <li>pair **Mi Band 7** as you normally would with <a href="https://play.google.com/store/apps/details?id=com.xiaomi.wearable&hl=en_US">Mi Fitness</a></li>
<li>Obtain auth key for Notify app using ADB</li> <li>obtain auth key for Notify app using ADB</li>
</ol> </ol>
<pre> <pre>
adb shell adb shell
grep -E "authKey=[a-z0-9]*," /sdcard/Android/data/com.xiaomi.wearable/files/log/XiaomiFit.device.log | grep -E "authKey=[a-z0-9]*," /sdcard/Android/data/com.xiaomi.wearable/files/log/XiaomiFit.device.log |
awk -F ", " '{print $17}' | grep authKey | tail -1 | awk -F "=" '{print $2}' awk -F ", " '{print $17}' | grep authKey | tail -1 | awk -F "=" '{print $2}'
</pre> </pre>
<p>Credit: <a href="https://www.reddit.com/r/miband/comments/15j0rfq/comment/kxlyzc6/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button">iamfosscad</a></p> <p>credit: <a href="https://www.reddit.com/r/miband/comments/15j0rfq/comment/kxlyzc6/">iamfosscad</a></p>
<ol start="3"> <ol start="3">
<li>Uninstall Mi Fitness</li> <li>uninstall **Mi Fitness**</li>
<li>Download/Install <a href="https://play.google.com/store/apps/details?id=com.mc.miband1&hl=en_US">Notify for Mi Band</a></li> <li>download/install <a href="https://play.google.com/store/apps/details?id=com.mc.miband1&hl=en_US">Notify for Mi Band</a></li>
<li>Follow prompts, input auth key, select Mi Fitness is not installed</li> <li>follow prompts, input auth key, select Mi Fitness is not installed</li>
<li>Enable Sleep as Android in Notify settings</li> <li>enable Sleep As Android in Notify settings</li>
</ol> </ol>
</details> </details>
<details> <details>
<summary><strong>🔐 Extracting the Zepp <code>authKey</code> on a Rooted Android Device</strong></summary> <summary><strong>🔐 extracting the Zepp <code>authKey</code> on a rooted android device</strong></summary>
<pre> <pre>
su su
cd /data/data/com.huami.watch.hmwatchmanager/databases/ cd /data/data/com.huami.watch.hmwatchmanager/databases/
ls origin_db_* ls origin_db_*
sqlite3 origin_db_1234567890 "SELECT AUTHKEY FROM DEVICE;" sqlite3 origin_db_1234567890 "SELECT AUTHKEY FROM DEVICE;"
</pre> </pre>
<ul> <ul>
<li>⚠️ Do Not Unpair before extracting</li> <li>⚠️ do not unpair before extracting</li>
<li>Use with caution root required</li> <li>use with caution root required</li>
<li>Modified apps are available on <a href="https://geekdoing.com">GeekDoing</a> and <a href="https://freemyband.com">freemyband.com</a></li> <li>modified apps are available on <a href="https://geekdoing.com">GeekDoing</a> and <a href="https://freemyband.com">FreeMyBand</a></li>
</ul> </ul>
</details> </details>
<h3>🛌 <a href="https://play.google.com/store/apps/details?id=com.urbandroid.sleep&hl=en_US">Sleep as Android Setup</a></h3> <h3>🛌 <a href="https://play.google.com/store/apps/details?id=com.urbandroid.sleep&hl=en_US">sleep as android setup</a></h3>
<ol> <ol>
<li>Open the app and follow setup</li> <li>open the app and follow setup</li>
<li>Settings wheel > Services > Automation > MQTT</li> <li>settings wheel > services > automation > MQTT</li>
</ol> </ol>
<pre> <pre>
@ -153,42 +175,54 @@ sqlite3 origin_db_1234567890 "SELECT AUTHKEY FROM DEVICE;"
</pre> </pre>
<ul> <ul>
<li>Topic: must match config</li> <li>topic: must match config</li>
<li>Client ID: any unique ID</li> <li>client id: any unique id</li>
</ul> </ul>
<ol start="4"> <ol start="4">
<li>Enable automatic tracking</li> <li>enable automatic tracking</li>
<li>Sensor: Sonar or Accelerometer</li> <li>sensor: sonar or accelerometer</li>
<li>Wearables > Xiaomi Mi Band > Test sensor</li> <li>wearables > **Xiaomi Mi Band** > test sensor</li>
</ol> </ol>
<details> <details>
<summary><strong>📦 Changes</strong></summary> <summary><strong>📦 changes</strong></summary>
<b>0.0.6a</b> <b>0.2.2</b>
<ul> <ul>
<li>Initial Beta Release</li> <li>added Next Alarm sensor with alarm label tracking</li>
<li>Added persistent states</li> <li>stores up to ten previous alarms in sensor attributes</li>
<li>Alarm Event sensor attributes</li> </ul>
<b>0.2.1</b>
<ul>
<li>fixed manifest error preventing config setup</li>
<li>fixed fine tuning in the configure section, now changing time or device actually works</li>
</ul>
<b>0.2.0</b>
<ul>
<li>added services.yaml to resolve known NoneType error</li>
<li>fixed deprecation warnings for future Home Assistant releases</li>
<li>breaking changes: remove and re-add existing integration entries after update</li>
</ul> </ul>
<b>0.1.0</b> <b>0.1.0</b>
<ul> <ul>
<li>Fixed Wake Status Timing</li> <li>fixed wake status timing</li>
<li>Bug fixes on Sound sensor</li> <li>bug fixes on sound sensor</li>
<li>Accurate updates to AlarmEvent, Disturbance, Sound</li> <li>accurate updates to alarmevent, disturbance, sound</li>
<li>Organized README</li> <li>organized readme</li>
</ul>
<b>0.0.6a</b>
<ul>
<li>initial beta release</li>
<li>added persistent states</li>
<li>alarm event sensor attributes</li>
</ul> </ul>
</details> </details>
<details> <details>
<summary><strong>🚨 Known Issues</strong></summary> <summary><strong>🚨 known issues</strong></summary>
<pre> <p>💬 no known issues at this time.</p>
Logger: homeassistant.helpers.service </details>
Source: /usr/src/homeassistant/homeassistant/helpers/service.py:708
Failed to load integration: saas
NoneType: None
</pre>
<p>💬 No known effects. Just an error message, everything works as expected.</p>
<p>This is my first integration.</p>
</details> </details>

View file

@ -3,60 +3,55 @@ import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from .const import DOMAIN
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .services import async_setup_services # Import the service setup function from .services import async_setup_services # Import the service setup function
_logger = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the SAAS - Sleep As Android Status component.""" """Set up the SAAS - Sleep As Android Status component."""
_logger.info("Starting setup of the SAAS component") _LOGGER.info("Starting setup of the SAAS component")
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
_logger.info(f"Starting setup of config entry with ID: {entry.entry_id}") _LOGGER.info(f"Starting setup of config entry with ID: {entry.entry_id}")
# ensure we have a dict for this integration
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
if entry.entry_id not in hass.data[DOMAIN] and entry.data: # merge original data + any saved options so runtime sees edits
hass.data[DOMAIN][entry.entry_id] = entry.data merged = {**entry.data, **entry.options}
hass.data[DOMAIN][entry.entry_id] = merged
_LOGGER.debug(
"Merged entry.data and entry.options for %s: %s",
entry.entry_id,
hass.data[DOMAIN][entry.entry_id],
)
_logger.info(f"hass.data[DOMAIN] after adding entry data: {hass.data[DOMAIN]}") # forward setup to sensor and button platforms
# Forward the setup to the sensor and button platforms using the new method.
# Note: async_forward_entry_setups must always be awaited.
await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "button"]) await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "button"])
_logger.info(f"hass.data[DOMAIN] before async_setup_services: {hass.data[DOMAIN]}") # set up any custom services
_LOGGER.info("Starting setup of services")
# Setup the services
_logger.info("Starting setup of services")
await async_setup_services(hass) await async_setup_services(hass)
_logger.info("Finished setup of services") _LOGGER.info("Finished setup of services")
_logger.info(f"hass.data[DOMAIN] after setup of services: {hass.data[DOMAIN]}") _LOGGER.info("Finished setup of config entry")
async def reload_entry():
_logger.info("Reloading entry")
await hass.config_entries.async_reload(entry.entry_id)
async_dispatcher_connect(hass, f"{DOMAIN}_reload_{entry.entry_id}", reload_entry)
_logger.info("Finished setup of config entry")
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
_logger.info(f"Starting unload of config entry with ID: {entry.entry_id}") _LOGGER.info(f"Starting unload of config entry with ID: {entry.entry_id}")
# Remove the sensor platform # unload sensor and button platforms
_logger.info("Removing sensor platform") unload_ok = await hass.config_entries.async_unload_platforms(entry, ["sensor", "button"])
await hass.config_entries.async_forward_entry_unload(entry, "sensor") if not unload_ok:
_LOGGER.error("Failed to unload platforms for saas")
return False
# Ensure hass.data[DOMAIN] is a dictionary before popping # clean up our stored data
if isinstance(hass.data.get(DOMAIN, {}), dict): hass.data[DOMAIN].pop(entry.entry_id, None)
hass.data[DOMAIN].pop(entry.entry_id, None)
_logger.info(f"hass.data[DOMAIN] after removing entry data: {hass.data[DOMAIN]}") _LOGGER.info(f"hass.data[{DOMAIN}] after unload: {hass.data.get(DOMAIN)}")
_logger.info("Finished unload of config entry") _LOGGER.info("Finished unload of config entry")
return True return True

View file

@ -1,7 +1,7 @@
import logging import logging
from homeassistant.components.button import ButtonEntity from homeassistant.components.button import ButtonEntity
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, INTEGRATION_NAME, MODEL, CONF_NAME from .const import DOMAIN, INTEGRATION_NAME, MODEL, CONF_NAME, CONF_NOTIFY_TARGET
import asyncio import asyncio
# Set up logging # Set up logging
@ -50,6 +50,12 @@ class SAASSleepTrackingStart(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -117,6 +123,12 @@ class SAASSleepTrackingStop(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -184,6 +196,12 @@ class SAASSleepTrackingPause(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -251,6 +269,12 @@ class SAASSleepTrackingResume(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -318,6 +342,12 @@ class SAASAlarmClockSnooze(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -385,6 +415,12 @@ class SAASAlarmClockDisable(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -452,6 +488,12 @@ class SAASSleepTrackingStartWithAlarm(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -519,6 +561,12 @@ class SAASLullabyStop(ButtonEntity):
return device_info return device_info
def press(self): def press(self):
if not self._notify_target:
self._hass.components.persistent_notification.async_create(
"add a mobile device to use this function",
title=self.name,
)
return
"""Press the button.""" """Press the button."""
service_name = self._notify_target # Remove the "notify." prefix service_name = self._notify_target # Remove the "notify." prefix
@ -546,12 +594,15 @@ class SAASLullabyStop(ButtonEntity):
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the SAAS Sleep Tracking Start, Stop and Pause buttons from a config entry.""" notify_target = config_entry.data.get(CONF_NOTIFY_TARGET)
if not notify_target:
_LOGGER.warning("no notify_target configured; skipping button setup")
return
# _LOGGER.debug("Setting up SAAS Sleep Tracking buttons from a config entry with data: %s", config_entry.data) # _LOGGER.debug("Setting up SAAS Sleep Tracking buttons from a config entry with data: %s", config_entry.data)
# Extract the necessary data from config_entry.data # Extract the necessary data from config_entry.data
name = config_entry.data[CONF_NAME] name = config_entry.data[CONF_NAME]
notify_target = config_entry.data['notify_target'] notify_target = config_entry.data.get(CONF_NOTIFY_TARGET)
# Create instances of SAASSleepTrackingStart, SAASSleepTrackingStop and SAASSleepTrackingPause # Create instances of SAASSleepTrackingStart, SAASSleepTrackingStop and SAASSleepTrackingPause
entities = [ entities = [

View file

@ -1,139 +1,184 @@
import logging import logging
import traceback
import voluptuous as vol import voluptuous as vol
from .const import DOMAIN, CONF_NAME, CONF_TOPIC, CONF_QOS, AVAILABLE_STATES, CONF_AWAKE_DURATION, CONF_SLEEP_DURATION, CONF_AWAKE_STATES, CONF_SLEEP_STATES, DEFAULT_AWAKE_DURATION, DEFAULT_SLEEP_DURATION, DEFAULT_AWAKE_STATES, DEFAULT_SLEEP_STATES, CONF_NOTIFY_TARGET from .const import (
DOMAIN,
CONF_NAME,
CONF_TOPIC,
AVAILABLE_STATES,
CONF_AWAKE_DURATION,
CONF_SLEEP_DURATION,
CONF_AWAKE_STATES,
CONF_SLEEP_STATES,
DEFAULT_AWAKE_DURATION,
DEFAULT_SLEEP_DURATION,
DEFAULT_AWAKE_STATES,
DEFAULT_SLEEP_STATES,
CONF_NOTIFY_TARGET,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.core import callback from voluptuous import Schema, Required, Optional, In
from voluptuous import Schema, Required, In, Optional
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
_logger = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 """Handle the initial config flow for SAAS."""
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user.""" """Initial setup step."""
errors = {} errors = {}
# Get the list of notify targets # discover available mobile_app notify services
notify_services = self.hass.services.async_services().get('notify', {}) notify_services = self.hass.services.async_services().get("notify", {})
notify_targets = {target.replace('mobile_app_', '').title(): target for target in notify_services.keys() if target.startswith('mobile_app_')} notify_targets = {
svc.replace("mobile_app_", "")
.replace("_", " ")
.lower(): svc
for svc in notify_services
if svc.startswith("mobile_app_")
}
if user_input is not None: if user_input is not None:
# Map the selected option back to the actual notify target name # map chosen label back to service name, or remove if invalid
user_input[CONF_NOTIFY_TARGET] = notify_targets[user_input[CONF_NOTIFY_TARGET]] nt_label = user_input.get(CONF_NOTIFY_TARGET)
if nt_label in notify_targets:
user_input[CONF_NOTIFY_TARGET] = notify_targets[nt_label]
else:
user_input.pop(CONF_NOTIFY_TARGET, None)
# Validate the user input here # basic validation
# If the input is valid, create an entry if not user_input.get(CONF_NAME):
# If the input is not valid, add an error message to the 'errors' dictionary
# For example:
if not user_input[CONF_NAME]:
errors[CONF_NAME] = "required" errors[CONF_NAME] = "required"
if not errors: if not errors:
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) return self.async_create_entry(
title=user_input[CONF_NAME],
data=user_input
)
# build initial form schema
schema = {
Required(CONF_NAME): str,
Required(CONF_TOPIC): str,
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(AVAILABLE_STATES),
Required(
CONF_SLEEP_STATES, default=DEFAULT_SLEEP_STATES
): cv.multi_select(AVAILABLE_STATES),
}
if notify_targets:
# truly optional, only real targets, no blank choice
schema[Optional(CONF_NOTIFY_TARGET)] = In(list(notify_targets.keys()))
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=Schema( data_schema=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(AVAILABLE_STATES),
Required(CONF_SLEEP_STATES, default=DEFAULT_SLEEP_STATES): cv.multi_select(AVAILABLE_STATES),
Optional(CONF_NOTIFY_TARGET): vol.In(list(notify_targets.keys())),
}
),
errors=errors, errors=errors,
) )
@staticmethod @staticmethod
@callback @config_entries.callback
def async_get_options_flow(config_entry): def async_get_options_flow(entry):
"""Get the options flow for this handler.""" """Return options flow handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(entry)
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options.""" """Handle SAAS options editing."""
def __init__(self, config_entry):
"""Initialize options flow.""" def __init__(self, entry):
self.config_entry = config_entry super().__init__()
self._config_entry = entry
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage the options.""" """Manage the options form (edit)."""
_logger.debug("Entering async_step_init with user_input: %s", user_input) current = dict(self._config_entry.data)
errors = {} # Define errors here # discover mobile_app notify services again
notify_services = self.hass.services.async_services().get("notify", {})
notify_targets = {
svc.replace("mobile_app_", "")
.replace("_", " ")
.lower(): svc
for svc in notify_services
if svc.startswith("mobile_app_")
}
# reverse map for defaults
reverse_map = {v: k for k, v in notify_targets.items()}
try: if user_input is not None:
# Fetch the initial configuration data new_data = current.copy()
current_data = self.hass.data[DOMAIN].get(self.config_entry.entry_id, self.config_entry.options)
_logger.debug("Current data fetched: %s", current_data)
# Get the list of notify targets # standard fields
notify_services = self.hass.services.async_services().get('notify', {}) for key in (
notify_targets = {target.replace('mobile_app_', '').title(): target for target in notify_services.keys() if target.startswith('mobile_app_')} CONF_NAME,
CONF_TOPIC,
CONF_AWAKE_DURATION,
CONF_SLEEP_DURATION,
CONF_AWAKE_STATES,
CONF_SLEEP_STATES,
):
if key in user_input:
new_data[key] = user_input[key]
if user_input is not None: # handle notify_target with "no mobile" option
# Validate the user input here sel = user_input.get(CONF_NOTIFY_TARGET)
# If the input is valid, create an entry if sel == "no mobile":
# If the input is not valid, add an error message to the 'errors' dictionary new_data.pop(CONF_NOTIFY_TARGET, None)
# For example: elif sel in notify_targets:
if not user_input[CONF_NAME]: new_data[CONF_NOTIFY_TARGET] = notify_targets[sel]
errors[CONF_NAME] = "required"
if errors:
return self.async_show_form(step_id="init", data_schema=self.get_data_schema(current_data), errors=errors) # Pass errors to async_show_form
# Map the selected option back to the actual notify target name # persist back into entry.data and reload
user_input[CONF_NOTIFY_TARGET] = notify_targets[user_input[CONF_NOTIFY_TARGET]] self.hass.config_entries.async_update_entry(
self._config_entry,
data=new_data,
)
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
# Merge current_data with user_input return self.async_create_entry(title="", data=None)
updated_data = {**current_data, **user_input}
_logger.debug("User input is not None, updated data: %s", updated_data)
_logger.debug("Updating entry with updated data: %s", updated_data) # build edit form schema with defaults
schema = {
Required(
CONF_NAME, default=current.get(CONF_NAME, "")
): str,
Required(
CONF_TOPIC, default=current.get(CONF_TOPIC, "")
): str,
Required(
CONF_AWAKE_DURATION,
default=current.get(CONF_AWAKE_DURATION, DEFAULT_AWAKE_DURATION),
): int,
Required(
CONF_SLEEP_DURATION,
default=current.get(CONF_SLEEP_DURATION, DEFAULT_SLEEP_DURATION),
): int,
Required(
CONF_AWAKE_STATES,
default=current.get(CONF_AWAKE_STATES, DEFAULT_AWAKE_STATES),
): cv.multi_select(AVAILABLE_STATES),
Required(
CONF_SLEEP_STATES,
default=current.get(CONF_SLEEP_STATES, DEFAULT_SLEEP_STATES),
): cv.multi_select(AVAILABLE_STATES),
}
if updated_data is not None: if notify_targets:
self.hass.data[DOMAIN][self.config_entry.entry_id] = updated_data # Save updated data # prepend "no mobile", then all real targets, all lowercase with spaces
labels = ["no mobile"] + list(notify_targets.keys())
default_label = reverse_map.get(
current.get(CONF_NOTIFY_TARGET), "no mobile"
)
schema[Optional(CONF_NOTIFY_TARGET, default=default_label)] = In(labels)
# Update the entry data return self.async_show_form(
self.hass.config_entries.async_update_entry(self.config_entry, data=updated_data) step_id="init",
data_schema=Schema(schema),
# Send a signal to reload the integration errors={},
async_dispatcher_send(self.hass, f"{DOMAIN}_reload_{self.config_entry.entry_id}")
return self.async_create_entry(title="", data=updated_data)
_logger.debug("User input is None, showing form with current_data: %s", current_data)
return self.async_show_form(step_id="init", data_schema=self.get_data_schema(current_data), errors=errors) # Pass errors to async_show_form
except Exception as e:
_logger.error("Error in async_step_init: %s", str(e))
return self.async_abort(reason=str(e))
def get_data_schema(self, current_data):
# Get the list of notify targets
notify_services = self.hass.services.async_services().get('notify', {})
notify_targets = {target.replace('mobile_app_', '').title(): target for target in notify_services.keys() if target.startswith('mobile_app_')}
# Extract the part after 'mobile_app_' and capitalize
notify_target = current_data.get(CONF_NOTIFY_TARGET, "")
notify_target = notify_target.replace('mobile_app_', '').title() if notify_target.startswith('mobile_app_') else notify_target
return Schema(
{
Required(CONF_NAME, default=current_data.get(CONF_NAME, "")): str,
Required(CONF_TOPIC, default=current_data.get(CONF_TOPIC, "")): str,
Required(CONF_QOS, default=current_data.get(CONF_QOS, 0)): In([0, 1, 2]),
Required(CONF_AWAKE_DURATION, default=current_data.get(CONF_AWAKE_DURATION, DEFAULT_AWAKE_DURATION)): int,
Required(CONF_SLEEP_DURATION, default=current_data.get(CONF_SLEEP_DURATION, DEFAULT_SLEEP_DURATION)): int,
Required(CONF_AWAKE_STATES, default=current_data.get(CONF_AWAKE_STATES, DEFAULT_AWAKE_STATES)): cv.multi_select(AVAILABLE_STATES),
Required(CONF_SLEEP_STATES, default=current_data.get(CONF_SLEEP_STATES, DEFAULT_SLEEP_STATES)): cv.multi_select(AVAILABLE_STATES),
Optional(CONF_NOTIFY_TARGET, default=notify_target): vol.In(list(notify_targets.keys())),
}
) )

View file

@ -5,21 +5,24 @@ DOMAIN = "saas"
INTEGRATION_NAME = "SAAS - Sleep As Android Stats" INTEGRATION_NAME = "SAAS - Sleep As Android Stats"
MODEL = "SAAS - Version 0.0.1" MODEL = "SAAS - Version 0.0.1"
CONF_NAME = "name" # Name of the Integration CONF_NAME = "name" # Name of the Integration
CONF_TOPIC = "topic_template" # MQTT Topic for Sleep As Android Events 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_AWAKE_DURATION = "awake_duration" # Awake Duration
CONF_SLEEP_DURATION = "sleep_duration" # Sleep Duration CONF_SLEEP_DURATION = "sleep_duration" # Sleep Duration
CONF_AWAKE_STATES = "awake_states" # Awake States CONF_AWAKE_STATES = "awake_states" # Awake States
CONF_SLEEP_STATES = "sleep_states" # Sleep States CONF_SLEEP_STATES = "sleep_states" # Sleep States
CONF_NOTIFY_TARGET = "notify_target" # Notify Target
CONF_NOTIFY_TARGET = "notify_target" # Notify Target DEFAULT_AWAKE_DURATION = 10 # Default Awake Duration
DEFAULT_SLEEP_DURATION = 10 # Default Sleep Duration
DEFAULT_AWAKE_DURATION = 10 # Default Awake Duration DEFAULT_AWAKE_STATES = ["Awake", "Sleep Tracking Stopped"] # Default Awake States
DEFAULT_SLEEP_DURATION = 10 # Default Sleep Duration DEFAULT_SLEEP_STATES = [
DEFAULT_AWAKE_STATES = ["Awake", "Sleep Tracking Stopped"] # Default Awake States "Not Awake",
DEFAULT_SLEEP_STATES = ["Not Awake", "Rem", "Light Sleep", "Deep Sleep", "Sleep Tracking Started"]# Default Sleep States "Rem",
"Light Sleep",
"Deep Sleep",
"Sleep Tracking Started",
] # Default Sleep States
SENSOR_TYPES = { SENSOR_TYPES = {
"state": {"name": "State", "device_class": None}, "state": {"name": "State", "device_class": None},
@ -33,7 +36,7 @@ DAY_MAPPING = {
"wednesday": 4, "wednesday": 4,
"thursday": 5, "thursday": 5,
"friday": 6, "friday": 6,
"saturday": 7 "saturday": 7,
} }
AVAILABLE_STATES = [ AVAILABLE_STATES = [
@ -67,8 +70,9 @@ AVAILABLE_STATES = [
'Sound Event Laugh', 'Sound Event Laugh',
'Sound Event Snore', 'Sound Event Snore',
'Sound Event Talk', 'Sound Event Talk',
'Time for Bed' 'Time for Bed',
] ]
STATE_MAPPING = { STATE_MAPPING = {
"unknown": "Unknown", "unknown": "Unknown",
"sleep_tracking_started": "Sleep Tracking Started", "sleep_tracking_started": "Sleep Tracking Started",
@ -100,7 +104,7 @@ STATE_MAPPING = {
"sound_event_cough": "Cough Detected", "sound_event_cough": "Cough Detected",
"sound_event_baby": "Baby Cry Detected", "sound_event_baby": "Baby Cry Detected",
"sound_event_laugh": "Laugh Detected", "sound_event_laugh": "Laugh Detected",
"alarm_rescheduled": "Alarm Rescheduled" "alarm_rescheduled": "Alarm Rescheduled",
} }
REVERSE_STATE_MAPPING = {v: k for k, v in STATE_MAPPING.items()} REVERSE_STATE_MAPPING = {v: k for k, v in STATE_MAPPING.items()}
@ -111,14 +115,15 @@ SLEEP_STAGE_MAPPING = {
"deep_sleep": "Deep Sleep", "deep_sleep": "Deep Sleep",
"light_sleep": "Light Sleep", "light_sleep": "Light Sleep",
"awake": "Awake", "awake": "Awake",
"not_awake": "Not Awake" "not_awake": "Not Awake",
} }
SOUND_MAPPING = { SOUND_MAPPING = {
'sound_event_snore': "Snore Detected", 'sound_event_snore': "Snore Detected",
'sound_event_talk': "Talk Detected", 'sound_event_talk': "Talk Detected",
'sound_event_cough': "Cough Detected", 'sound_event_cough': "Cough Detected",
'sound_event_baby': "Baby Cry Detected", 'sound_event_baby': "Baby Cry Detected",
'sound_event_laugh': "Laugh Detected" 'sound_event_laugh': "Laugh Detected",
} }
LULLABY_MAPPING = { LULLABY_MAPPING = {
@ -129,7 +134,7 @@ LULLABY_MAPPING = {
DISTURBANCE_MAPPING = { DISTURBANCE_MAPPING = {
'apnea_alarm': "Apnea Alarm", 'apnea_alarm': "Apnea Alarm",
'antisnoring': "Antisnoring" 'antisnoring': "Antisnoring",
} }
ALARM_EVENT_MAPPING = { ALARM_EVENT_MAPPING = {
@ -143,12 +148,12 @@ ALARM_EVENT_MAPPING = {
'show_skip_next_alarm': "Show Skip Next Alarm", 'show_skip_next_alarm': "Show Skip Next Alarm",
'smart_period': "Smart Period", 'smart_period': "Smart Period",
'before_smart_period': "Before Smart Period", 'before_smart_period': "Before Smart Period",
"alarm_rescheduled": "Alarm Rescheduled" "alarm_rescheduled": "Alarm Rescheduled",
} }
SLEEP_TRACKING_MAPPING = { SLEEP_TRACKING_MAPPING = {
'sleep_tracking_started': "Sleep Tracking Started", 'sleep_tracking_started': "Sleep Tracking Started",
'sleep_tracking_stopped': "Sleep Tracking Stopped", 'sleep_tracking_stopped': "Sleep Tracking Stopped",
'sleep_tracking_paused': "Sleep Tracking Paused", 'sleep_tracking_paused': "Sleep Tracking Paused",
'sleep_tracking_resumed': "Sleep Tracking Resumed" 'sleep_tracking_resumed': "Sleep Tracking Resumed",
} }

View file

@ -6,5 +6,5 @@
"dependencies": ["mqtt", "mobile_app"], "dependencies": ["mqtt", "mobile_app"],
"documentation": "https://www.github.com/sudoxnym/saas", "documentation": "https://www.github.com/sudoxnym/saas",
"issue_tracker": "https://www.github.com/sudoxnym/saas/issues", "issue_tracker": "https://www.github.com/sudoxnym/saas/issues",
"version": "0.1.0" "version": "0.2.1"
} }

View file

@ -219,6 +219,120 @@ class SAASAlarmEventSensor(RestoreEntity):
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to 'None' due to timeout for sensor {self.name}") _LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to 'None' due to timeout for sensor {self.name}")
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
class SAASNextAlarmSensor(RestoreEntity):
"""Sensor that exposes the next scheduled alarm time and label."""
def __init__(self, hass, name, entry_id):
self._hass = hass
self._name = name
self.entry_id = entry_id
self._state = None
self._label = None
self._alarms = []
@property
def unique_id(self):
return f"saas_next_alarm_{self._name}"
@property
def name(self):
return f"SAAS {self._name} Next Alarm"
@property
def state(self):
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,
}
@property
def extra_state_attributes(self):
attrs = {}
if self._label:
attrs["Label"] = self._label
if self._alarms:
attrs["Alarms"] = self._alarms
return attrs
async def async_added_to_hass(self):
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
self._state = state.state if state.state != "None" else None
self._label = state.attributes.get("Label")
saved = state.attributes.get("Alarms")
if isinstance(saved, list):
self._alarms = saved
async def message_received(msg):
msg_json = json.loads(msg.payload)
event = msg_json.get("event")
value1 = msg_json.get("value1")
value2 = msg_json.get("value2")
if event == "alarm_rescheduled":
if value1:
ts = int(value1) / 1000.0
dt = dt_util.as_local(datetime.fromtimestamp(ts))
epoch = dt.timestamp()
found = False
for alarm in self._alarms:
if alarm["timestamp"] == epoch:
alarm["label"] = value2
found = True
break
if not found:
self._alarms.append({"timestamp": epoch, "label": value2})
self._alarms.sort(key=lambda x: x["timestamp"])
self._alarms = self._alarms[:10]
elif event in ("alarm_alert_dismiss", "alarm_skip_next"):
if value1:
ts = int(value1) / 1000.0
dt = dt_util.as_local(datetime.fromtimestamp(ts))
epoch = dt.timestamp()
self._alarms = [a for a in self._alarms if a["timestamp"] != epoch]
elif self._alarms:
self._alarms.pop(0)
else:
return
if self._alarms:
next_alarm = self._alarms[0]
dt = dt_util.as_local(datetime.fromtimestamp(next_alarm["timestamp"]))
self._state = dt.strftime("%Y-%m-%d %H:%M")
self._label = next_alarm.get("label")
else:
self._state = None
self._label = None
self.async_schedule_update_ha_state()
await async_subscribe(
self._hass,
self._hass.data[DOMAIN][self.entry_id][CONF_TOPIC],
message_received,
)
async def async_will_remove_from_hass(self):
"""Run when entity will be removed from hass."""
self.hass.states.async_set(
self.entity_id,
self._state,
{"Label": self._label, "Alarms": self._alarms},
)
_LOGGER.info(
f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Saved state: {self._state} for sensor {self.name}"
)
class SAASSoundSensor(RestoreEntity): class SAASSoundSensor(RestoreEntity):
"""Representation of a SAAS - Sleep As Android Stats sensor for Sound Events.""" """Representation of a SAAS - Sleep As Android Stats sensor for Sound Events."""
@ -820,6 +934,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
entities = [ entities = [
SAASSensor(hass, name, STATE_MAPPING, entry_id), SAASSensor(hass, name, STATE_MAPPING, entry_id),
SAASAlarmEventSensor(hass, name, ALARM_EVENT_MAPPING, entry_id), SAASAlarmEventSensor(hass, name, ALARM_EVENT_MAPPING, entry_id),
SAASNextAlarmSensor(hass, name, entry_id),
SAASSoundSensor(hass, name, SOUND_MAPPING, entry_id), SAASSoundSensor(hass, name, SOUND_MAPPING, entry_id),
SAASSleepTrackingSensor(hass, name, SLEEP_TRACKING_MAPPING, entry_id), SAASSleepTrackingSensor(hass, name, SLEEP_TRACKING_MAPPING, entry_id),
SAASDisturbanceSensor(hass, name, DISTURBANCE_MAPPING, entry_id), SAASDisturbanceSensor(hass, name, DISTURBANCE_MAPPING, entry_id),

View file

@ -0,0 +1 @@
#empty on purpose