mirror of
https://github.com/sudoxnym/saas.git
synced 2026-04-16 04:27:46 +00:00
Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2e35f8f20 | ||
|
|
0cbe7ba589 | ||
|
|
e4b0fa0375 | ||
|
|
45767967a4 | ||
|
|
7c359a4b6e | ||
|
|
23f5d5dd18 | ||
|
|
5f3a7e78f5 | ||
|
|
359dd530fe | ||
|
|
6de713d0c6 | ||
|
|
6dc8fa9afa | ||
|
|
f5161159d7 | ||
|
|
ccbdd154f2 | ||
|
|
3f7c73ab46 | ||
|
|
3a41318279 | ||
|
|
1f52692d07 |
8 changed files with 475 additions and 227 deletions
232
README.md
232
README.md
|
|
@ -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.
|
||||
|
||||
[](https://github.com/sudoxnym/sleepd)
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>original readme (archived)</summary>
|
||||
|
||||
<p align="center">
|
||||
<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>
|
||||
</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>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<li>
|
||||
<details>
|
||||
<summary><strong>📡 8 Sensors</strong></summary>
|
||||
<ul>
|
||||
<li>Message Received *State</li>
|
||||
<li>Wake Status</li>
|
||||
<li>Sound</li>
|
||||
<li>Disturbance</li>
|
||||
<li>Alarm</li>
|
||||
<li>Lullaby</li>
|
||||
<li>Sleep Tracking</li>
|
||||
<li>Sleep Stage</li>
|
||||
</ul>
|
||||
<p>This should intelligently and dynamically allow for state changes in the Wake Status Sensor.</p>
|
||||
<summary><strong>📡 9 sensors</strong></summary>
|
||||
|
||||
| Sensor | Description |
|
||||
| ------ | ----------- |
|
||||
| message received | shows the last raw MQTT event payload |
|
||||
| wake status | indicates awake or asleep based on your sleep stage |
|
||||
| sound | snore, talk, cough, and other sound events |
|
||||
| disturbance | reports apnea and antisnoring events |
|
||||
| **next alarm** | upcoming alarm time and label; stores the last ten alarms in attributes |
|
||||
| alarm | alarm related events such as snooze or dismiss |
|
||||
| lullaby | lullaby status |
|
||||
| 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>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><strong>🎛️ 8 Buttons</strong></summary>
|
||||
<summary><strong>🎛️ 8 buttons *only if mobile_app selected</strong></summary>
|
||||
<ul>
|
||||
<li>Alarm Dismiss</li>
|
||||
<li>Alarm Snooze</li>
|
||||
<li>Lullaby Stop</li>
|
||||
<li>Sleep Tracking Pause</li>
|
||||
<li>Sleep Tracking Resume</li>
|
||||
<li>Sleep Tracking Start</li>
|
||||
<li>Sleep Tracking Start with Optimal Alarm</li>
|
||||
<li>Sleep Tracking Stop</li>
|
||||
<li>alarm dismiss</li>
|
||||
<li>alarm snooze</li>
|
||||
<li>lullaby stop</li>
|
||||
<li>sleep tracking pause</li>
|
||||
<li>sleep tracking resume</li>
|
||||
<li>sleep tracking start</li>
|
||||
<li>sleep tracking start with optimal alarm</li>
|
||||
<li>sleep tracking stop</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary><strong>🛠️ 1 Service</strong></summary>
|
||||
<summary><strong>🛠️ 1 service</strong></summary>
|
||||
<pre>
|
||||
service: saas.saas_example_alarm_set
|
||||
data:
|
||||
|
|
@ -62,90 +85,89 @@ data:
|
|||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<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>
|
||||
<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>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<details>
|
||||
<summary><strong>✅ Known working</strong></summary>
|
||||
<summary><strong>✅ known working</strong></summary>
|
||||
<ul>
|
||||
<li>📟 Xioami 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>⌚ Garmin Fenix 7X with Garmin Alternative, <b>NOT</b> 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 Mi Band 7**</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, **not** the free one.</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>
|
||||
</details>
|
||||
|
||||
<h2>🧪 Installation:</h2>
|
||||
<h2>🧪 installation:</h2>
|
||||
<ul>
|
||||
<li>Add https://www.github.com/sudoxnym/saas to your Custom Repositories in HACS</li>
|
||||
<li>Search and Download SAAS - Sleep As Android Status</li>
|
||||
<li>Restart Home Assistant</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>restart Home Assistant</li>
|
||||
<li>
|
||||
<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>
|
||||
</li>
|
||||
<li>Add Integration: SAAS - Sleep As Android Status</li>
|
||||
<li>add integration: **SAAS - Sleep As Android status**</li>
|
||||
</ul>
|
||||
|
||||
<h2>⚙️ Configuration:</h2>
|
||||
<h2>⚙️ configuration:</h2>
|
||||
<ul>
|
||||
<li>Name: Name of user</li>
|
||||
<li>Topic: MQTT Topic from Sleep As Android</li>
|
||||
<li>QoS: Quality of Service</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. <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>Asleep States: States to indicate being asleep</li>
|
||||
<li>Mobile App: Target for buttons <b>REQUIRES COMPANION APP</b></li>
|
||||
<li>name: name of user</li>
|
||||
<li>topic: MQTT topic from Sleep As Android *MUST MATCH*</li>
|
||||
<li>awake duration: time in seconds in which awake states = true to indicate awake. <b>fixed</b></li>
|
||||
<li>asleep duration: time in seconds in which sleep states = true to indicate asleep. <b>fixed</b></li>
|
||||
<li>awake states: states to indicate being awake</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>
|
||||
</ul>
|
||||
|
||||
<details>
|
||||
<summary><strong>📲 Set Up Notify for Mi Band 7</strong></summary>
|
||||
<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>Obtain auth key for Notify app using ADB</li>
|
||||
</ol>
|
||||
<summary><strong>📲 set up Notify for Mi Band 7</strong></summary>
|
||||
<ol>
|
||||
<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>
|
||||
</ol>
|
||||
|
||||
<pre>
|
||||
<pre>
|
||||
adb shell
|
||||
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}'
|
||||
</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">
|
||||
<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>Follow prompts, input auth key, select Mi Fitness is not installed</li>
|
||||
<li>Enable Sleep as Android in Notify settings</li>
|
||||
</ol>
|
||||
<ol start="3">
|
||||
<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>follow prompts, input auth key, select Mi Fitness is not installed</li>
|
||||
<li>enable Sleep As Android in Notify settings</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🔐 Extracting the Zepp <code>authKey</code> on a Rooted Android Device</strong></summary>
|
||||
<pre>
|
||||
<summary><strong>🔐 extracting the Zepp <code>authKey</code> on a rooted android device</strong></summary>
|
||||
<pre>
|
||||
su
|
||||
cd /data/data/com.huami.watch.hmwatchmanager/databases/
|
||||
ls origin_db_*
|
||||
sqlite3 origin_db_1234567890 "SELECT AUTHKEY FROM DEVICE;"
|
||||
</pre>
|
||||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>⚠️ Do Not Unpair before extracting</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>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>⚠️ do not unpair before extracting</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</a></li>
|
||||
</ul>
|
||||
</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>
|
||||
<li>Open the app and follow setup</li>
|
||||
<li>Settings wheel > Services > Automation > MQTT</li>
|
||||
<li>open the app and follow setup</li>
|
||||
<li>settings wheel > services > automation > MQTT</li>
|
||||
</ol>
|
||||
|
||||
<pre>
|
||||
|
|
@ -153,42 +175,54 @@ sqlite3 origin_db_1234567890 "SELECT AUTHKEY FROM DEVICE;"
|
|||
</pre>
|
||||
|
||||
<ul>
|
||||
<li>Topic: must match config</li>
|
||||
<li>Client ID: any unique ID</li>
|
||||
<li>topic: must match config</li>
|
||||
<li>client id: any unique id</li>
|
||||
</ul>
|
||||
|
||||
<ol start="4">
|
||||
<li>Enable automatic tracking</li>
|
||||
<li>Sensor: Sonar or Accelerometer</li>
|
||||
<li>Wearables > Xiaomi Mi Band > Test sensor</li>
|
||||
<li>enable automatic tracking</li>
|
||||
<li>sensor: sonar or accelerometer</li>
|
||||
<li>wearables > **Xiaomi Mi Band** > test sensor</li>
|
||||
</ol>
|
||||
|
||||
<details>
|
||||
<summary><strong>📦 Changes</strong></summary>
|
||||
<b>0.0.6a</b>
|
||||
<summary><strong>📦 changes</strong></summary>
|
||||
<b>0.2.2</b>
|
||||
<ul>
|
||||
<li>Initial Beta Release</li>
|
||||
<li>Added persistent states</li>
|
||||
<li>Alarm Event sensor attributes</li>
|
||||
<li>added Next Alarm sensor with alarm label tracking</li>
|
||||
<li>stores up to ten previous alarms in 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>
|
||||
|
||||
<b>0.1.0</b>
|
||||
<ul>
|
||||
<li>Fixed Wake Status Timing</li>
|
||||
<li>Bug fixes on Sound sensor</li>
|
||||
<li>Accurate updates to AlarmEvent, Disturbance, Sound</li>
|
||||
<li>Organized README</li>
|
||||
<li>fixed wake status timing</li>
|
||||
<li>bug fixes on sound sensor</li>
|
||||
<li>accurate updates to alarmevent, disturbance, sound</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>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>🚨 Known Issues</strong></summary>
|
||||
<pre>
|
||||
Logger: homeassistant.helpers.service
|
||||
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>
|
||||
<summary><strong>🚨 known issues</strong></summary>
|
||||
<p>💬 no known issues at this time.</p>
|
||||
</details>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -5,50 +5,53 @@ from homeassistant.core import HomeAssistant
|
|||
from .const import DOMAIN
|
||||
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) -> bool:
|
||||
"""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
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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, {})
|
||||
|
||||
if entry.entry_id not in hass.data[DOMAIN] and entry.data:
|
||||
hass.data[DOMAIN][entry.entry_id] = entry.data
|
||||
# merge original data + any saved options so runtime sees edits
|
||||
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 the setup to the sensor and button platforms.
|
||||
# forward setup to sensor and button platforms
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "button"])
|
||||
|
||||
_logger.info(f"hass.data[DOMAIN] before async_setup_services: {hass.data[DOMAIN]}")
|
||||
|
||||
# Setup the services.
|
||||
_logger.info("Starting setup of services")
|
||||
# set up any custom services
|
||||
_LOGGER.info("Starting setup of services")
|
||||
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")
|
||||
_LOGGER.info("Finished setup of config entry")
|
||||
return True
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""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}")
|
||||
|
||||
# Unload sensor and button platforms.
|
||||
# unload sensor and button platforms
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, ["sensor", "button"])
|
||||
if not unload_ok:
|
||||
_logger.error("Failed to unload platforms for saas")
|
||||
_LOGGER.error("Failed to unload platforms for saas")
|
||||
return False
|
||||
|
||||
if isinstance(hass.data.get(DOMAIN, {}), dict):
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
# clean up our stored data
|
||||
hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
|
||||
_logger.info(f"hass.data[DOMAIN] after removing entry data: {hass.data[DOMAIN]}")
|
||||
_logger.info("Finished unload of config entry")
|
||||
_LOGGER.info(f"hass.data[{DOMAIN}] after unload: {hass.data.get(DOMAIN)}")
|
||||
_LOGGER.info("Finished unload of config entry")
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
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
|
||||
|
||||
# Set up logging
|
||||
|
|
@ -50,6 +50,12 @@ class SAASSleepTrackingStart(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -117,6 +123,12 @@ class SAASSleepTrackingStop(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -184,6 +196,12 @@ class SAASSleepTrackingPause(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -251,6 +269,12 @@ class SAASSleepTrackingResume(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -318,6 +342,12 @@ class SAASAlarmClockSnooze(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -385,6 +415,12 @@ class SAASAlarmClockDisable(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -452,6 +488,12 @@ class SAASSleepTrackingStartWithAlarm(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
service_name = self._notify_target # Remove the "notify." prefix
|
||||
|
||||
|
|
@ -519,6 +561,12 @@ class SAASLullabyStop(ButtonEntity):
|
|||
return device_info
|
||||
|
||||
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."""
|
||||
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):
|
||||
"""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)
|
||||
|
||||
# Extract the necessary data from config_entry.data
|
||||
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
|
||||
entities = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from .const import (
|
|||
DOMAIN,
|
||||
CONF_NAME,
|
||||
CONF_TOPIC,
|
||||
CONF_QOS,
|
||||
AVAILABLE_STATES,
|
||||
CONF_AWAKE_DURATION,
|
||||
CONF_SLEEP_DURATION,
|
||||
|
|
@ -17,129 +16,169 @@ from .const import (
|
|||
CONF_NOTIFY_TARGET,
|
||||
)
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from voluptuous import Schema, Required, In, Optional
|
||||
from voluptuous import Schema, Required, Optional, In
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for SAAS."""
|
||||
"""Handle the initial config flow for SAAS."""
|
||||
VERSION = 2
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
"""Initial setup step."""
|
||||
errors = {}
|
||||
|
||||
# Build notify targets list
|
||||
# discover available mobile_app notify services
|
||||
notify_services = self.hass.services.async_services().get("notify", {})
|
||||
notify_targets = {
|
||||
key.replace("mobile_app_", "").title(): key
|
||||
for key in notify_services.keys()
|
||||
if key.startswith("mobile_app_")
|
||||
svc.replace("mobile_app_", "")
|
||||
.replace("_", " ")
|
||||
.lower(): svc
|
||||
for svc in notify_services
|
||||
if svc.startswith("mobile_app_")
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
# Map back the chosen label to service name
|
||||
if user_input.get(CONF_NOTIFY_TARGET):
|
||||
user_input[CONF_NOTIFY_TARGET] = notify_targets.get(user_input[CONF_NOTIFY_TARGET])
|
||||
# map chosen label back to service name, or remove if invalid
|
||||
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)
|
||||
|
||||
# basic validation
|
||||
if not user_input.get(CONF_NAME):
|
||||
errors[CONF_NAME] = "required"
|
||||
|
||||
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(
|
||||
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(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())),
|
||||
}
|
||||
),
|
||||
data_schema=Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_migrate_entry(self, hass, entry):
|
||||
"""Migrate old config entries to the new schema."""
|
||||
_LOGGER.debug("Migrating config entry %s from version %s", entry.entry_id, entry.version)
|
||||
data = {**entry.data}
|
||||
options = {**entry.options}
|
||||
|
||||
# If you renamed keys in entry.data/options, do it here when entry.version == 1
|
||||
# e.g.:
|
||||
# if entry.version == 1:
|
||||
# data["topic_template"] = data.pop("topic")
|
||||
# entry.version = 2
|
||||
|
||||
# For no data changes, just bump the version:
|
||||
entry.version = self.VERSION
|
||||
hass.config_entries.async_update_entry(entry, data=data, options=options)
|
||||
_LOGGER.info("Migrated config entry %s to version %s", entry.entry_id, entry.version)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@config_entries.callback
|
||||
def async_get_options_flow(entry):
|
||||
"""Get the options flow handler."""
|
||||
"""Return options flow handler."""
|
||||
return OptionsFlowHandler(entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle SAAS options."""
|
||||
"""Handle SAAS options editing."""
|
||||
|
||||
def __init__(self, entry):
|
||||
"""Initialize options flow."""
|
||||
super().__init__()
|
||||
self._config_entry = entry # use private attribute
|
||||
self._config_entry = entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
# Load current options or fall back to data
|
||||
current = self._config_entry.options.copy()
|
||||
for key in [
|
||||
CONF_NAME,
|
||||
CONF_TOPIC,
|
||||
CONF_QOS,
|
||||
CONF_AWAKE_DURATION,
|
||||
CONF_SLEEP_DURATION,
|
||||
CONF_AWAKE_STATES,
|
||||
CONF_SLEEP_STATES,
|
||||
CONF_NOTIFY_TARGET,
|
||||
]:
|
||||
if key not in current and key in self._config_entry.data:
|
||||
current[key] = self._config_entry.data[key]
|
||||
"""Manage the options form (edit)."""
|
||||
current = dict(self._config_entry.data)
|
||||
|
||||
# Build notify targets list
|
||||
# discover mobile_app notify services again
|
||||
notify_services = self.hass.services.async_services().get("notify", {})
|
||||
notify_targets = {
|
||||
key.replace("mobile_app_", "").title(): key
|
||||
for key in notify_services.keys()
|
||||
if key.startswith("mobile_app_")
|
||||
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()}
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
new_data = current.copy()
|
||||
|
||||
# standard fields
|
||||
for key in (
|
||||
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]
|
||||
|
||||
# handle notify_target with "no mobile" option
|
||||
sel = user_input.get(CONF_NOTIFY_TARGET)
|
||||
if sel == "no mobile":
|
||||
new_data.pop(CONF_NOTIFY_TARGET, None)
|
||||
elif sel in notify_targets:
|
||||
new_data[CONF_NOTIFY_TARGET] = notify_targets[sel]
|
||||
|
||||
# persist back into entry.data and reload
|
||||
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)
|
||||
|
||||
return self.async_create_entry(title="", data=None)
|
||||
|
||||
# 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 notify_targets:
|
||||
# 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)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
Required(CONF_NAME, default=current.get(CONF_NAME, "")): str,
|
||||
Required(CONF_TOPIC, default=current.get(CONF_TOPIC, "")): str,
|
||||
Required(CONF_QOS, default=current.get(CONF_QOS, 0)): In([0, 1, 2]),
|
||||
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),
|
||||
Optional(CONF_NOTIFY_TARGET, default=current.get(CONF_NOTIFY_TARGET, "")): vol.In(list(notify_targets.keys())),
|
||||
}
|
||||
),
|
||||
data_schema=Schema(schema),
|
||||
errors={},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,21 +5,24 @@ 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_NAME = "name" # Name of the Integration
|
||||
CONF_TOPIC = "topic_template" # MQTT Topic for Sleep As Android Events
|
||||
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
|
||||
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_STATES = ["Awake", "Sleep Tracking Stopped"] # Default Awake States
|
||||
DEFAULT_SLEEP_STATES = ["Not Awake", "Rem", "Light Sleep", "Deep Sleep", "Sleep Tracking Started"]# Default Sleep States
|
||||
DEFAULT_AWAKE_DURATION = 10 # Default Awake Duration
|
||||
DEFAULT_SLEEP_DURATION = 10 # Default Sleep Duration
|
||||
DEFAULT_AWAKE_STATES = ["Awake", "Sleep Tracking Stopped"] # Default Awake States
|
||||
DEFAULT_SLEEP_STATES = [
|
||||
"Not Awake",
|
||||
"Rem",
|
||||
"Light Sleep",
|
||||
"Deep Sleep",
|
||||
"Sleep Tracking Started",
|
||||
] # Default Sleep States
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"state": {"name": "State", "device_class": None},
|
||||
|
|
@ -33,7 +36,7 @@ DAY_MAPPING = {
|
|||
"wednesday": 4,
|
||||
"thursday": 5,
|
||||
"friday": 6,
|
||||
"saturday": 7
|
||||
"saturday": 7,
|
||||
}
|
||||
|
||||
AVAILABLE_STATES = [
|
||||
|
|
@ -67,8 +70,9 @@ AVAILABLE_STATES = [
|
|||
'Sound Event Laugh',
|
||||
'Sound Event Snore',
|
||||
'Sound Event Talk',
|
||||
'Time for Bed'
|
||||
'Time for Bed',
|
||||
]
|
||||
|
||||
STATE_MAPPING = {
|
||||
"unknown": "Unknown",
|
||||
"sleep_tracking_started": "Sleep Tracking Started",
|
||||
|
|
@ -100,7 +104,7 @@ STATE_MAPPING = {
|
|||
"sound_event_cough": "Cough Detected",
|
||||
"sound_event_baby": "Baby Cry 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()}
|
||||
|
|
@ -111,14 +115,15 @@ SLEEP_STAGE_MAPPING = {
|
|||
"deep_sleep": "Deep Sleep",
|
||||
"light_sleep": "Light Sleep",
|
||||
"awake": "Awake",
|
||||
"not_awake": "Not Awake"
|
||||
"not_awake": "Not Awake",
|
||||
}
|
||||
|
||||
SOUND_MAPPING = {
|
||||
'sound_event_snore': "Snore Detected",
|
||||
'sound_event_talk': "Talk Detected",
|
||||
'sound_event_cough': "Cough Detected",
|
||||
'sound_event_baby': "Baby Cry Detected",
|
||||
'sound_event_laugh': "Laugh Detected"
|
||||
'sound_event_laugh': "Laugh Detected",
|
||||
}
|
||||
|
||||
LULLABY_MAPPING = {
|
||||
|
|
@ -129,7 +134,7 @@ LULLABY_MAPPING = {
|
|||
|
||||
DISTURBANCE_MAPPING = {
|
||||
'apnea_alarm': "Apnea Alarm",
|
||||
'antisnoring': "Antisnoring"
|
||||
'antisnoring': "Antisnoring",
|
||||
}
|
||||
|
||||
ALARM_EVENT_MAPPING = {
|
||||
|
|
@ -143,12 +148,12 @@ ALARM_EVENT_MAPPING = {
|
|||
'show_skip_next_alarm': "Show Skip Next Alarm",
|
||||
'smart_period': "Smart Period",
|
||||
'before_smart_period': "Before Smart Period",
|
||||
"alarm_rescheduled": "Alarm Rescheduled"
|
||||
"alarm_rescheduled": "Alarm Rescheduled",
|
||||
}
|
||||
|
||||
SLEEP_TRACKING_MAPPING = {
|
||||
'sleep_tracking_started': "Sleep Tracking Started",
|
||||
'sleep_tracking_stopped': "Sleep Tracking Stopped",
|
||||
'sleep_tracking_paused': "Sleep Tracking Paused",
|
||||
'sleep_tracking_resumed': "Sleep Tracking Resumed"
|
||||
}
|
||||
'sleep_tracking_resumed': "Sleep Tracking Resumed",
|
||||
}
|
||||
|
|
@ -6,5 +6,5 @@
|
|||
"dependencies": ["mqtt", "mobile_app"],
|
||||
"documentation": "https://www.github.com/sudoxnym/saas",
|
||||
"issue_tracker": "https://www.github.com/sudoxnym/saas/issues",
|
||||
"version": "0.1.0"
|
||||
"version": "0.2.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
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):
|
||||
"""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 = [
|
||||
SAASSensor(hass, name, STATE_MAPPING, entry_id),
|
||||
SAASAlarmEventSensor(hass, name, ALARM_EVENT_MAPPING, entry_id),
|
||||
SAASNextAlarmSensor(hass, name, entry_id),
|
||||
SAASSoundSensor(hass, name, SOUND_MAPPING, entry_id),
|
||||
SAASSleepTrackingSensor(hass, name, SLEEP_TRACKING_MAPPING, entry_id),
|
||||
SAASDisturbanceSensor(hass, name, DISTURBANCE_MAPPING, entry_id),
|
||||
|
|
@ -832,4 +947,4 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
if hasattr(entity, "async_setup"):
|
||||
await entity.async_setup()
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities)
|
||||
|
|
@ -0,0 +1 @@
|
|||
#empty on purpose
|
||||
Loading…
Reference in a new issue