sleepd/custom_components/saas/sensor.py

835 lines
36 KiB
Python
Raw Normal View History

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)
class sleepdSensor(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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 sleepdAlarmEventSensor(RestoreEntity):
"""Representation of a sleepd - 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._last_event = None
self._timeout_task = None
@property
def unique_id(self):
"""Return a unique ID."""
return f"sleepd_alarm_event_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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."""
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 sleepdSoundSensor(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_sound_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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,
"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}")
# 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 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)
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)
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 sleepdSleepTrackingSensor(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_sleep_tracking_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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 sleepdDisturbanceSensor(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_disturbance_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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,
}
@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."""
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 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)
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)
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 sleepdLullabySensor(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_lullaby_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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 sleepdSleepStage(RestoreEntity):
"""Representation of a sleepd - 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"sleepd_sleep_stage_sensor_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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 sleepdWakeStatusSensor(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_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
self.awake_timer = None
self.sleep_timer = None
@property
def unique_id(self):
"""Return a unique ID."""
return f"sleepd_wake_status_{self._name}"
@property
def name(self):
"""Return the name of the sensor."""
return f"sleepd {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, start or restart the awake timer and cancel the sleep timer
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
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.")
except Exception as e:
_LOGGER.error(f"Error processing message: {e}")
def set_state(self, state):
self._state = state
self.async_schedule_update_ha_state()
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}")
# 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 sleepd 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 = [
sleepdSensor(hass, name, STATE_MAPPING, entry_id),
sleepdAlarmEventSensor(hass, name, ALARM_EVENT_MAPPING, entry_id),
sleepdSoundSensor(hass, name, SOUND_MAPPING, entry_id),
sleepdSleepTrackingSensor(hass, name, SLEEP_TRACKING_MAPPING, entry_id),
sleepdDisturbanceSensor(hass, name, DISTURBANCE_MAPPING, entry_id),
sleepdLullabySensor(hass, name, LULLABY_MAPPING, entry_id),
sleepdSleepStage(hass, name, SLEEP_STAGE_MAPPING, entry_id),
sleepdWakeStatusSensor(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()
2025-06-29 04:28:11 +00:00
async_add_entities(entities)