mirror of
https://github.com/sudoxnym/sleepd.git
synced 2026-04-14 19:47:29 +00:00
-Added persistent state through HA restart -Added Attributes to Alarm Event sensor for use in automations and template sensors.
837 lines
No EOL
37 KiB
Python
837 lines
No EOL
37 KiB
Python
import asyncio, json, logging, time, inspect, pytz
|
|
from datetime import timedelta, datetime, timezone
|
|
from collections import deque
|
|
from homeassistant.helpers.event import async_track_time_interval, async_call_later
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.restore_state import RestoreEntity
|
|
from homeassistant.components.mqtt import async_subscribe
|
|
from homeassistant.helpers.entity import Entity
|
|
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."""
|
|
|
|
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_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} State"
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the sensor."""
|
|
return self._state
|
|
|
|
@property
|
|
def device_info(self):
|
|
"""Return information about the device."""
|
|
return {
|
|
"identifiers": {(DOMAIN, self._name)},
|
|
"name": self._name,
|
|
"manufacturer": INTEGRATION_NAME,
|
|
"model": MODEL,
|
|
}
|
|
|
|
async def async_added_to_hass(self):
|
|
"""Run when entity about to be added."""
|
|
await super().async_added_to_hass()
|
|
|
|
# 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}")
|
|
|
|
# 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}")
|
|
return
|
|
|
|
# Use the mapping to convert the event to the corresponding state
|
|
new_state = self._mapping.get(event)
|
|
if new_state is not None:
|
|
self._state = new_state
|
|
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 SAASAlarmEventSensor(RestoreEntity):
|
|
"""Representation of a SAAS - Sleep As Android Stats sensor for Alarm 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._reset_timer = None
|
|
self._last_event = None
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a unique ID."""
|
|
return f"saas_alarm_event_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Alarm Event"
|
|
|
|
@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",
|
|
}
|
|
|
|
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 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 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}")
|
|
|
|
# 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
|
|
_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
|
|
_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 SAASSleepTrackingSensor(RestoreEntity):
|
|
"""Representation of a SAAS - Sleep As Android Stats sensor for Sleep Tracking."""
|
|
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_sleep_tracking_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Sleep Tracking"
|
|
|
|
@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) # Removed default to "None"
|
|
if new_state is not None: # Only update state if a mapping is 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)
|
|
|
|
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 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.entry_id = entry_id
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a unique ID."""
|
|
return f"saas_disturbance_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Disturbance"
|
|
|
|
@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
|
|
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 SAASLullabySensor(RestoreEntity):
|
|
"""Representation of a SAAS - Sleep As Android Stats sensor for Lullaby."""
|
|
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_lullaby_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Lullaby"
|
|
|
|
@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
|
|
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 SAASSleepStage(RestoreEntity):
|
|
"""Representation of a SAAS - Sleep As Android Stats sensor for Sleep Stage."""
|
|
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_sleep_stage_sensor_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Sleep Stage"
|
|
|
|
@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
|
|
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 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
|
|
self.sleep_states = sleep_states
|
|
self.hass = hass
|
|
self.mqtt_topic = mqtt_topic
|
|
self.entry_id = entry_id
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a unique ID."""
|
|
return f"saas_wake_status_{self._name}"
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the sensor."""
|
|
return f"SAAS {self._name} Wake Status"
|
|
|
|
@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,
|
|
}
|
|
|
|
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, add it to the awake bucket and clear the sleep bucket
|
|
if mapped_value in self.awake_states:
|
|
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:
|
|
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}")
|
|
|
|
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,
|
|
self.mqtt_topic,
|
|
lambda message: self.process_message(json.loads(message.payload))
|
|
)
|
|
|
|
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 async_setup_entry(hass, entry, async_add_entities):
|
|
"""Set up the SAAS sensor platform from a config entry."""
|
|
name = entry.data.get(CONF_NAME, "Default Name")
|
|
topic = entry.data.get(CONF_TOPIC)
|
|
awake_states = entry.data.get(CONF_AWAKE_STATES)
|
|
sleep_states = entry.data.get(CONF_SLEEP_STATES)
|
|
awake_duration = entry.data.get(CONF_AWAKE_DURATION)
|
|
sleep_duration = entry.data.get(CONF_SLEEP_DURATION)
|
|
entry_id = entry.entry_id
|
|
hass.data[DOMAIN][entry.entry_id] = entry.data
|
|
|
|
entities = [
|
|
SAASSensor(hass, name, STATE_MAPPING, entry_id),
|
|
SAASAlarmEventSensor(hass, name, ALARM_EVENT_MAPPING, entry_id),
|
|
SAASSoundSensor(hass, name, SOUND_MAPPING, entry_id),
|
|
SAASSleepTrackingSensor(hass, name, SLEEP_TRACKING_MAPPING, entry_id),
|
|
SAASDisturbanceSensor(hass, name, DISTURBANCE_MAPPING, entry_id),
|
|
SAASLullabySensor(hass, name, LULLABY_MAPPING, entry_id),
|
|
SAASSleepStage(hass, name, SLEEP_STAGE_MAPPING, entry_id),
|
|
SAASWakeStatusSensor(hass, name, awake_states, sleep_states, awake_duration, sleep_duration, topic, entry_id)
|
|
]
|
|
|
|
for entity in entities:
|
|
if hasattr(entity, "async_setup"):
|
|
await entity.async_setup()
|
|
|
|
async_add_entities(entities) |