Compare commits

..

No commits in common. "main" and "Beta" have entirely different histories.
main ... Beta

15 changed files with 525 additions and 1041 deletions

View file

@ -1,32 +0,0 @@
---
name: 🐛 Bug Report
about: Report a problem with SAAS - Sleep As Android Status
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or Logs**
If applicable, add screenshots or error logs to help explain your problem.
**System Info:**
- Home Assistant Version:
- Integration Version:
- Device (Mi Band, Zepp, etc):
- Android Version:
**Additional context**
Add any other context about the problem here.

View file

@ -1,19 +0,0 @@
---
name: 🚀 Feature Request
about: Suggest a new idea or feature
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem?**
Please describe what you're trying to solve.
**Describe the solution you'd like**
What should be added, improved, or changed?
**Describe alternatives you've considered**
Is there a workaround or a different approach?
**Additional context**
Add any other context or mockups here.

View file

@ -1,25 +0,0 @@
# 🛠️ Pull Request
## Description
Please include a summary of the changes and the related issue.
Fixes #[issue-number]
## Type of change
Please delete options that are not relevant.
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Checklist
- [ ] Ive tested these changes locally
- [ ] Ive added appropriate comments and documentation
- [ ] Ive added tests or updated existing ones
- [ ] Ive updated the README if necessary
## Additional Info
Anything else to include?

View file

@ -1,30 +0,0 @@
# Code of Conduct
## Contributor Covenant Code of Conduct
### Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone.
### Our Standards
Examples of behavior that contributes to a positive environment:
- Using welcoming and inclusive language
- Respecting differing viewpoints and experiences
- Accepting constructive criticism gracefully
- Showing empathy toward others
Examples of unacceptable behavior:
- The use of sexualized language or imagery
- Trolling, insulting or derogatory comments
- Personal or political attacks
- Public or private harassment
- Publishing others' private information without consent
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported
to the maintainers at sudoxnym@thearkdropship.com.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.

View file

@ -1,28 +0,0 @@
# Contributing to SAAS - Sleep As Android Status
Thanks for helping improve the project!
## 🧰 Getting Started
1. Fork this repo
2. Clone your fork
3. Create a feature branch (`git checkout -b feature/new-thing`)
4. Make your changes and commit (`git commit -m 'Add cool thing'`)
5. Push to your fork (`git push origin feature/new-thing`)
6. Open a Pull Request on this repo
## 🛠️ Development Tips
- This integration is a custom HACS component written in Python.
- Please test your changes with valid MQTT messages using the same topic format.
- Use clear commit messages and PR descriptions.
## 🐞 Bug Reports
Before opening an issue, check existing issues and include:
- Home Assistant version
- Logs (if possible)
- Steps to reproduce
- Device details
Thanks again for your help!

View file

@ -1,30 +0,0 @@
# 🧪 Deploying SAAS - Sleep As Android Status via HACS
## 🛠️ Pre-requisites
- Home Assistant (2023.x or newer recommended)
- HACS installed (https://hacs.xyz/)
- MQTT broker set up and working
- Android device running Sleep As Android
## 🚀 Installation Steps
1. In HACS, go to "Integrations"
2. Click the 3-dot menu > Custom repositories
3. Paste: `https://github.com/sudoxnym/saas`
4. Set Category to "Integration"
5. Click "Add"
6. Refresh, search for `SAAS - Sleep As Android Status`, and install
7. Restart Home Assistant
8. Go to Settings > Devices & Services > Add Integration > search "SAAS"
9. Configure the integration with your MQTT topic and states
## 🧪 Testing
- Trigger sleep/wake events from Sleep As Android
- Confirm sensor states update in Home Assistant
- Optional: Use HA Companion App to link button services
## 📄 More
See the [README](./README.md) for configuration, supported devices, and example automations.

46
LICENSE
View file

@ -1,21 +1,33 @@
MIT License
# License for SAAS - Sleep As Android Status Integration
Copyright (c) 2025 Joseph Ayers
## 1. Terms
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The software, "SAAS - Sleep As Android Status" (hereafter referred to as "the software"), is owned and copyrighted by @sudoxnym. The source code is available on GitHub at [SAAS Repository](https://www.github.com/sudoxnym/saas).
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
## 2. Grant of License
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This License grants you, free of charge, the non-exclusive right to use the software for personal use only. You are not permitted to redistribute, modify, or use the software for commercial purposes without explicit permission from the copyright owner.
## 3. Restrictions
- **No Redistribution**: You may not distribute copies of the software to third parties.
- **No Modification**: You may not modify, adapt, translate, or create derivative works based on the software without prior written consent from the copyright owner.
- **No Commercial Use**: You may not use the software for commercial purposes without obtaining a license to do so from the copyright owner.
## 4. Disclaimer of Warranty
The software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort or otherwise, arising from, out of, or in connection with the software or the use or other dealings in the software.
## 5. Limitation of Liability
In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
## 6. Termination
This License is effective until terminated. You can terminate it at any time by destroying all copies of the software in your possession. It will also automatically terminate if you fail to comply with any term or condition of this License. Upon termination, you agree to destroy all copies of the software.
## 7. Miscellaneous
This License constitutes the entire agreement between the parties concerning the subject matter hereof. If any provision of this License is found to be unenforceable, that provision shall be enforced to the maximum extent permissible so as to effect the intent of the parties, and the remainder of this License shall continue in full force and effect.
By downloading, installing, or using the software, you agree to be bound by the terms of this License. If you do not agree to the terms of this License, do not download, install, or use the software.

262
README.md
View file

@ -1,228 +1,72 @@
<p align="center">
<img src="https://img.shields.io/badge/⚠_DEPRECATED-red?style=for-the-badge" alt="deprecated">
</p>
[![Add to HACS](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge&logo=home%20assistant&labelColor=202020&color=41BDF5)](https://hacs.xyz/docs/faq/custom_repositories)<br>
<h1>SAAS - Sleep As Android Status</h1>
<h2>Description:</h2></br>
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.</br></br>
<h3>This integration works best with a Xioami MiBand (7 or older) mixed with the Notify app and Sleep As Android configured.</h3>
# ⚠️ THIS PROJECT HAS BEEN DEPRECATED
<h3>This integration will create 8 Sensors, 8 Buttons, 1 service, and 1 device per user:</h3></br>
**this project has been rebranded and moved to [sleepd](https://github.com/sudoxnym/sleepd)**
<h3>Sensors</h3>
Message Received *State</br>
Wake Status</br>
Sound</br>
Disturbance</br>
Alarm</br>
Lullaby</br>
Sleep Tracking</br>
Sleep Statge</br>
please use the new repository for future updates and support.
This should intelligently and dynamically allow for state changes in the Wake Status Sensor.</br></br>
[![go to sleepd](https://img.shields.io/badge/GO_TO_SLEEPD-blue?style=for-the-badge&logo=github)](https://github.com/sudoxnym/sleepd)
<h3>Buttons</h3>
Alarm Dismiss</br>
Alarm Snooze</br>
Lullaby Stop</br>
Sleep Tracking Pause</br>
Sleep Tracking Resum</br>
Sleep Tracking Start</br>
Sleep Tracking Start with Optimal Alarm</br>
Sleep Tracking Stop</br>
---
<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">
</a>
</p>
<h1>🌙 saas - Sleep As Android status</h1>
<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.
</p>
<h3>🧱 this integration will create:</h3>
<ul>
<li>
<details>
<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 *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>
</ul>
</details>
</li>
<li>
<details>
<summary><strong>🛠️ 1 service</strong></summary>
<pre>
<h3>Service</h3>
Set alarm service</br>
<pre>
service: saas.saas_example_alarm_set
data:
message: Example Message!
day: monday
hour: 7
minute: 30
</pre>
</details>
</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>
</details>
</li>
</ul>
<details>
<summary><strong>✅ known working</strong></summary>
<ul>
<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>
<ul>
<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">
</a>
</li>
<li>add integration: **SAAS - Sleep As Android status**</li>
</ul>
<h2>⚙️ configuration:</h2>
<ul>
<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 **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>
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>
<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>
</details>
<details>
<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>
<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>
<ol>
<li>open the app and follow setup</li>
<li>settings wheel > services > automation > MQTT</li>
</ol>
<pre>
(tcp/ssl)://(MQTT User):(MQTT Pass)@(HA URL):(port)
</pre>
<ul>
<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>
</ol>
<details>
<summary><strong>📦 changes</strong></summary>
<b>0.2.2</b>
<ul>
<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>
</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>
<h2>Installation:</h2>
<details>
<summary><strong>🚨 known issues</strong></summary>
<p>💬 no known issues at this time.</p>
</details>
</details>
&nbsp;&nbsp;Add https://www.github.com/sudoxnym/saas to your Custom Repositories in HACS</br>
&nbsp;&nbsp;Search and Download SAAS - Sleep As Android Status</br>
&nbsp;&nbsp;Restart Home Assistant</br>
&nbsp;&nbsp;[![Add To My Home Assistant](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=saas)<br>
&nbsp;&nbsp;Add Integration: SAAS - Sleep As Android Status</br></br>
<h2>Configuration:</h3>
&nbsp;&nbsp;Name: Name of user</br>
&nbsp;&nbsp;Topic: MQTT Topic from Sleep As Android</br>
&nbsp;&nbsp;QoS: Quality of Service</br></br>
&nbsp;&nbsp;Awake Duration: This is for tuning. Time in seconds in which awake states = true to indicate awake. Sensor usually updates within 30 seconds or so after the duration, not entirely sure why the delay.</br>
&nbsp;&nbsp;Asleep Duration: This is for tuning. Time in seconds in which sleep states = true to indicate asleep Sensor usually updates within 30 seconds or so after the duration, not entirely sure why the delay.</br>
&nbsp;&nbsp;Awake States: States to indicate being awake</br>
&nbsp;&nbsp;Asleep States: States to indicate being asleep</br>
&nbsp;&nbsp;Mobile App: Target for buttons </br></br>
Please report any issues.</br>
This is my first integration.
Built this in less than a week, with no prior knowledge of Python.

View file

@ -1,57 +1,64 @@
import asyncio
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from homeassistant.helpers.dispatcher import async_dispatcher_connect
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:
async def async_setup(hass: HomeAssistant, config: dict):
"""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:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up a config entry."""
_LOGGER.info(f"Starting setup of config entry with ID: {entry.entry_id}")
# ensure we have a dict for this integration
_logger.info(f"Starting setup of config entry with ID: {entry.entry_id}")
hass.data.setdefault(DOMAIN, {})
# 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],
)
if entry.entry_id not in hass.data[DOMAIN] and entry.data:
hass.data[DOMAIN][entry.entry_id] = entry.data
# forward setup to sensor and button platforms
await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "button"])
_logger.info(f"hass.data[DOMAIN] after adding entry data: {hass.data[DOMAIN]}")
# set up any custom services
_LOGGER.info("Starting setup of services")
# Forward the setup to the sensor and button platforms
for platform in ["sensor", "button"]:
_logger.info(f"Forwarding setup to {platform} platform")
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
_logger.info(f"hass.data[DOMAIN] before async_setup_services: {hass.data[DOMAIN]}")
# Setup the 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("Finished setup of config entry")
_logger.info(f"hass.data[DOMAIN] after setup of services: {hass.data[DOMAIN]}") # New log
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
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""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_ok = await hass.config_entries.async_unload_platforms(entry, ["sensor", "button"])
if not unload_ok:
_LOGGER.error("Failed to unload platforms for saas")
return False
# Remove the sensor platform
_logger.info("Removing sensor platform")
await hass.config_entries.async_forward_entry_unload(entry, "sensor")
# clean up our stored data
hass.data[DOMAIN].pop(entry.entry_id, None)
# Ensure hass.data[DOMAIN] is a dictionary before popping
if isinstance(hass.data.get(DOMAIN, {}), dict):
hass.data[DOMAIN].pop(entry.entry_id, None)
_LOGGER.info(f"hass.data[{DOMAIN}] after unload: {hass.data.get(DOMAIN)}")
_LOGGER.info("Finished unload of config entry")
return True
_logger.info(f"hass.data[DOMAIN] after removing entry data: {hass.data[DOMAIN]}")
_logger.info("Finished unload of config entry")
return True

View file

@ -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, CONF_NOTIFY_TARGET
from .const import DOMAIN, INTEGRATION_NAME, MODEL, CONF_NAME
import asyncio
# Set up logging
@ -50,12 +50,6 @@ 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
@ -123,12 +117,6 @@ 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
@ -196,12 +184,6 @@ 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
@ -269,12 +251,6 @@ 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
@ -342,12 +318,6 @@ 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
@ -415,12 +385,6 @@ 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
@ -488,12 +452,6 @@ 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
@ -561,12 +519,6 @@ 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
@ -594,15 +546,12 @@ class SAASLullabyStop(ButtonEntity):
async def async_setup_entry(hass, config_entry, async_add_entities):
notify_target = config_entry.data.get(CONF_NOTIFY_TARGET)
if not notify_target:
_LOGGER.warning("no notify_target configured; skipping button setup")
return
"""Set up the SAAS Sleep Tracking Start, Stop and Pause buttons from a config entry."""
# _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.get(CONF_NOTIFY_TARGET)
notify_target = config_entry.data['notify_target']
# Create instances of SAASSleepTrackingStart, SAASSleepTrackingStop and SAASSleepTrackingPause
entities = [

View file

@ -1,184 +1,139 @@
import logging
import traceback
import voluptuous as vol
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 .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 homeassistant import config_entries
from voluptuous import Schema, Required, Optional, In
from homeassistant.core import callback
from voluptuous import Schema, Required, In, Optional
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):
"""Handle the initial config flow for SAAS."""
VERSION = 2
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
async def async_step_user(self, user_input=None):
"""Initial setup step."""
"""Handle a flow initialized by the user."""
errors = {}
# discover available mobile_app notify services
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_")
}
# 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_')}
if user_input is not None:
# 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):
# Map the selected option back to the actual notify target name
user_input[CONF_NOTIFY_TARGET] = notify_targets[user_input[CONF_NOTIFY_TARGET]]
# Validate the user input here
# If the input is valid, create an entry
# 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"
if not errors:
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_create_entry(title=user_input[CONF_NAME], data=user_input)
return self.async_show_form(
step_id="user",
data_schema=Schema(schema),
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())),
}
),
errors=errors,
)
@staticmethod
@config_entries.callback
def async_get_options_flow(entry):
"""Return options flow handler."""
return OptionsFlowHandler(entry)
@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 SAAS options editing."""
def __init__(self, entry):
super().__init__()
self._config_entry = entry
"""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 form (edit)."""
current = dict(self._config_entry.data)
"""Manage the options."""
_logger.debug("Entering async_step_init with user_input: %s", user_input)
errors = {} # Define errors here
try:
# Fetch the initial configuration data
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
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_')}
if user_input is not None:
# Validate the user input here
# If the input is valid, create an entry
# 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"
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
user_input[CONF_NOTIFY_TARGET] = notify_targets[user_input[CONF_NOTIFY_TARGET]]
# Merge current_data with user_input
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)
if updated_data is not None:
self.hass.data[DOMAIN][self.config_entry.entry_id] = updated_data # Save updated data
# Update the entry data
self.hass.config_entries.async_update_entry(self.config_entry, data=updated_data)
# Send a signal to reload the integration
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))
# 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()}
if user_input is not None:
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=Schema(schema),
errors={},
)
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,24 +5,21 @@ 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_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_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
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
CONF_AWAKE_STATES = "awake_states" # Awake States
CONF_SLEEP_STATES = "sleep_states" # Sleep States
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
SENSOR_TYPES = {
"state": {"name": "State", "device_class": None},
@ -36,7 +33,7 @@ DAY_MAPPING = {
"wednesday": 4,
"thursday": 5,
"friday": 6,
"saturday": 7,
"saturday": 7
}
AVAILABLE_STATES = [
@ -70,9 +67,8 @@ 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",
@ -104,7 +100,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()}
@ -115,15 +111,14 @@ 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 = {
@ -134,7 +129,7 @@ LULLABY_MAPPING = {
DISTURBANCE_MAPPING = {
'apnea_alarm': "Apnea Alarm",
'antisnoring': "Antisnoring",
'antisnoring': "Antisnoring"
}
ALARM_EVENT_MAPPING = {
@ -148,12 +143,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"
}

View file

@ -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.2.1"
"version": "0.0.6"
}

View file

@ -10,9 +10,13 @@ from homeassistant.util import dt as dt_util
from homeassistant.components import mqtt
from .const import DOMAIN, CONF_NAME, CONF_TOPIC, CONF_AWAKE_STATES, CONF_SLEEP_STATES, CONF_AWAKE_DURATION, CONF_SLEEP_DURATION, INTEGRATION_NAME, MODEL, STATE_MAPPING, SOUND_MAPPING, DISTURBANCE_MAPPING, ALARM_EVENT_MAPPING, SLEEP_TRACKING_MAPPING, LULLABY_MAPPING, REVERSE_STATE_MAPPING, SLEEP_STAGE_MAPPING
_LOGGER = logging.getLogger(__name__)
_LOGGER.setLevel(logging.DEBUG)
import inspect
from datetime import datetime
class SAASSensor(RestoreEntity):
"""Representation of a SAAS - Sleep As Android Stats sensor."""
@ -101,8 +105,8 @@ class SAASAlarmEventSensor(RestoreEntity):
self._value2 = None
self._time = None
self.entry_id = entry_id
self._reset_timer = None
self._last_event = None
self._timeout_task = None
@property
def unique_id(self):
@ -129,256 +133,12 @@ class SAASAlarmEventSensor(RestoreEntity):
"model": MODEL,
}
@property
def extra_state_attributes(self):
"""Return the extra state attributes."""
if self._state is not None:
return {
"Last Event": self._last_event,
"Message": self._value2 if self._value2 else "No message received",
"Timestamp": self._value1 if self._value1 else "No timestamp received",
"Time": self._time.strftime('%H:%M') if self._time else "No time received",
"Date": self._time.strftime('%m/%d/%Y') if self._time else "No date received",
}
return {}
async def async_added_to_hass(self):
"""Run when entity about to be added."""
await super().async_added_to_hass()
# Load the previous state from the state machine
state = await self.async_get_last_state()
if state:
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
# Start the timeout task as soon as the sensor is loaded
self._timeout_task = asyncio.create_task(self.timeout())
async def message_received(msg):
"""Handle new MQTT messages."""
# Cancel the previous timeout task if it exists
if self._timeout_task:
self._timeout_task.cancel()
# Parse the incoming message
msg_json = json.loads(msg.payload)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Received MQTT message: {msg_json} for sensor {self.name}")
# Extract the 'value1' and 'value2' fields
value1 = msg_json.get('value1')
# Parse 'value1' as a datetime
if value1:
timestamp = int(value1) / 1000.0
self._time = datetime.fromtimestamp(timestamp)
self._value1 = value1
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Parsed 'value1' as datetime: {self._time} for sensor {self.name}")
# Extract the 'value2' field
value2 = msg_json.get('value2')
# Store 'value2' as the message if it exists
self._value2 = value2 if value2 else "None"
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Stored 'value2' as message: {self._value2} for sensor {self.name}")
# Extract the EVENT field
event = msg_json.get('event')
if event is None:
_LOGGER.warning(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): No 'event' key in the MQTT message: {msg_json} for sensor {self.name}")
return
# Use the mapping to convert the event to the corresponding state
new_state = self._mapping.get(event) # Default to "None" if no mapping found
if new_state is not None:
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {new_state} for sensor {self.name}")
self._state = new_state
self._last_event = new_state # Update the last event
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
self.async_schedule_update_ha_state()
# Create a new timeout task
self._timeout_task = asyncio.create_task(self.timeout())
# Subscribe to the topic from the user input
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."""
# Save the current state to the state machine
self._hass.states.async_set(self.entity_id, self._state)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Saved state: {self._state} for sensor {self.name}")
async def timeout(self):
"""Set the state to 'None' after a timeout."""
await asyncio.sleep(15)
self._state = "None"
self._last_event = "None"
_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."""
def __init__(self, hass, name, mapping, entry_id):
"""Initialize the sensor."""
self._state = None
self._name = name
self._hass = hass
self._mapping = mapping
self._value1 = None
self._value2 = None
self._time = None
self.entry_id = entry_id
self._last_event = None
self._timeout_task = None
@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"
@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,
}
@property
def extra_state_attributes(self):
"""Return the extra state attributes."""
return {
"Last Event": self._last_event,
"Message": self._value2 if self._value2 else "No message received",
"Timestamp": self._value1 if self._value1 else "No timestamp received",
"Time": self._time.strftime('%H:%M') if self._time else "No time received",
"Date": self._time.strftime('%m/%d/%Y') if self._time else "No date received",
@ -394,17 +154,118 @@ class SAASSoundSensor(RestoreEntity):
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
# Start the timeout task as soon as the sensor is loaded
self._timeout_task = asyncio.create_task(self.timeout())
async def reset_state(_):
"""Reset the state to None after a delay."""
self._state = 'None'
self.async_write_ha_state()
async def message_received(msg):
"""Handle new MQTT messages."""
# Cancel the previous timeout task if it exists
if self._timeout_task:
self._timeout_task.cancel()
# Cancel the previous state reset timer
if self._reset_timer:
self._reset_timer.cancel()
# Schedule a state reset after 15 seconds
self._reset_timer = async_call_later(self._hass, 15, reset_state)
# Parse the incoming message
msg_json = json.loads(msg.payload)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Received MQTT message: {msg_json} for sensor {self.name}")
# Extract the 'value1' and 'value2' fields
value1 = msg_json.get('value1')
# Parse 'value1' as a datetime
if value1:
timestamp = int(value1) / 1000.0
self._time = datetime.fromtimestamp(timestamp)
self._value1 = value1
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Parsed 'value1' as datetime: {self._time} for sensor {self.name}")
# Extract the 'value2' field
value2 = msg_json.get('value2')
# Store 'value2' as the message if it exists
self._value2 = value2 if value2 else "None"
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Stored 'value2' as message: {self._value2} for sensor {self.name}")
# Extract the EVENT field
event = msg_json.get('event')
if event is None:
_LOGGER.warning(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): No 'event' key in the MQTT message: {msg_json} for sensor {self.name}")
return
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Extracted Event {event} from message for sensor {self.name}")
# Use the mapping to convert the event to the corresponding state
new_state = self._mapping.get(event)
if new_state is not None:
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {new_state} for sensor {self.name}")
self._state = new_state
self._last_event = new_state # Update the last event
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
self.async_write_ha_state()
# Subscribe to the topic from the user input
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."""
# Save the current state to the state machine
self._hass.states.async_set(self.entity_id, self._state)
_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."""
def __init__(self, hass, name, mapping, entry_id):
"""Initialize the sensor."""
self._state = None
self._name = name
self._hass = hass
self._mapping = mapping
self.entry_id = entry_id
@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"
@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()
# Load the previous state from the state machine
state = await self.async_get_last_state()
if state:
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
async def message_received(msg):
"""Handle new MQTT messages."""
# Parse the incoming message
msg_json = json.loads(msg.payload)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Received MQTT message: {msg_json} for sensor {self.name}")
@ -416,35 +277,89 @@ class SAASSoundSensor(RestoreEntity):
return
# Use the mapping to convert the event to the corresponding state
new_state = self._mapping.get(event)
new_state = self._mapping.get(event, "None") # Default to "None" if no mapping found
if new_state is not None:
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {new_state} for sensor {self.name}")
self._state = new_state
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
self.async_schedule_update_ha_state()
# Subscribe to the topic from the user input
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."""
# Save the current state to the state machine
self._hass.states.async_set(self.entity_id, self._state)
_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."""
def __init__(self, hass, name, mapping, entry_id):
"""Initialize the sensor."""
self._state = None
self._name = name
self._hass = hass
self._mapping = mapping
self.entry_id = entry_id
@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"
@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()
# Load the previous state from the state machine
state = await self.async_get_last_state()
if state:
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
async def message_received(msg):
"""Handle new MQTT messages."""
# Parse the incoming message
msg_json = json.loads(msg.payload)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Received MQTT message: {msg_json} for sensor {self.name}")
# Extract the EVENT field
event = msg_json.get('event')
if event is None:
_LOGGER.warning(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): No 'event' key in the MQTT message: {msg_json} for sensor {self.name}")
return
# Use the mapping to convert the event to the corresponding state
new_state = self._mapping.get(event, "None") # Default to "None" if no mapping found
if new_state is not None:
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {new_state} for sensor {self.name}")
self._state = new_state
self._last_event = new_state # Update the last event
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
# Extract the 'value1' and 'value2' fields
value1 = msg_json.get('value1')
# Parse 'value1' as a datetime
if value1:
timestamp = int(value1) / 1000.0
self._time = datetime.fromtimestamp(timestamp)
self._value1 = value1
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Parsed 'value1' as datetime: {self._time} for sensor {self.name}")
# Extract the 'value2' field
value2 = msg_json.get('value2')
# Store 'value2' as the message if it exists
self._value2 = value2 if value2 else "None"
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Stored 'value2' as message: {self._value2} for sensor {self.name}")
self.async_schedule_update_ha_state()
# Create a new timeout task
self._timeout_task = asyncio.create_task(self.timeout())
# Subscribe to the topic from the user input
await async_subscribe(self._hass, self._hass.data[DOMAIN][self.entry_id][CONF_TOPIC], message_received)
@ -454,14 +369,6 @@ class SAASSoundSensor(RestoreEntity):
self._hass.states.async_set(self.entity_id, self._state)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Saved state: {self._state} for sensor {self.name}")
async def timeout(self):
"""Set the state to 'None' after a timeout."""
await asyncio.sleep(15)
self._state = "None"
self._last_event = "None"
_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 SAASSleepTrackingSensor(RestoreEntity):
"""Representation of a SAAS - Sleep As Android Stats sensor for Sleep Tracking."""
def __init__(self, hass, name, mapping, entry_id):
@ -539,25 +446,19 @@ class SAASSleepTrackingSensor(RestoreEntity):
class SAASDisturbanceSensor(RestoreEntity):
"""Representation of a SAAS - Sleep As Android Stats sensor for Disturbance Events."""
def __init__(self, hass, name, mapping, entry_id):
"""Initialize the sensor."""
self._state = None
self._name = name
self._hass = hass
self._mapping = mapping
self._value1 = None
self._value2 = None
self._time = None
self.entry_id = entry_id
self._last_event = None
self._timeout_task = None
@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."""
@ -567,7 +468,7 @@ class SAASDisturbanceSensor(RestoreEntity):
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def device_info(self):
"""Return information about the device."""
@ -577,16 +478,6 @@ class SAASDisturbanceSensor(RestoreEntity):
"manufacturer": INTEGRATION_NAME,
"model": MODEL,
}
@property
def extra_state_attributes(self):
"""Return the extra state attributes."""
return {
"Last Event": self._last_event,
"Timestamp": self._value1 if self._value1 else "No timestamp received",
"Time": self._time.strftime('%H:%M') if self._time else "No time received",
"Date": self._time.strftime('%m/%d/%Y') if self._time else "No date received",
}
async def async_added_to_hass(self):
"""Run when entity about to be added."""
@ -598,15 +489,8 @@ class SAASDisturbanceSensor(RestoreEntity):
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
# Start the timeout task as soon as the sensor is loaded
self._timeout_task = asyncio.create_task(self.timeout())
async def message_received(msg):
"""Handle new MQTT messages."""
# Cancel the previous timeout task if it exists
if self._timeout_task:
self._timeout_task.cancel()
# Parse the incoming message
msg_json = json.loads(msg.payload)
@ -620,34 +504,10 @@ class SAASDisturbanceSensor(RestoreEntity):
return
# Use the mapping to convert the event to the corresponding state
new_state = self._mapping.get(event)
if new_state is not None:
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {new_state} for sensor {self.name}")
self._state = new_state
self._last_event = new_state # Update the last event
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
# Extract the 'value1' and 'value2' fields
value1 = msg_json.get('value1')
# Parse 'value1' as a datetime
if value1:
timestamp = int(value1) / 1000.0
self._time = datetime.fromtimestamp(timestamp)
self._value1 = value1
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Parsed 'value1' as datetime: {self._time} for sensor {self.name}")
# Extract the 'value2' field
value2 = msg_json.get('value2')
# Store 'value2' as the message if it exists
self._value2 = value2 if value2 else "None"
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Stored 'value2' as message: {self._value2} for sensor {self.name}")
self.async_schedule_update_ha_state()
# Create a new timeout task
self._timeout_task = asyncio.create_task(self.timeout())
new_state = self._mapping.get(event, "None") # Default to "None" if no mapping found
self._state = new_state
_LOGGER.debug(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Set state to {new_state} for sensor {self.name}")
self.async_schedule_update_ha_state()
# Subscribe to the topic from the user input
await async_subscribe(self._hass, self._hass.data[DOMAIN][self.entry_id][CONF_TOPIC], message_received)
@ -658,14 +518,6 @@ class SAASDisturbanceSensor(RestoreEntity):
self._hass.states.async_set(self.entity_id, self._state)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Saved state: {self._state} for sensor {self.name}")
async def timeout(self):
"""Set the state to 'None' after a timeout."""
await asyncio.sleep(15)
self._state = "None"
self._last_event = "None"
_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 SAASLullabySensor(RestoreEntity):
"""Representation of a SAAS - Sleep As Android Stats sensor for Lullaby."""
def __init__(self, hass, name, mapping, entry_id):
@ -818,6 +670,8 @@ class SAASWakeStatusSensor(RestoreEntity):
def __init__(self, hass, name, awake_states, sleep_states, awake_duration, sleep_duration, mqtt_topic, entry_id):
self._state = None
self._name = name
self.awake_bucket = []
self.sleep_bucket = []
self.awake_duration = timedelta(seconds=awake_duration)
self.sleep_duration = timedelta(seconds=sleep_duration)
self.awake_states = awake_states
@ -825,8 +679,6 @@ class SAASWakeStatusSensor(RestoreEntity):
self.hass = hass
self.mqtt_topic = mqtt_topic
self.entry_id = entry_id
self.awake_timer = None
self.sleep_timer = None
@property
def unique_id(self):
@ -856,57 +708,93 @@ class SAASWakeStatusSensor(RestoreEntity):
def process_message(self, message):
try:
now = dt_util.utcnow() # Define 'now' before using it for logging
# Extract the 'event' from the incoming message
event = message.get('event')
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Extracted Event {event} from message.")
# Map the event to a known state, or "Unknown" if the event is not recognized
mapped_value = STATE_MAPPING.get(event, "Unknown")
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped {event} to {mapped_value}.")
# If the event could not be mapped to a known state, set the sensor state to "Unknown"
if mapped_value == "Unknown":
self._state = "Unknown"
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Event {event} could not be mapped to a known state. Setting sensor state to 'Unknown'.")
# If the mapped value is in the awake states, start or restart the awake timer and cancel the sleep timer
# If the mapped value is in the awake states, add it to the awake bucket and clear the sleep bucket
if mapped_value in self.awake_states:
if self.awake_timer:
self.awake_timer.cancel()
self.awake_timer = self.hass.loop.call_later(self.awake_duration.total_seconds(), self.set_state, "Awake")
if self.sleep_timer:
self.sleep_timer.cancel()
self.sleep_timer = None
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped value {mapped_value} is in awake states. Starting or restarting awake timer and cancelling sleep timer.")
# If the mapped value is in the sleep states, start or restart the sleep timer and cancel the awake timer
self.awake_bucket.append((mapped_value, now))
self.sleep_bucket = []
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped value {mapped_value} is in awake states. Adding to awake bucket and clearing sleep bucket.")
# If the mapped value is in the sleep states, add it to the sleep bucket and clear the awake bucket
elif mapped_value in self.sleep_states:
if self.sleep_timer:
self.sleep_timer.cancel()
self.sleep_timer = self.hass.loop.call_later(self.sleep_duration.total_seconds(), self.set_state, "Asleep")
if self.awake_timer:
self.awake_timer.cancel()
self.awake_timer = None
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped value {mapped_value} is in sleep states. Starting or restarting sleep timer and cancelling awake timer.")
self.sleep_bucket.append((mapped_value, now))
self.awake_bucket = []
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Mapped value {mapped_value} is in sleep states. Adding to sleep bucket and clearing awake bucket.")
except Exception as e:
_LOGGER.error(f"Error processing message: {e}")
async def async_update(self, _=None):
"""Update the state."""
now = dt_util.utcnow()
# If any message in the awake bucket has reached the awake duration, set the state to "Awake"
if self.awake_bucket and any(now - timestamp >= self.awake_duration for _, timestamp in self.awake_bucket):
if self._state != "Awake":
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): State changed to 'Awake'")
self._state = "Awake"
# If any message in the sleep bucket has reached the sleep duration, set the state to "Asleep"
elif self.sleep_bucket and any(now - timestamp >= self.sleep_duration for _, timestamp in self.sleep_bucket):
if self._state != "Asleep":
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): State changed to 'Asleep'")
self._state = "Asleep"
# Remove messages from the awake bucket that are older than the awake duration and log if a message is removed
self.awake_bucket = [(val, timestamp) for val, timestamp in self.awake_bucket if now - timestamp < self.awake_duration]
for val, timestamp in self.awake_bucket:
if now - timestamp >= self.awake_duration:
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Removed message from awake bucket.")
# Remove messages from the sleep bucket that are older than the sleep duration and log if a message is removed
self.sleep_bucket = [(val, timestamp) for val, timestamp in self.sleep_bucket if now - timestamp < self.sleep_duration]
for val, timestamp in self.sleep_bucket:
if now - timestamp >= self.sleep_duration:
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Removed message from sleep bucket.")
# Log the contents of the awake bucket if it is not empty
if self.awake_bucket:
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Awake bucket: {self.awake_bucket}")
# Log the contents of the sleep bucket if it is not empty
if self.sleep_bucket:
_LOGGER.debug(f"{dt_util.as_local(now).strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Sleep bucket: {self.sleep_bucket}")
def set_state(self, state):
self._state = state
self.async_schedule_update_ha_state()
async def interval_callback(self, _):
"""Wrapper function for async_track_time_interval."""
# Call the async_update method to update the state
await self.async_update()
async def async_added_to_hass(self):
"""Run when entity about to be added."""
await super().async_added_to_hass()
# Load the previous state from the state machine
state = await self.async_get_last_state()
if state:
self._state = state.state
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Loaded state: {self._state} for sensor {self.name}")
# Schedule the interval callback to run every second
async_track_time_interval(
self.hass,
self.interval_callback,
timedelta(seconds=1)
)
# Subscribe to the MQTT topic to receive messages
await mqtt.async_subscribe(
self.hass,
@ -919,7 +807,7 @@ class SAASWakeStatusSensor(RestoreEntity):
# Save the current state to the state machine
self.hass.states.async_set(self.entity_id, self._state)
_LOGGER.info(f"{datetime.now().strftime('%H:%M:%S:%f')} (Line {inspect.currentframe().f_lineno}): Saved state: {self._state} for sensor {self.name}")
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")
@ -934,7 +822,6 @@ 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),

View file

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