diff --git a/custom_components/saas/__init__.py b/custom_components/saas/__init__.py new file mode 100644 index 0000000..f72cc8a --- /dev/null +++ b/custom_components/saas/__init__.py @@ -0,0 +1,64 @@ +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__) + +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") + return True + +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}") + hass.data.setdefault(DOMAIN, {}) + + if entry.entry_id not in hass.data[DOMAIN] and entry.data: + hass.data[DOMAIN][entry.entry_id] = entry.data + + _logger.info(f"hass.data[DOMAIN] after adding entry data: {hass.data[DOMAIN]}") + + # 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(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): + """Unload a config entry.""" + _logger.info(f"Starting unload of config entry with ID: {entry.entry_id}") + + # Remove the sensor platform + _logger.info("Removing sensor platform") + await hass.config_entries.async_forward_entry_unload(entry, "sensor") + + # 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 removing entry data: {hass.data[DOMAIN]}") + _logger.info("Finished unload of config entry") + return True \ No newline at end of file diff --git a/custom_components/saas/button.py b/custom_components/saas/button.py new file mode 100644 index 0000000..945dcb2 --- /dev/null +++ b/custom_components/saas/button.py @@ -0,0 +1,575 @@ +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 +import asyncio + +# Set up logging +# _LOGGER = logging.getLogger(__name__) + +class SAASSleepTrackingStart(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Sleep Tracking Start button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target # Store notify_target as an instance variable + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_sleep_tracking_start_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Sleep Tracking Start" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.alarmclock.START_SLEEP_TRACK", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASSleepTrackingStop(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Sleep Tracking Stop button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_sleep_tracking_stop_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Sleep Tracking Stop" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.alarmclock.STOP_SLEEP_TRACK", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASSleepTrackingPause(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Sleep Tracking Pause button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_sleep_tracking_pause_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Sleep Tracking Pause" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.ACTION_PAUSE_TRACKING", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASSleepTrackingResume(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Sleep Tracking Pause button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_sleep_tracking_resume_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Sleep Tracking Resume" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.ACTION_RESUME_TRACKING", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASAlarmClockSnooze(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Alarm Clock Snooze button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_alarm_clock_snooze_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Alarm Clock Snooze" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.alarmclock.ALARM_SNOOZE", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASAlarmClockDisable(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Alarm Clock Disable button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_alarm_clock_disable_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Alarm Clock Disable" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.alarmclock.ALARM_DISMISS_CAPTCHA", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASSleepTrackingStartWithAlarm(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Sleep Tracking Start with Alarm button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_sleep_tracking_start_with_alarm_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Sleep Tracking Start with Alarm" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.alarmclock.START_SLEEP_TRACK_WITH_IDEAL_ALARM_ACTION", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + +class SAASLullabyStop(ButtonEntity): + def __init__(self, hass, name, notify_target): + """Initialize the button.""" + # _LOGGER.debug("Initializing SAAS Lullaby Stop button with name: %s", name) + self._hass = hass + self._name = name + self._notify_target = notify_target + self._state = "off" + # _LOGGER.debug("Button initialized with state: %s", self._state) + + @property + def unique_id(self): + """Return a unique ID.""" + unique_id = f"saas_lullaby_stop_{self._name}" + # _LOGGER.debug("Getting unique ID for the button: %s", unique_id) + return unique_id + + @property + def name(self): + """Return the name of the button.""" + name = f"SAAS {self._name} Lullaby Stop" + # _LOGGER.debug("Getting name of the button: %s", name) + return name + + @property + def is_on(self): + """Return true if the button is on.""" + # _LOGGER.debug("Checking if the button is on. Current state: %s", self._state) + return self._state == "on" + + @property + def device_info(self): + """Return information about the device.""" + device_info = { + "identifiers": {(DOMAIN, self._name)}, + "name": self._name, + "manufacturer": INTEGRATION_NAME, + "model": MODEL, + } + # _LOGGER.debug("Getting device information: %s", device_info) + return device_info + + def press(self): + """Press the button.""" + service_name = self._notify_target # Remove the "notify." prefix + + service_data = { + "message": "command_broadcast_intent", + "data": { + "intent_package_name": "com.urbandroid.sleep", + "intent_action": "com.urbandroid.sleep.ACTION_LULLABY_STOP_PLAYBACK", + }, + } + # _LOGGER.debug("Pressing the button with service call:\n" + # "service: %s\n" + # "data:\n" + # "%s", service_name, service_data) + self._hass.services.call( + "notify", + service_name, + service_data, + blocking=True, + ) + self._state = "on" + self.schedule_update_ha_state() + # _LOGGER.debug("Button pressed. Current state: %s", self._state) + + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the SAAS Sleep Tracking Start, Stop and Pause buttons from a config entry.""" + # _LOGGER.debug("Setting up SAAS Sleep Tracking buttons from a config entry with data: %s", config_entry.data) + + # Extract the necessary data from config_entry.data + name = config_entry.data[CONF_NAME] + notify_target = config_entry.data['notify_target'] + + # Create instances of SAASSleepTrackingStart, SAASSleepTrackingStop and SAASSleepTrackingPause + entities = [ + SAASSleepTrackingStart(hass, name, notify_target), + SAASSleepTrackingStop(hass, name, notify_target), + SAASSleepTrackingPause(hass, name, notify_target), + SAASSleepTrackingResume(hass, name, notify_target), + SAASAlarmClockSnooze(hass, name, notify_target), + SAASAlarmClockDisable(hass, name, notify_target), + SAASSleepTrackingStartWithAlarm(hass, name, notify_target), + SAASLullabyStop(hass, name, notify_target), + ] + + # Call async_setup on each entity if it has that method + for entity in entities: + if hasattr(entity, "async_setup"): + await entity.async_setup() + + # Add the entities + async_add_entities(entities) + # _LOGGER.debug("SAAS Sleep Tracking buttons set up successfully") \ No newline at end of file diff --git a/custom_components/saas/config_flow.py b/custom_components/saas/config_flow.py new file mode 100644 index 0000000..9bd25fe --- /dev/null +++ b/custom_components/saas/config_flow.py @@ -0,0 +1,139 @@ +import logging +import traceback +import voluptuous as vol +from .const import DOMAIN, CONF_NAME, CONF_TOPIC, CONF_QOS, AVAILABLE_STATES, CONF_AWAKE_DURATION, CONF_SLEEP_DURATION, CONF_AWAKE_STATES, CONF_SLEEP_STATES, DEFAULT_AWAKE_DURATION, DEFAULT_SLEEP_DURATION, DEFAULT_AWAKE_STATES, DEFAULT_SLEEP_STATES, CONF_NOTIFY_TARGET +from homeassistant import config_entries +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__) + +class MyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + # 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 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) + + return self.async_show_form( + step_id="user", + data_schema=Schema( + { + Required(CONF_NAME): str, + Required(CONF_TOPIC): str, + Required(CONF_QOS, default=0): In([0, 1, 2]), + Required(CONF_AWAKE_DURATION, default=DEFAULT_AWAKE_DURATION): int, + Required(CONF_SLEEP_DURATION, default=DEFAULT_SLEEP_DURATION): int, + Required(CONF_AWAKE_STATES, default=DEFAULT_AWAKE_STATES): cv.multi_select(AVAILABLE_STATES), + Required(CONF_SLEEP_STATES, default=DEFAULT_SLEEP_STATES): cv.multi_select(AVAILABLE_STATES), + Optional(CONF_NOTIFY_TARGET): vol.In(list(notify_targets.keys())), + } + ), + errors=errors, + ) + + @staticmethod + @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 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.""" + _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)) + + 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())), + } + ) \ No newline at end of file diff --git a/custom_components/saas/const.py b/custom_components/saas/const.py new file mode 100644 index 0000000..4d75d51 --- /dev/null +++ b/custom_components/saas/const.py @@ -0,0 +1,154 @@ +"""Constants for the SAAS - Sleep As Android Stats integration.""" + +DOMAIN = "saas" + +INTEGRATION_NAME = "SAAS - Sleep As Android Stats" +MODEL = "SAAS - Version 0.0.1" + +CONF_NAME = "name" # Name of the Integration +CONF_TOPIC = "topic_template" # MQTT Topic for Sleep As Android Events +CONF_QOS = "qos" # Quality of Service +CONF_AWAKE_DURATION = "awake_duration" # Awake Duration +CONF_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 + +SENSOR_TYPES = { + "state": {"name": "State", "device_class": None}, + "awake": {"name": "Awake", "device_class": "motion"}, +} + +DAY_MAPPING = { + "sunday": 1, + "monday": 2, + "tuesday": 3, + "wednesday": 4, + "thursday": 5, + "friday": 6, + "saturday": 7 +} + +AVAILABLE_STATES = [ + 'Unknown', + 'Alarm Alert Dismiss', + 'Alarm Alert Start', + 'Alarm Rescheduled', + 'Alarm Skip Next', + 'Alarm Snooze Canceled', + 'Alarm Snooze Clicked', + 'Antisnoring', + 'Apnea Alarm', + 'Awake', + 'Before Alarm', + 'Before Smart Period', + 'Deep Sleep', + 'Light Sleep', + 'Lullaby Start', + 'Lullaby Stop', + 'Lullaby Volume Down', + 'Not Awake', + 'Rem', + 'Show Skip Next Alarm', + 'Sleep Tracking Paused', + 'Sleep Tracking Resumed', + 'Sleep Tracking Started', + 'Sleep Tracking Stopped', + 'Smart Period', + 'Sound Event Baby', + 'Sound Event Cough', + 'Sound Event Laugh', + 'Sound Event Snore', + 'Sound Event Talk', + 'Time for Bed' +] +STATE_MAPPING = { + "unknown": "Unknown", + "sleep_tracking_started": "Sleep Tracking Started", + "sleep_tracking_stopped": "Sleep Tracking Stopped", + "sleep_tracking_paused": "Sleep Tracking Paused", + "sleep_tracking_resumed": "Sleep Tracking Resumed", + "alarm_snooze_clicked": "Alarm Snoozed", + "alarm_snooze_canceled": "Snooze Canceled", + "time_to_bed_alarm_alert": "Time for Bed", + "alarm_alert_start": "Alarm Alert Started", + "alarm_alert_dismiss": "Alarm Dismissed", + "alarm_skip_next": "Skip Next Alarm", + "show_skip_next_alarm": "Show Skip Next Alarm", + "rem": "REM", + "smart_period": "Smart Period", + "before_smart_period": "Before Smart Period", + "lullaby_start": "Lullaby Start", + "lullaby_stop": "Lullaby Stop", + "lullaby_volume_down": "Lullaby Volume Down", + "deep_sleep": "Deep Sleep", + "light_sleep": "Light Sleep", + "awake": "Awake", + "not_awake": "Not Awake", + "apnea_alarm": "Apnea Alarm", + "antisnoring": "Antisnoring", + "before_alarm": "Before Alarm", + "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", + "alarm_rescheduled": "Alarm Rescheduled" +} + +REVERSE_STATE_MAPPING = {v: k for k, v in STATE_MAPPING.items()} + +SLEEP_STAGE_MAPPING = { + "unknown": "Unknown", + "rem": "REM", + "deep_sleep": "Deep Sleep", + "light_sleep": "Light Sleep", + "awake": "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" +} + +LULLABY_MAPPING = { + "lullaby_start": "Lullaby Start", + "lullaby_stop": "Lullaby Stop", + "lullaby_volume_down": "Lullaby Volume Down", +} + +DISTURBANCE_MAPPING = { + 'apnea_alarm': "Apnea Alarm", + 'antisnoring': "Antisnoring" +} + +ALARM_EVENT_MAPPING = { + 'before_alarm': "Before Alarm", + 'alarm_snooze_clicked': "Alarm Snoozed", + 'alarm_snooze_canceled': "Snooze Canceled", + 'time_to_bed_alarm_alert': "Time for Bed", + 'alarm_alert_start': "Alarm Alert Started", + 'alarm_alert_dismiss': "Alarm Dismissed", + 'alarm_skip_next': "Skip Next Alarm", + 'show_skip_next_alarm': "Show Skip Next Alarm", + 'smart_period': "Smart Period", + 'before_smart_period': "Before Smart Period", + "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" +} diff --git a/custom_components/saas/manifest.json b/custom_components/saas/manifest.json new file mode 100644 index 0000000..f5c0398 --- /dev/null +++ b/custom_components/saas/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "saas", + "name": "SAAS - Sleep As Android Status", + "codeowners": ["@sudoxnym"], + "config_flow": true, + "dependencies": ["mqtt", "mobile_app"], + "documentation": "https://www.github.com/sudoxnym/saas", + "issue_tracker": "https://www.github.com/sudoxnym/saas/issues", + "version": "0.0.6" +} diff --git a/custom_components/saas/sensor.py b/custom_components/saas/sensor.py new file mode 100644 index 0000000..6bac1f7 --- /dev/null +++ b/custom_components/saas/sensor.py @@ -0,0 +1,837 @@ +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) \ No newline at end of file diff --git a/custom_components/saas/services.py b/custom_components/saas/services.py new file mode 100644 index 0000000..d61ad18 --- /dev/null +++ b/custom_components/saas/services.py @@ -0,0 +1,73 @@ +import logging +from homeassistant.core import ServiceCall, HomeAssistant +from .const import DOMAIN, DAY_MAPPING + +_LOGGER = logging.getLogger(__name__) + +class SAASService: + def __init__(self, hass, name, notify_target): + self._hass = hass + self._name = name + self._notify_target = notify_target + _LOGGER.debug(f"SAASService initialized with name: {name}, notify_target: {notify_target}") + + async def call_service(self, service_call: ServiceCall): + """Call the service.""" + service_name = self._notify_target # Remove the "notify." prefix + + # Extract parameters with default values + params = ['message', 'day', 'hour', 'minute'] + defaults = ['Default Message', 'monday', 0, 0] + message, day, hour, minute = (service_call.data.get(param, default) for param, default in zip(params, defaults)) + + _LOGGER.debug(f"Extracted parameters from service call: message: {message}, day: {day}, hour: {hour}, minute: {minute}") + + # Convert day name to number + day_number = DAY_MAPPING.get(day.lower(), day) + + _LOGGER.debug(f"Converted day name to number: {day_number}") + + _LOGGER.info(f"Service called with message: {message}, day: {day_number}, hours: {hour}, minutes: {minute}") + + service_data = { + "message": "command_activity", + "data": { + "intent_action": "android.intent.action.SET_ALARM", + "intent_extras": f"android.intent.extra.alarm.SKIP_UI:true,android.intent.extra.alarm.MESSAGE:{message},android.intent.extra.alarm.DAYS:{day_number}:ArrayList,android.intent.extra.alarm.HOUR:{hour},android.intent.extra.alarm.MINUTES:{minute}" + }, + } + + _LOGGER.debug(f"Prepared service data: {service_data}") + + try: + await self._hass.services.async_call( + "notify", + service_name, + service_data, + blocking=True, + ) + _LOGGER.info(f"Service call completed") + except Exception as e: + _LOGGER.error(f"Error occurred while calling service: {e}") + +async def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for the SAAS component.""" + _LOGGER.info(f"Setting up services for {DOMAIN}") + # Register the service for each entry + for entry_id, entry_data in hass.data.get(DOMAIN, {}).items(): + if entry_data: + name = entry_data.get('name', 'default name') + notify_target = entry_data.get('notify_target', 'default notify target') + if notify_target: # Only register the service if notify_target was chosen + _LOGGER.debug(f"Found notify_target: {notify_target} for name: {name}. Registering service.") + saas_service = SAASService(hass, name, notify_target) + try: + hass.services.async_register(DOMAIN, f'saas_{name}_alarm_set', saas_service.call_service) + _LOGGER.info(f"Registered service: saas_{name}_alarm_set") + except Exception as e: + _LOGGER.error(f"Error occurred while registering service: {e}") + else: + _LOGGER.warning(f"No notify_target found for name: {name}. Skipping service registration.") + else: + _LOGGER.warning(f"No entry data found for entry_id: {entry_id}") + _LOGGER.info(f"Finished setting up services for {DOMAIN}") \ No newline at end of file diff --git a/custom_components/saas/translations/ar.json b/custom_components/saas/translations/ar.json new file mode 100644 index 0000000..1e3a4a8 --- /dev/null +++ b/custom_components/saas/translations/ar.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - حالة Sleep As Android", + "config": { + "step": { + "user": { + "title": "التكوين لـ SAAS - حالة Sleep As Android", + "description": "تكوين الإعدادات الأساسية لتكامل SAAS.", + "data": { + "name": "الاسم للمستشعر", + "topic_template": "موضوع MQTT لأحداث Sleep As Android", + "qos": "جودة الخدمة (QoS) لـ MQTT", + "awake_duration": "مدة اليقظة", + "sleep_duration": "مدة النوم", + "awake_states": "حالات اليقظة*", + "sleep_states": "حالات النوم*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "إعدادات تكامل SAAS", + "description": "تكوين الإعدادات الأساسية لتكامل SAAS.", + "data": { + "name": "الاسم للمستشعر", + "topic_template": "موضوع MQTT (من Sleep As Android)", + "qos": "جودة الخدمة (QoS) لـ MQTT", + "awake_duration": "مدة اليقظة", + "sleep_duration": "مدة النوم", + "awake_states": "حالات اليقظة*", + "sleep_states": "حالات النوم*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "غير معروف", + "Sleep Tracking Started": "بدأ تتبع النوم", + "Sleep Tracking Stopped": "توقف تتبع النوم", + "Sleep Tracking Paused": "تم إيقاف تتبع النوم مؤقتًا", + "Sleep Tracking Resumed": "استأنف تتبع النوم", + "Alarm Snoozed": "تأجيل الإنذار", + "Snooze Canceled": "تم إلغاء التأجيل", + "Time for Bed": "وقت النوم", + "Alarm Alert Started": "بدأ تنبيه الإنذار", + "Alarm Dismissed": "تم تجاهل الإنذار", + "Skip Next Alarm": "تخطي الإنذار التالي", + "Show Skip Next Alarm": "إظهار تخطي الإنذار التالي", + "REM": "REM", + "Smart Period": "الفترة الذكية", + "Before Smart Period": "قبل الفترة الذكية", + "Lullaby Start": "بداية التهليل", + "Lullaby Stop": "توقف التهليل", + "Lullaby Volume Down": "خفض صوت التهليل", + "Deep Sleep": "نوم عميق", + "Light Sleep": "نوم خفيف", + "Awake": "مستيقظ", + "Not Awake": "غير مستيقظ", + "Apnea Alarm": "إنذار الأنابيب", + "Antisnoring": "مضاد للشخير", + "Before Alarm": "قبل الإنذار", + "Snore Detected": "تم اكتشاف الشخير", + "Talk Detected": "تم اكتشاف الكلام", + "Cough Detected": "تم اكتشاف السعال", + "Baby Cry Detected": "تم اكتشاف بكاء الطفل", + "Laugh Detected": "تم اكتشاف الضحك", + "Alarm Rescheduled": "تم إعادة جدولة الإنذار", + "None": "لا شيء" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/de.json b/custom_components/saas/translations/de.json new file mode 100644 index 0000000..dea54ce --- /dev/null +++ b/custom_components/saas/translations/de.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Sleep As Android Status", + "config": { + "step": { + "user": { + "title": "Konfiguration für SAAS - Sleep As Android Status", + "description": "Konfigurieren Sie die grundlegenden Einstellungen für die SAAS-Integration.", + "data": { + "name": "Name für Sensor", + "topic_template": "MQTT-Thema für Sleep As Android Ereignisse", + "qos": "Quality of Service (QoS) für MQTT", + "awake_duration": "Wachdauer", + "sleep_duration": "Schlafdauer", + "awake_states": "Wachzustände*", + "sleep_states": "Schlafzustände*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS-Integrationseinstellungen", + "description": "Konfigurieren Sie die grundlegenden Einstellungen für die SAAS-Integration.", + "data": { + "name": "Name für Sensor", + "topic_template": "MQTT-Thema (von Sleep As Android)", + "qos": "MQTT Quality of Service (QoS)", + "awake_duration": "Wachdauer", + "sleep_duration": "Schlafdauer", + "awake_states": "Wachzustände*", + "sleep_states": "Schlafzustände*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Unbekannt", + "Sleep Tracking Started": "Schlafüberwachung Gestartet", + "Sleep Tracking Stopped": "Schlafüberwachung Gestoppt", + "Sleep Tracking Paused": "Schlafüberwachung Pausiert", + "Sleep Tracking Resumed": "Schlafüberwachung Fortgesetzt", + "Alarm Snoozed": "Alarm Snoozed", + "Snooze Canceled": "Snooze Abgebrochen", + "Time for Bed": "Zeit fürs Bett", + "Alarm Alert Started": "Alarmalarm Gestartet", + "Alarm Dismissed": "Alarm Abgewiesen", + "Skip Next Alarm": "Nächsten Alarm Überspringen", + "Show Skip Next Alarm": "Zeige 'Nächsten Alarm Überspringen'", + "REM": "REM", + "Smart Period": "Intelligenter Zeitraum", + "Before Smart Period": "Vor Intelligenter Zeitraum", + "Lullaby Start": "Wiegenlied Start", + "Lullaby Stop": "Wiegenlied Stop", + "Lullaby Volume Down": "Wiegenlied Lautstärke Runter", + "Deep Sleep": "Tiefschlaf", + "Light Sleep": "Leichter Schlaf", + "Awake": "Wach", + "Not Awake": "Nicht Wach", + "Apnea Alarm": "Apnoe Alarm", + "Antisnoring": "Antischnarchen", + "Before Alarm": "Vor Alarm", + "Snore Detected": "Schnarchen Erkannt", + "Talk Detected": "Gespräch Erkannt", + "Cough Detected": "Husten Erkannt", + "Baby Cry Detected": "Babyweinen Erkannt", + "Laugh Detected": "Lachen Erkannt", + "Alarm Rescheduled": "Alarm Neu Geplant", + "None": "Keine" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/en.json b/custom_components/saas/translations/en.json new file mode 100644 index 0000000..630e021 --- /dev/null +++ b/custom_components/saas/translations/en.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Sleep As Android Status", + "config": { + "step": { + "user": { + "title": "Configuration for SAAS - Sleep As Android Status", + "description": "Configure the basic settings for the SAAS integration.", + "data": { + "name": "Name for Sensor", + "topic_template": "MQTT Topic for Sleep As Android Events", + "qos": "Quality of Service (QoS) for MQTT", + "awake_duration": "Awake Duration", + "sleep_duration": "Asleep Duration", + "awake_states": "Awake States*", + "sleep_states": "Asleep States*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS Integration Settings", + "description": "Configure the basic settings for the SAAS integration.", + "data": { + "name": "Name for Sensor", + "topic_template": "MQTT Topic (from Sleep As Android)", + "qos": "MQTT Quality of Service (QoS)", + "awake_duration": "Awake Duration", + "sleep_duration": "Asleep Duration", + "awake_states": "Awake States*", + "sleep_states": "Asleep States*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Unknown", + "Sleep Tracking Started": "Sleep Tracking Started", + "Sleep Tracking Stopped": "Sleep Tracking Stopped", + "Sleep Tracking Paused": "Sleep Tracking Paused", + "Sleep Tracking Resumed": "Sleep Tracking Resumed", + "Alarm Snoozed": "Alarm Snoozed", + "Snooze Canceled": "Snooze Canceled", + "Time for Bed": "Time for Bed", + "Alarm Alert Started": "Alarm Alert Started", + "Alarm Dismissed": "Alarm Dismissed", + "Skip Next Alarm": "Skip Next Alarm", + "Show Skip Next Alarm": "Show Skip Next Alarm", + "REM": "REM", + "Smart Period": "Smart Period", + "Before Smart Period": "Before Smart Period", + "Lullaby Start": "Lullaby Start", + "Lullaby Stop": "Lullaby Stop", + "Lullaby Volume Down": "Lullaby Volume Down", + "Deep Sleep": "Deep Sleep", + "Light Sleep": "Light Sleep", + "Awake": "Awake", + "Not Awake": "Not Awake", + "Apnea Alarm": "Apnea Alarm", + "Antisnoring": "Antisnoring", + "Before Alarm": "Before Alarm", + "Snore Detected": "Snore Detected", + "Talk Detected": "Talk Detected", + "Cough Detected": "Cough Detected", + "Baby Cry Detected": "Baby Cry Detected", + "Laugh Detected": "Laugh Detected", + "Alarm Rescheduled": "Alarm Rescheduled", + "None": "None" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/es.json b/custom_components/saas/translations/es.json new file mode 100644 index 0000000..c600e23 --- /dev/null +++ b/custom_components/saas/translations/es.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Estado de Sleep As Android", + "config": { + "step": { + "user": { + "title": "Configuración para SAAS - Estado de Sleep As Android", + "description": "Configura los ajustes básicos para la integración de SAAS.", + "data": { + "name": "Nombre para el Sensor", + "topic_template": "Tema MQTT para Eventos de Sleep As Android", + "qos": "Calidad de Servicio (QoS) para MQTT", + "awake_duration": "Duración Despierto", + "sleep_duration": "Duración Durmiendo", + "awake_states": "Estados Despierto*", + "sleep_states": "Estados Durmiendo*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Configuración de Integración SAAS", + "description": "Configura los ajustes básicos para la integración de SAAS.", + "data": { + "name": "Nombre para el Sensor", + "topic_template": "Tema MQTT (de Sleep As Android)", + "qos": "Calidad de Servicio (QoS) de MQTT", + "awake_duration": "Duración Despierto", + "sleep_duration": "Duración Durmiendo", + "awake_states": "Estados Despierto*", + "sleep_states": "Estados Durmiendo*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Desconocido", + "Sleep Tracking Started": "Seguimiento del Sueño Iniciado", + "Sleep Tracking Stopped": "Seguimiento del Sueño Detenido", + "Sleep Tracking Paused": "Seguimiento del Sueño Pausado", + "Sleep Tracking Resumed": "Seguimiento del Sueño Reanudado", + "Alarm Snoozed": "Alarma en Snooze", + "Snooze Canceled": "Snooze Cancelado", + "Time for Bed": "Hora de Dormir", + "Alarm Alert Started": "Alerta de Alarma Iniciada", + "Alarm Dismissed": "Alarma Desactivada", + "Skip Next Alarm": "Saltar Próxima Alarma", + "Show Skip Next Alarm": "Mostrar Saltar Próxima Alarma", + "REM": "REM", + "Smart Period": "Periodo Inteligente", + "Before Smart Period": "Antes del Periodo Inteligente", + "Lullaby Start": "Inicio de Canción de Cuna", + "Lullaby Stop": "Fin de Canción de Cuna", + "Lullaby Volume Down": "Bajar Volumen de Canción de Cuna", + "Deep Sleep": "Sueño Profundo", + "Light Sleep": "Sueño Ligero", + "Awake": "Despierto", + "Not Awake": "No Despierto", + "Apnea Alarm": "Alarma de Apnea", + "Antisnoring": "Antirronquidos", + "Before Alarm": "Antes de la Alarma", + "Snore Detected": "Ronquido Detectado", + "Talk Detected": "Habla Detectada", + "Cough Detected": "Tos Detectada", + "Baby Cry Detected": "Llanto de Bebé Detectado", + "Laugh Detected": "Risa Detectada", + "Alarm Rescheduled": "Alarma Re-programada", + "None": "Ninguno" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/fr.json b/custom_components/saas/translations/fr.json new file mode 100644 index 0000000..30becb7 --- /dev/null +++ b/custom_components/saas/translations/fr.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Statut de Sleep As Android", + "config": { + "step": { + "user": { + "title": "Configuration pour SAAS - Statut de Sleep As Android", + "description": "Configurez les paramètres de base pour l'intégration de SAAS.", + "data": { + "name": "Nom pour le Capteur", + "topic_template": "Sujet MQTT pour les Événements de Sleep As Android", + "qos": "Qualité de Service (QoS) pour MQTT", + "awake_duration": "Durée Éveillé", + "sleep_duration": "Durée Endormi", + "awake_states": "États Éveillé*", + "sleep_states": "États Endormi*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Paramètres d'Intégration SAAS", + "description": "Configurez les paramètres de base pour l'intégration de SAAS.", + "data": { + "name": "Nom pour le Capteur", + "topic_template": "Sujet MQTT (de Sleep As Android)", + "qos": "Qualité de Service (QoS) de MQTT", + "awake_duration": "Durée Éveillé", + "sleep_duration": "Durée Endormi", + "awake_states": "États Éveillé*", + "sleep_states": "États Endormi*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Inconnu", + "Sleep Tracking Started": "Suivi du Sommeil Commencé", + "Sleep Tracking Stopped": "Suivi du Sommeil Arrêté", + "Sleep Tracking Paused": "Suivi du Sommeil en Pause", + "Sleep Tracking Resumed": "Suivi du Sommeil Repris", + "Alarm Snoozed": "Alarme en Snooze", + "Snooze Canceled": "Snooze Annulé", + "Time for Bed": "Heure du Coucher", + "Alarm Alert Started": "Alerte d'Alarme Commencée", + "Alarm Dismissed": "Alarme Dismissed", + "Skip Next Alarm": "Passer la Prochaine Alarme", + "Show Skip Next Alarm": "Montrer Passer la Prochaine Alarme", + "REM": "REM", + "Smart Period": "Période Intelligente", + "Before Smart Period": "Avant la Période Intelligente", + "Lullaby Start": "Début de Berceuse", + "Lullaby Stop": "Fin de Berceuse", + "Lullaby Volume Down": "Diminuer le Volume de la Berceuse", + "Deep Sleep": "Sommeil Profond", + "Light Sleep": "Sommeil Léger", + "Awake": "Éveillé", + "Not Awake": "Pas Éveillé", + "Apnea Alarm": "Alarme d'Apnée", + "Antisnoring": "Anti-ronflement", + "Before Alarm": "Avant l'Alarme", + "Snore Detected": "Ronflement Détecté", + "Talk Detected": "Parole Détectée", + "Cough Detected": "Toux Détectée", + "Baby Cry Detected": "Pleurs de Bébé Détectés", + "Laugh Detected": "Rire Détecté", + "Alarm Rescheduled": "Alarme Re-programmée", + "None": "Aucun" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/it.json b/custom_components/saas/translations/it.json new file mode 100644 index 0000000..c0e05d0 --- /dev/null +++ b/custom_components/saas/translations/it.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Stato di Sleep As Android", + "config": { + "step": { + "user": { + "title": "Configurazione per SAAS - Stato di Sleep As Android", + "description": "Configura le impostazioni di base per l'integrazione SAAS.", + "data": { + "name": "Nome per Sensore", + "topic_template": "Topic MQTT per Eventi Sleep As Android", + "qos": "Quality of Service (QoS) per MQTT", + "awake_duration": "Durata Sveglio", + "sleep_duration": "Durata Addormentato", + "awake_states": "Stati di Veglia*", + "sleep_states": "Stati di Sonno*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Impostazioni di Integrazione SAAS", + "description": "Configura le impostazioni di base per l'integrazione SAAS.", + "data": { + "name": "Nome per Sensore", + "topic_template": "Topic MQTT (da Sleep As Android)", + "qos": "Quality of Service (QoS) MQTT", + "awake_duration": "Durata Sveglio", + "sleep_duration": "Durata Addormentato", + "awake_states": "Stati di Veglia*", + "sleep_states": "Stati di Sonno*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Sconosciuto", + "Sleep Tracking Started": "Inizio Monitoraggio del Sonno", + "Sleep Tracking Stopped": "Fine Monitoraggio del Sonno", + "Sleep Tracking Paused": "Monitoraggio del Sonno in Pausa", + "Sleep Tracking Resumed": "Ripresa Monitoraggio del Sonno", + "Alarm Snoozed": "Allarme Posticipato", + "Snooze Canceled": "Posticipo Annullato", + "Time for Bed": "Ora di Andare a Letto", + "Alarm Alert Started": "Inizio Allarme", + "Alarm Dismissed": "Allarme Disattivato", + "Skip Next Alarm": "Salta il Prossimo Allarme", + "Show Skip Next Alarm": "Mostra 'Salta il Prossimo Allarme'", + "REM": "REM", + "Smart Period": "Periodo Smart", + "Before Smart Period": "Prima del Periodo Smart", + "Lullaby Start": "Inizio Ninna Nanna", + "Lullaby Stop": "Fine Ninna Nanna", + "Lullaby Volume Down": "Diminuzione Volume Ninna Nanna", + "Deep Sleep": "Sonno Profondo", + "Light Sleep": "Sonno Leggero", + "Awake": "Sveglio", + "Not Awake": "Non Sveglio", + "Apnea Alarm": "Allarme Apnea", + "Antisnoring": "Antirussamento", + "Before Alarm": "Prima dell'Allarme", + "Snore Detected": "Russamento Rilevato", + "Talk Detected": "Conversazione Rilevata", + "Cough Detected": "Tosse Rilevata", + "Baby Cry Detected": "Pianto del Bambino Rilevato", + "Laugh Detected": "Risata Rilevata", + "Alarm Rescheduled": "Allarme Riprogrammato", + "None": "Nessuno" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/ja.json b/custom_components/saas/translations/ja.json new file mode 100644 index 0000000..52dcaa6 --- /dev/null +++ b/custom_components/saas/translations/ja.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - 睡眠如 Android 狀態", + "config": { + "step": { + "user": { + "title": "SAAS - 睡眠如 Android 狀態配置", + "description": "配置 SAAS 整合的基本設置。", + "data": { + "name": "感應器名稱", + "topic_template": "睡眠如 Android 事件的 MQTT 主題", + "qos": "MQTT 的服務質量 (QoS)", + "awake_duration": "清醒持續時間", + "sleep_duration": "睡眠持續時間", + "awake_states": "清醒狀態*", + "sleep_states": "睡眠狀態*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS 整合設置", + "description": "配置 SAAS 整合的基本設置。", + "data": { + "name": "感應器名稱", + "topic_template": "MQTT 主題(來自睡眠如 Android)", + "qos": "MQTT 服務質量 (QoS)", + "awake_duration": "清醒持續時間", + "sleep_duration": "睡眠持續時間", + "awake_states": "清醒狀態*", + "sleep_states": "睡眠狀態*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "未知", + "Sleep Tracking Started": "開始追蹤睡眠", + "Sleep Tracking Stopped": "停止追蹤睡眠", + "Sleep Tracking Paused": "暫停追蹤睡眠", + "Sleep Tracking Resumed": "恢復追蹤睡眠", + "Alarm Snoozed": "鬧鐘貪睡", + "Snooze Canceled": "取消貪睡", + "Time for Bed": "該睡覺了", + "Alarm Alert Started": "鬧鐘警報開始", + "Alarm Dismissed": "鬧鐘解除", + "Skip Next Alarm": "跳過下一個鬧鐘", + "Show Skip Next Alarm": "顯示跳過下一個鬧鐘", + "REM": "REM", + "Smart Period": "智能期間", + "Before Smart Period": "在智能期間之前", + "Lullaby Start": "搖籃曲開始", + "Lullaby Stop": "搖籃曲停止", + "Lullaby Volume Down": "搖籃曲音量下降", + "Deep Sleep": "深度睡眠", + "Light Sleep": "淺度睡眠", + "Awake": "清醒", + "Not Awake": "未清醒", + "Apnea Alarm": "呼吸暫停警報", + "Antisnoring": "防打鼾", + "Before Alarm": "在鬧鐘之前", + "Snore Detected": "檢測到打鼾", + "Talk Detected": "檢測到說話", + "Cough Detected": "檢測到咳嗽", + "Baby Cry Detected": "檢測到嬰兒哭聲", + "Laugh Detected": "檢測到笑聲", + "Alarm Rescheduled": "鬧鐘重新安排", + "None": "無" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/ko.json b/custom_components/saas/translations/ko.json new file mode 100644 index 0000000..9d71829 --- /dev/null +++ b/custom_components/saas/translations/ko.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Sleep As Android 상태", + "config": { + "step": { + "user": { + "title": "SAAS - Sleep As Android 상태 설정", + "description": "SAAS 통합의 기본 설정을 구성합니다.", + "data": { + "name": "센서 이름", + "topic_template": "Sleep As Android 이벤트의 MQTT 주제", + "qos": "MQTT의 서비스 품질(QoS)", + "awake_duration": "깨어 있는 시간", + "sleep_duration": "수면 시간", + "awake_states": "깨어 있는 상태*", + "sleep_states": "수면 상태*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS 통합 설정", + "description": "SAAS 통합의 기본 설정을 구성합니다.", + "data": { + "name": "센서 이름", + "topic_template": "Sleep As Android에서의 MQTT 주제", + "qos": "MQTT의 서비스 품질(QoS)", + "awake_duration": "깨어 있는 시간", + "sleep_duration": "수면 시간", + "awake_states": "깨어 있는 상태*", + "sleep_states": "수면 상태*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "알 수 없음", + "Sleep Tracking Started": "수면 추적 시작", + "Sleep Tracking Stopped": "수면 추적 중지", + "Sleep Tracking Paused": "수면 추적 일시 중지", + "Sleep Tracking Resumed": "수면 추적 재개", + "Alarm Snoozed": "알람 스누즈", + "Snooze Canceled": "스누즈 취소", + "Time for Bed": "침대 시간", + "Alarm Alert Started": "알람 경고 시작", + "Alarm Dismissed": "알람 해제", + "Skip Next Alarm": "다음 알람 건너뛰기", + "Show Skip Next Alarm": "다음 알람 건너뛰기 표시", + "REM": "REM", + "Smart Period": "스마트 기간", + "Before Smart Period": "스마트 기간 이전", + "Lullaby Start": "자장가 시작", + "Lullaby Stop": "자장가 중지", + "Lullaby Volume Down": "자장가 볼륨 다운", + "Deep Sleep": "깊은 잠", + "Light Sleep": "얕은 잠", + "Awake": "깨어 있음", + "Not Awake": "깨어 있지 않음", + "Apnea Alarm": "무호흡 알람", + "Antisnoring": "코골이 방지", + "Before Alarm": "알람 이전", + "Snore Detected": "코골이 감지됨", + "Talk Detected": "대화 감지됨", + "Cough Detected": "기침 감지됨", + "Baby Cry Detected": "아기 울음소리 감지됨", + "Laugh Detected": "웃음소리 감지됨", + "Alarm Rescheduled": "알람 재스케줄", + "None": "없음" + } + } + } + } \ No newline at end of file diff --git a/custom_components/saas/translations/nl.json b/custom_components/saas/translations/nl.json new file mode 100644 index 0000000..2164d9d --- /dev/null +++ b/custom_components/saas/translations/nl.json @@ -0,0 +1,75 @@ +{ + "title": "Configuratie voor SAAS - Sleep As Android Status", + "config": { + "step": { + "user": { + "title": "Configuratie voor SAAS - Sleep As Android Status", + "description": "Configureer de basisinstellingen voor de SAAS-integratie.", + "data": { + "name": "Naam voor Sensor", + "topic_template": "MQTT-onderwerp voor Sleep As Android Gebeurtenissen", + "qos": "Quality of Service (QoS) voor MQTT", + "awake_duration": "Wakker Duur", + "sleep_duration": "Slaapduur", + "awake_states": "Wakker Staten*", + "sleep_states": "Slaap Staten*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS-Integratie-Instellingen", + "description": "Configureer de basisinstellingen voor de SAAS-integratie.", + "data": { + "name": "Naam voor Sensor", + "topic_template": "MQTT-Onderwerp (van Sleep As Android)", + "qos": "MQTT Quality of Service (QoS)", + "awake_duration": "Wakker Duur", + "sleep_duration": "Slaapduur", + "awake_states": "Wakker Staten*", + "sleep_states": "Slaap Staten*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Onbekend", + "Sleep Tracking Started": "Slaaptracking Gestart", + "Sleep Tracking Stopped": "Slaaptracking Gestopt", + "Sleep Tracking Paused": "Slaaptracking Gepauzeerd", + "Sleep Tracking Resumed": "Slaaptracking Hervat", + "Alarm Snoozed": "Alarm Gesnoozed", + "Snooze Canceled": "Snooze Geannuleerd", + "Time for Bed": "Tijd om te Gaan Slapen", + "Alarm Alert Started": "Alarm Alert Gestart", + "Alarm Dismissed": "Alarm Afgeslagen", + "Skip Next Alarm": "Volgende Alarm Overslaan", + "Show Skip Next Alarm": "Toon 'Volgende Alarm Overslaan'", + "REM": "REM", + "Smart Period": "Slimme Periode", + "Before Smart Period": "Voor Slimme Periode", + "Lullaby Start": "Slaapliedje Start", + "Lullaby Stop": "Slaapliedje Stop", + "Lullaby Volume Down": "Slaapliedje Volume Omlaag", + "Deep Sleep": "Diepe Slaap", + "Light Sleep": "Lichte Slaap", + "Awake": "Wakker", + "Not Awake": "Niet Wakker", + "Apnea Alarm": "Apneu Alarm", + "Antisnoring": "Antisnurken", + "Before Alarm": "Voor Alarm", + "Snore Detected": "Snurken Gedetecteerd", + "Talk Detected": "Gesprek Gedetecteerd", + "Cough Detected": "Hoesten Gedetecteerd", + "Baby Cry Detected": "Baby Huilen Gedetecteerd", + "Laugh Detected": "Lachen Gedetecteerd", + "Alarm Rescheduled": "Alarm Herpland", + "None": "Geen" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/pl.json b/custom_components/saas/translations/pl.json new file mode 100644 index 0000000..7cf84d9 --- /dev/null +++ b/custom_components/saas/translations/pl.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Status Sleep As Android", + "config": { + "step": { + "user": { + "title": "Konfiguracja dla SAAS - Status Sleep As Android", + "description": "Konfiguracja podstawowych ustawień dla integracji SAAS.", + "data": { + "name": "Nazwa dla czujnika", + "topic_template": "Temat MQTT dla zdarzeń Sleep As Android", + "qos": "Jakość usługi (QoS) dla MQTT", + "awake_duration": "Czas czuwania", + "sleep_duration": "Czas snu", + "awake_states": "Stany czuwania*", + "sleep_states": "Stany snu*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Ustawienia integracji SAAS", + "description": "Konfiguracja podstawowych ustawień dla integracji SAAS.", + "data": { + "name": "Nazwa dla czujnika", + "topic_template": "Temat MQTT (od Sleep As Android)", + "qos": "Jakość usługi (QoS) MQTT", + "awake_duration": "Czas czuwania", + "sleep_duration": "Czas snu", + "awake_states": "Stany czuwania*", + "sleep_states": "Stany snu*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Nieznane", + "Sleep Tracking Started": "Rozpoczęto śledzenie snu", + "Sleep Tracking Stopped": "Zatrzymano śledzenie snu", + "Sleep Tracking Paused": "Wstrzymano śledzenie snu", + "Sleep Tracking Resumed": "Wznowiono śledzenie snu", + "Alarm Snoozed": "Alarm w drzemce", + "Snooze Canceled": "Drzemka anulowana", + "Time for Bed": "Czas na sen", + "Alarm Alert Started": "Rozpoczęto alarm", + "Alarm Dismissed": "Alarm wyłączony", + "Skip Next Alarm": "Pomiń następny alarm", + "Show Skip Next Alarm": "Pokaż pomiń następny alarm", + "REM": "REM", + "Smart Period": "Inteligentny okres", + "Before Smart Period": "Przed inteligentnym okresem", + "Lullaby Start": "Rozpoczęcie kołysanki", + "Lullaby Stop": "Zakończenie kołysanki", + "Lullaby Volume Down": "Zmniejsz głośność kołysanki", + "Deep Sleep": "Głęboki sen", + "Light Sleep": "Lekki sen", + "Awake": "Czuwanie", + "Not Awake": "Nie czuwa", + "Apnea Alarm": "Alarm apnei", + "Antisnoring": "Przeciw chrapaniu", + "Before Alarm": "Przed alarmem", + "Snore Detected": "Wykryto chrapanie", + "Talk Detected": "Wykryto mówienie", + "Cough Detected": "Wykryto kaszel", + "Baby Cry Detected": "Wykryto płacz dziecka", + "Laugh Detected": "Wykryto śmiech", + "Alarm Rescheduled": "Alarm został przesunięty", + "None": "Brak" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/pt.json b/custom_components/saas/translations/pt.json new file mode 100644 index 0000000..592e132 --- /dev/null +++ b/custom_components/saas/translations/pt.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Status do Sleep As Android", + "config": { + "step": { + "user": { + "title": "Configuração para SAAS - Status do Sleep As Android", + "description": "Configure as configurações básicas para a integração SAAS.", + "data": { + "name": "Nome para Sensor", + "topic_template": "Tópico MQTT para Eventos do Sleep As Android", + "qos": "Qualidade de Serviço (QoS) para MQTT", + "awake_duration": "Duração Acordado", + "sleep_duration": "Duração Adormecido", + "awake_states": "Estados Acordado*", + "sleep_states": "Estados Adormecido*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Configurações de Integração SAAS", + "description": "Configure as configurações básicas para a integração SAAS.", + "data": { + "name": "Nome para Sensor", + "topic_template": "Tópico MQTT (do Sleep As Android)", + "qos": "Qualidade de Serviço (QoS) MQTT", + "awake_duration": "Duração Acordado", + "sleep_duration": "Duração Adormecido", + "awake_states": "Estados Acordado*", + "sleep_states": "Estados Adormecido*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Desconhecido", + "Sleep Tracking Started": "Monitoramento do Sono Iniciado", + "Sleep Tracking Stopped": "Monitoramento do Sono Parado", + "Sleep Tracking Paused": "Monitoramento do Sono Pausado", + "Sleep Tracking Resumed": "Monitoramento do Sono Retomado", + "Alarm Snoozed": "Alarme Adiado", + "Snooze Canceled": "Adiamento Cancelado", + "Time for Bed": "Hora de Dormir", + "Alarm Alert Started": "Alerta de Alarme Iniciado", + "Alarm Dismissed": "Alarme Desligado", + "Skip Next Alarm": "Pular Próximo Alarme", + "Show Skip Next Alarm": "Mostrar 'Pular Próximo Alarme'", + "REM": "REM", + "Smart Period": "Período Inteligente", + "Before Smart Period": "Antes do Período Inteligente", + "Lullaby Start": "Início de Canção de Ninar", + "Lullaby Stop": "Fim de Canção de Ninar", + "Lullaby Volume Down": "Diminuir Volume da Canção de Ninar", + "Deep Sleep": "Sono Profundo", + "Light Sleep": "Sono Leve", + "Awake": "Acordado", + "Not Awake": "Não Acordado", + "Apnea Alarm": "Alarme de Apneia", + "Antisnoring": "Antirronco", + "Before Alarm": "Antes do Alarme", + "Snore Detected": "Ronco Detectado", + "Talk Detected": "Conversa Detectada", + "Cough Detected": "Tosse Detectada", + "Baby Cry Detected": "Choro de Bebê Detectado", + "Laugh Detected": "Risada Detectada", + "Alarm Rescheduled": "Alarme Remarcado", + "None": "Nenhum" + } + } + } + } \ No newline at end of file diff --git a/custom_components/saas/translations/pue.json b/custom_components/saas/translations/pue.json new file mode 100644 index 0000000..b4fac33 --- /dev/null +++ b/custom_components/saas/translations/pue.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - 睡眠如 Android 狀態", + "config": { + "step": { + "user": { + "title": "SAAS - 睡眠如 Android 狀態配置", + "description": "配置 SAAS 整合的基本設置。", + "data": { + "name": "感應器名稱", + "topic_template": "睡眠如 Android 事件的 MQTT 主題", + "qos": "MQTT 的服務質量 (QoS)", + "awake_duration": "清醒持續時間", + "sleep_duration": "睡眠持續時間", + "awake_states": "清醒狀態*", + "sleep_states": "睡眠狀態*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS 整合設置", + "description": "配置 SAAS 整合的基本設置。", + "data": { + "name": "感應器名稱", + "topic_template": "MQTT 主題(來自睡眠如 Android)", + "qos": "MQTT 服務質量 (QoS)", + "awake_duration": "清醒持續時間", + "sleep_duration": "睡眠持續時間", + "awake_states": "清醒狀態*", + "sleep_states": "睡眠狀態*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "未知", + "Sleep Tracking Started": "開始追蹤睡眠", + "Sleep Tracking Stopped": "停止追蹤睡眠", + "Sleep Tracking Paused": "暫停追蹤睡眠", + "Sleep Tracking Resumed": "恢復追蹤睡眠", + "Alarm Snoozed": "鬧鐘貪睡", + "Snooze Canceled": "取消貪睡", + "Time for Bed": "該睡覺了", + "Alarm Alert Started": "鬧鐘警報開始", + "Alarm Dismissed": "鬧鐘解除", + "Skip Next Alarm": "跳過下一個鬧鐘", + "Show Skip Next Alarm": "顯示跳過下一個鬧鐘", + "REM": "REM", + "Smart Period": "智能期間", + "Before Smart Period": "在智能期間之前", + "Lullaby Start": "搖籃曲開始", + "Lullaby Stop": "搖籃曲停止", + "Lullaby Volume Down": "搖籃曲音量下降", + "Deep Sleep": "深度睡眠", + "Light Sleep": "淺度睡眠", + "Awake": "清醒", + "Not Awake": "未清醒", + "Apnea Alarm": "呼吸暫停警報", + "Antisnoring": "防打鼾", + "Before Alarm": "在鬧鐘之前", + "Snore Detected": "檢測到打鼾", + "Talk Detected": "檢測到說話", + "Cough Detected": "檢測到咳嗽", + "Baby Cry Detected": "檢測到嬰兒哭聲", + "Laugh Detected": "檢測到笑聲", + "Alarm Rescheduled": "鬧鐘重新安排", + "None": "無" + } + } + } + } \ No newline at end of file diff --git a/custom_components/saas/translations/ru.json b/custom_components/saas/translations/ru.json new file mode 100644 index 0000000..fbbed4a --- /dev/null +++ b/custom_components/saas/translations/ru.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Статус Sleep As Android", + "config": { + "step": { + "user": { + "title": "Конфигурация для SAAS - Статус Sleep As Android", + "description": "Настройка основных параметров для интеграции SAAS.", + "data": { + "name": "Имя для датчика", + "topic_template": "Тема MQTT для событий Sleep As Android", + "qos": "Качество обслуживания (QoS) для MQTT", + "awake_duration": "Продолжительность бодрствования", + "sleep_duration": "Продолжительность сна", + "awake_states": "Состояния бодрствования*", + "sleep_states": "Состояния сна*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Настройки интеграции SAAS", + "description": "Настройка основных параметров для интеграции SAAS.", + "data": { + "name": "Имя для датчика", + "topic_template": "Тема MQTT (от Sleep As Android)", + "qos": "Качество обслуживания (QoS) MQTT", + "awake_duration": "Продолжительность бодрствования", + "sleep_duration": "Продолжительность сна", + "awake_states": "Состояния бодрствования*", + "sleep_states": "Состояния сна*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Неизвестно", + "Sleep Tracking Started": "Начато отслеживание сна", + "Sleep Tracking Stopped": "Отслеживание сна остановлено", + "Sleep Tracking Paused": "Отслеживание сна приостановлено", + "Sleep Tracking Resumed": "Отслеживание сна возобновлено", + "Alarm Snoozed": "Будильник в режиме паузы", + "Snooze Canceled": "Пауза отменена", + "Time for Bed": "Время спать", + "Alarm Alert Started": "Начало сигнала будильника", + "Alarm Dismissed": "Будильник отключен", + "Skip Next Alarm": "Пропустить следующий будильник", + "Show Skip Next Alarm": "Показать пропуск следующего будильника", + "REM": "REM", + "Smart Period": "Умный период", + "Before Smart Period": "До умного периода", + "Lullaby Start": "Начало колыбельной", + "Lullaby Stop": "Конец колыбельной", + "Lullaby Volume Down": "Уменьшить громкость колыбельной", + "Deep Sleep": "Глубокий сон", + "Light Sleep": "Легкий сон", + "Awake": "Бодрствование", + "Not Awake": "Не бодрствует", + "Apnea Alarm": "Сигнал апноэ", + "Antisnoring": "Против храпа", + "Before Alarm": "До будильника", + "Snore Detected": "Обнаружен храп", + "Talk Detected": "Обнаружена речь", + "Cough Detected": "Обнаружен кашель", + "Baby Cry Detected": "Обнаружен плач ребенка", + "Laugh Detected": "Обнаружен смех", + "Alarm Rescheduled": "Будильник перенесен", + "None": "Нет" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/sv.json b/custom_components/saas/translations/sv.json new file mode 100644 index 0000000..5c6ed52 --- /dev/null +++ b/custom_components/saas/translations/sv.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Sleep As Android Status", + "config": { + "step": { + "user": { + "title": "Konfiguration för SAAS - Sleep As Android Status", + "description": "Konfigurera de grundläggande inställningarna för SAAS-integrationen.", + "data": { + "name": "Namn för Sensor", + "topic_template": "MQTT Ämne för Sleep As Android Händelser", + "qos": "Kvalitet på Tjänst (QoS) för MQTT", + "awake_duration": "Vaken Varaktighet", + "sleep_duration": "Sovande Varaktighet", + "awake_states": "Vakna Tillstånd*", + "sleep_states": "Sovande Tillstånd*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS Integrationsinställningar", + "description": "Konfigurera de grundläggande inställningarna för SAAS-integrationen.", + "data": { + "name": "Namn för Sensor", + "topic_template": "MQTT Ämne (från Sleep As Android)", + "qos": "MQTT Kvalitet på Tjänst (QoS)", + "awake_duration": "Vaken Varaktighet", + "sleep_duration": "Sovande Varaktighet", + "awake_states": "Vakna Tillstånd*", + "sleep_states": "Sovande Tillstånd*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "Okänd", + "Sleep Tracking Started": "Sömnmätning Startad", + "Sleep Tracking Stopped": "Sömnmätning Stoppad", + "Sleep Tracking Paused": "Sömnmätning Pausad", + "Sleep Tracking Resumed": "Sömnmätning Återupptagen", + "Alarm Snoozed": "Alarm Snoozed", + "Snooze Canceled": "Snooze Avbruten", + "Time for Bed": "Dags att Gå och Lägga Sig", + "Alarm Alert Started": "Alarmvarning Startad", + "Alarm Dismissed": "Alarm Avvisat", + "Skip Next Alarm": "Hoppa över Nästa Alarm", + "Show Skip Next Alarm": "Visa 'Hoppa över Nästa Alarm'", + "REM": "REM", + "Smart Period": "Smart Period", + "Before Smart Period": "Före Smart Period", + "Lullaby Start": "Vaggvisa Start", + "Lullaby Stop": "Vaggvisa Stopp", + "Lullaby Volume Down": "Vaggvisa Volym Ner", + "Deep Sleep": "Djup Sömn", + "Light Sleep": "Lätt Sömn", + "Awake": "Vaken", + "Not Awake": "Inte Vaken", + "Apnea Alarm": "Apné Alarm", + "Antisnoring": "Antisnarkning", + "Before Alarm": "Före Alarm", + "Snore Detected": "Snarkning Upptäckt", + "Talk Detected": "Prat Upptäckt", + "Cough Detected": "Hosta Upptäckt", + "Baby Cry Detected": "Bebisgråt Upptäckt", + "Laugh Detected": "Skratt Upptäckt", + "Alarm Rescheduled": "Alarm Omplanerat", + "None": "Ingen" + } + } + } +} \ No newline at end of file diff --git a/custom_components/saas/translations/zh.json b/custom_components/saas/translations/zh.json new file mode 100644 index 0000000..3ca0f23 --- /dev/null +++ b/custom_components/saas/translations/zh.json @@ -0,0 +1,75 @@ +{ + "title": "SAAS - Sleep As Android 状态", + "config": { + "step": { + "user": { + "title": "SAAS - Sleep As Android 状态配置", + "description": "配置 SAAS 集成的基本设置。", + "data": { + "name": "传感器名称", + "topic_template": "Sleep As Android 事件的 MQTT 主题", + "qos": "MQTT 的服务质量 (QoS)", + "awake_duration": "清醒持续时间", + "sleep_duration": "睡眠持续时间", + "awake_states": "清醒状态*", + "sleep_states": "睡眠状态*" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "SAAS 集成设置", + "description": "配置 SAAS 集成的基本设置。", + "data": { + "name": "传感器名称", + "topic_template": "Sleep As Android 的 MQTT 主题", + "qos": "MQTT 的服务质量 (QoS)", + "awake_duration": "清醒持续时间", + "sleep_duration": "睡眠持续时间", + "awake_states": "清醒状态*", + "sleep_states": "睡眠状态*" + } + } + } + }, + "entity": { + "sensor": { + "state": { + "Unknown": "未知", + "Sleep Tracking Started": "开始睡眠跟踪", + "Sleep Tracking Stopped": "停止睡眠跟踪", + "Sleep Tracking Paused": "暂停睡眠跟踪", + "Sleep Tracking Resumed": "恢复睡眠跟踪", + "Alarm Snoozed": "闹钟贪睡", + "Snooze Canceled": "取消贪睡", + "Time for Bed": "该睡觉了", + "Alarm Alert Started": "闹钟警报开始", + "Alarm Dismissed": "闹钟解除", + "Skip Next Alarm": "跳过下一个闹钟", + "Show Skip Next Alarm": "显示跳过下一个闹钟", + "REM": "REM", + "Smart Period": "智能周期", + "Before Smart Period": "在智能周期之前", + "Lullaby Start": "开始摇篮曲", + "Lullaby Stop": "停止摇篮曲", + "Lullaby Volume Down": "降低摇篮曲音量", + "Deep Sleep": "深度睡眠", + "Light Sleep": "浅睡", + "Awake": "清醒", + "Not Awake": "未清醒", + "Apnea Alarm": "呼吸暂停警报", + "Antisnoring": "防打鼾", + "Before Alarm": "在闹钟之前", + "Snore Detected": "检测到打鼾", + "Talk Detected": "检测到说话", + "Cough Detected": "检测到咳嗽", + "Baby Cry Detected": "检测到婴儿哭声", + "Laugh Detected": "检测到笑声", + "Alarm Rescheduled": "闹钟重新安排", + "None": "无" + } + } + } + } \ No newline at end of file