diff --git a/custom_components/o365/__init__.py b/custom_components/o365/__init__.py index 98d3d9e..7536481 100644 --- a/custom_components/o365/__init__.py +++ b/custom_components/o365/__init__.py @@ -158,7 +158,7 @@ async def _async_setup_account(hass, account_conf, conf_type): if is_authenticated and permissions and permissions != TOKEN_FILE_MISSING: check_token = await _async_check_token(hass, account, account_name) if check_token: - do_setup(hass, account_conf, account, account_name, conf_type, perms) + await do_setup(hass, account_conf, account, account_name, conf_type, perms) else: await _async_authorization_repair( hass, diff --git a/custom_components/o365/calendar.py b/custom_components/o365/calendar.py index 6b88f02..61b10f5 100644 --- a/custom_components/o365/calendar.py +++ b/custom_components/o365/calendar.py @@ -242,7 +242,6 @@ def _init_data(self, account, calendar_id, entity): max_results = entity.get(CONF_MAX_RESULTS) search = entity.get(CONF_SEARCH) exclude = entity.get(CONF_EXCLUDE) - # _LOGGER.debug("Initialising calendar: %s", calendar_id) return O365CalendarData( account, self.entity_id, diff --git a/custom_components/o365/classes/mailsensor.py b/custom_components/o365/classes/mailsensor.py index 1f33877..0edbca4 100644 --- a/custom_components/o365/classes/mailsensor.py +++ b/custom_components/o365/classes/mailsensor.py @@ -1,13 +1,14 @@ """O365 mail sensors.""" import datetime +from operator import itemgetter from homeassistant.components.sensor import SensorEntity from O365 import mailbox # pylint: disable=no-name-in-module from ..const import ( - ATTR_ATTRIBUTES, ATTR_AUTOREPLIESSETTINGS, + ATTR_DATA, ATTR_END, ATTR_EXTERNAL_AUDIENCE, ATTR_EXTERNALREPLY, @@ -21,133 +22,57 @@ CONF_IMPORTANCE, CONF_IS_UNREAD, CONF_MAIL_FROM, - CONF_MAX_ITEMS, CONF_SUBJECT_CONTAINS, CONF_SUBJECT_IS, DATETIME_FORMAT, PERM_MAILBOX_SETTINGS, PERM_MINIMUM_MAILBOX_SETTINGS, SENSOR_AUTO_REPLY, - SENSOR_MAIL, + SENSOR_EMAIL, ) -from ..utils.utils import clean_html +from ..utils.utils import clean_html, get_email_attributes from .sensorentity import O365Sensor class O365MailSensor(O365Sensor): """O365 generic Mail Sensor class.""" - def __init__( - self, coordinator, config, sensor_conf, mail_folder, name, entity_id, unique_id - ): + def __init__(self, coordinator, config, sensor_conf, name, entity_id, unique_id): """Initialise the O365 Sensor.""" - super().__init__(coordinator, config, name, entity_id, SENSOR_MAIL, unique_id) - self.mail_folder = mail_folder + super().__init__(coordinator, config, name, entity_id, SENSOR_EMAIL, unique_id) self.download_attachments = sensor_conf.get(CONF_DOWNLOAD_ATTACHMENTS) self.html_body = sensor_conf.get(CONF_HTML_BODY) - self.max_items = sensor_conf.get(CONF_MAX_ITEMS, 5) - self.query = self.mail_folder.new_query() - self.query = self.query.select( - "sender", - "from", - "subject", - "body", - "receivedDateTime", - "toRecipients", - "ccRecipients", - "has_attachments", - "importance", - "is_read", - ) - if self.download_attachments: - self.query = self.query.select( - "attachments", - ) - self._config = config + self._state = None + self._extra_attributes = None @property def icon(self): """Entity icon.""" return "mdi:microsoft-outlook" + @property + def state(self): + """Sensor state.""" + return self._state + @property def extra_state_attributes(self): """Device state attributes.""" - return self.coordinator.data[self.entity_key][ATTR_ATTRIBUTES] - - -class O365QuerySensor(O365MailSensor, SensorEntity): - """O365 Query sensor processing.""" - - def __init__( - self, coordinator, config, sensor_conf, mail_folder, name, entity_id, unique_id - ): - """Initialise the O365 Query.""" - super().__init__( - coordinator, config, sensor_conf, mail_folder, name, entity_id, unique_id - ) - - self.query.order_by("receivedDateTime", ascending=False) - - self._build_query(sensor_conf) - - def _build_query(self, sensor_conf): - body_contains = sensor_conf.get(CONF_BODY_CONTAINS) - subject_contains = sensor_conf.get(CONF_SUBJECT_CONTAINS) - subject_is = sensor_conf.get(CONF_SUBJECT_IS) - has_attachment = sensor_conf.get(CONF_HAS_ATTACHMENT) - importance = sensor_conf.get(CONF_IMPORTANCE) - email_from = sensor_conf.get(CONF_MAIL_FROM) - is_unread = sensor_conf.get(CONF_IS_UNREAD) - if ( - body_contains is not None - or subject_contains is not None - or subject_is is not None - or has_attachment is not None - or importance is not None - or email_from is not None - or is_unread is not None - ): - self._add_to_query("ge", "receivedDateTime", datetime.datetime(1900, 5, 1)) - self._add_to_query("contains", "body", body_contains) - self._add_to_query("contains", "subject", subject_contains) - self._add_to_query("equals", "subject", subject_is) - self._add_to_query("equals", "hasAttachments", has_attachment) - self._add_to_query("equals", "from", email_from) - self._add_to_query("equals", "IsRead", not is_unread, is_unread) - self._add_to_query("equals", "importance", importance) - - def _add_to_query(self, qtype, attribute_name, attribute_value, check_value=True): - if attribute_value is None or check_value is None: - return - - if qtype == "ge": - self.query.chain("and").on_attribute(attribute_name).greater_equal( - attribute_value - ) - if qtype == "contains": - self.query.chain("and").on_attribute(attribute_name).contains( - attribute_value - ) - if qtype == "equals": - self.query.chain("and").on_attribute(attribute_name).equals(attribute_value) - - -class O365EmailSensor(O365MailSensor, SensorEntity): - """O365 Email sensor processing.""" - - def __init__( - self, coordinator, config, sensor_conf, mail_folder, name, entity_id, unique_id - ): - """Initialise the O365 Email sensor.""" - super().__init__( - coordinator, config, sensor_conf, mail_folder, name, entity_id, unique_id - ) + return self._extra_attributes - is_unread = sensor_conf.get(CONF_IS_UNREAD) + def _handle_coordinator_update(self) -> None: + data = self.coordinator.data[self.entity_key][ATTR_DATA] + attrs = self._get_attributes(data) + attrs.sort(key=itemgetter("received"), reverse=True) + self._state = len(attrs) + self._extra_attributes = {ATTR_DATA: attrs} + self.async_write_ha_state() - if is_unread is not None: - self.query.chain("and").on_attribute("IsRead").equals(not is_unread) + def _get_attributes(self, data): + return [ + get_email_attributes(x, self.download_attachments, self.html_body) + for x in data + ] class O365AutoReplySensor(O365Sensor, SensorEntity): @@ -208,3 +133,87 @@ def _validate_autoreply_permissions(self): "Not authorisied to update auto reply - requires permission: " + f"{PERM_MAILBOX_SETTINGS}", ) + + +def _build_base_query(mail_folder, sensor_conf): + """Build base query for mail.""" + download_attachments = sensor_conf.get(CONF_DOWNLOAD_ATTACHMENTS) + query = mail_folder.new_query() + query = query.select( + "sender", + "from", + "subject", + "body", + "receivedDateTime", + "toRecipients", + "ccRecipients", + "has_attachments", + "importance", + "is_read", + ) + if download_attachments: + query = query.select( + "attachments", + ) + return query + + +def build_inbox_query(mail_folder, sensor_conf): + """Build query for email sensor.""" + query = _build_base_query(mail_folder, sensor_conf) + + is_unread = sensor_conf.get(CONF_IS_UNREAD) + + if is_unread is not None: + query.chain("and").on_attribute("IsRead").equals(not is_unread) + + return query + + +def build_query_query(mail_folder, sensor_conf): + """Build query for query sensor.""" + query = _build_base_query(mail_folder, sensor_conf) + query.order_by("receivedDateTime", ascending=False) + + body_contains = sensor_conf.get(CONF_BODY_CONTAINS) + subject_contains = sensor_conf.get(CONF_SUBJECT_CONTAINS) + subject_is = sensor_conf.get(CONF_SUBJECT_IS) + has_attachment = sensor_conf.get(CONF_HAS_ATTACHMENT) + importance = sensor_conf.get(CONF_IMPORTANCE) + email_from = sensor_conf.get(CONF_MAIL_FROM) + is_unread = sensor_conf.get(CONF_IS_UNREAD) + if ( + body_contains is not None + or subject_contains is not None + or subject_is is not None + or has_attachment is not None + or importance is not None + or email_from is not None + or is_unread is not None + ): + query = _add_to_query( + query, "ge", "receivedDateTime", datetime.datetime(1900, 5, 1) + ) + query = _add_to_query(query, "contains", "body", body_contains) + query = _add_to_query(query, "contains", "subject", subject_contains) + query = _add_to_query(query, "equals", "subject", subject_is) + query = _add_to_query(query, "equals", "hasAttachments", has_attachment) + query = _add_to_query(query, "equals", "from", email_from) + query = _add_to_query(query, "equals", "IsRead", not is_unread, is_unread) + query = _add_to_query(query, "equals", "importance", importance) + + return query + + +def _add_to_query(query, qtype, attribute_name, attribute_value, check_value=True): + if attribute_value is None or check_value is None: + return + + if qtype == "ge": + query.chain("and").on_attribute(attribute_name).greater_equal(attribute_value) + if qtype == "contains": + query.chain("and").on_attribute(attribute_name).contains(attribute_value) + if qtype == "equals": + query.chain("and").on_attribute(attribute_name).equals(attribute_value) + + return query diff --git a/custom_components/o365/classes/sensorentity.py b/custom_components/o365/classes/sensorentity.py index f219649..84602af 100644 --- a/custom_components/o365/classes/sensorentity.py +++ b/custom_components/o365/classes/sensorentity.py @@ -27,7 +27,7 @@ def name(self): @property def entity_key(self): - """Entity Keyr property.""" + """Entity Key property.""" return self._entity_id @property diff --git a/custom_components/o365/classes/taskssensor.py b/custom_components/o365/classes/taskssensor.py index f93bab6..3f165f8 100644 --- a/custom_components/o365/classes/taskssensor.py +++ b/custom_components/o365/classes/taskssensor.py @@ -1,14 +1,17 @@ """O365 tasks sensors.""" import logging -from datetime import timedelta +from datetime import datetime, timedelta import voluptuous as vol from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_ENABLED from homeassistant.util import dt from ..const import ( ATTR_ALL_TASKS, ATTR_COMPLETED, + ATTR_CREATED, + ATTR_DATA, ATTR_DESCRIPTION, ATTR_DUE, ATTR_OVERDUE_TASKS, @@ -16,9 +19,13 @@ ATTR_SUBJECT, ATTR_TASK_ID, ATTR_TASKS, + CONF_ACCOUNT, CONF_DUE_HOURS_BACKWARD_TO_GET, CONF_DUE_HOURS_FORWARD_TO_GET, CONF_SHOW_COMPLETED, + CONF_TASK_LIST, + CONF_TODO_SENSORS, + CONF_TRACK_NEW, DATETIME_FORMAT, DOMAIN, EVENT_COMPLETED_TASK, @@ -31,6 +38,7 @@ PERM_TASKS_READWRITE, SENSOR_TODO, ) +from ..utils.filemgmt import update_task_list_file from .sensorentity import O365Sensor _LOGGER = logging.getLogger(__name__) @@ -44,26 +52,65 @@ def __init__(self, coordinator, todo, name, task, config, entity_id, unique_id): super().__init__(coordinator, config, name, entity_id, SENSOR_TODO, unique_id) self.todo = todo self._show_completed = task.get(CONF_SHOW_COMPLETED) - self.query = self.todo.new_query() - if not self._show_completed: - self.query = self.query.on_attribute("status").unequal("completed") - self.start_offset = task.get(CONF_DUE_HOURS_BACKWARD_TO_GET) - self.end_offset = task.get(CONF_DUE_HOURS_FORWARD_TO_GET) self.task_last_created = dt.utcnow() - timedelta(minutes=5) self.task_last_completed = dt.utcnow() - timedelta(minutes=5) + self._zero_date = datetime(1, 1, 1, 0, 0, 0, tzinfo=dt.DEFAULT_TIME_ZONE) + self._state = None + self._extra_attributes = None @property def icon(self): """Entity icon.""" return "mdi:clipboard-check-outline" + @property + def state(self): + """Sensor state.""" + return self._state + @property def extra_state_attributes(self): + """Device state attributes.""" + return self._extra_attributes + + def _handle_coordinator_update(self) -> None: + tasks = list(self.coordinator.data[self.entity_key][ATTR_DATA]) + self._state = len(tasks) + self._extra_attributes = self._update_extra_state_attributes(tasks) + + tasks = self.coordinator.data[self.entity_key][ATTR_TASKS] + task_last_completed = self._zero_date + task_last_created = self._zero_date + for task in tasks: + if task.completed and task.completed > self.task_last_completed: + self._raise_event_external( + EVENT_COMPLETED_TASK, + task.task_id, + ATTR_COMPLETED, + task.completed, + ) + if task.completed > task_last_completed: + task_last_completed = task.completed + if task.created and task.created > self.task_last_created: + self._raise_event_external( + EVENT_NEW_TASK, task.task_id, ATTR_CREATED, task.created + ) + if task.created > task_last_created: + task_last_created = task.created + + if task_last_completed > self._zero_date: + self.task_last_completed = task_last_completed + if task_last_created > self._zero_date: + self.task_last_created = task_last_created + + self.async_write_ha_state() + + def _update_extra_state_attributes(self, tasks): """Extra state attributes.""" all_tasks = [] overdue_tasks = [] - for item in self.coordinator.data[self.entity_key][ATTR_TASKS]: + for item in tasks: task = {ATTR_SUBJECT: item.subject, ATTR_TASK_ID: item.task_id} if item.body: task[ATTR_DESCRIPTION] = item.body @@ -186,8 +233,63 @@ def _raise_event(self, event_type, task_id): ) _LOGGER.debug("%s - %s", event_type, task_id) + def _raise_event_external(self, event_type, task_id, time_type, task_datetime): + self.hass.bus.fire( + f"{DOMAIN}_{event_type}", + {ATTR_TASK_ID: task_id, time_type: task_datetime, EVENT_HA_EVENT: False}, + ) + _LOGGER.debug("%s - %s - %s", event_type, task_id, task_datetime) + def _validate_task_permissions(self): return self._validate_permissions( PERM_MINIMUM_TASKS_WRITE, f"Not authorised to create new task - requires permission: {PERM_TASKS_READWRITE}", ) + + +def build_todo_query(key, todo): + """Build query for ToDo.""" + task = key[CONF_TASK_LIST] + show_completed = task[CONF_SHOW_COMPLETED] + query = todo.new_query() + if not show_completed: + query = query.on_attribute("status").unequal("completed") + start_offset = task.get(CONF_DUE_HOURS_BACKWARD_TO_GET) + end_offset = task.get(CONF_DUE_HOURS_FORWARD_TO_GET) + if start_offset: + start = dt.utcnow() + timedelta(hours=start_offset) + query.chain("and").on_attribute("due").greater_equal( + start.strftime("%Y-%m-%dT%H:%M:%S") + ) + if end_offset: + end = dt.utcnow() + timedelta(hours=end_offset) + query.chain("and").on_attribute("due").less_equal( + end.strftime("%Y-%m-%dT%H:%M:%S") + ) + return query + + +class O365TasksSensorSensorServices: + """Sensor Services.""" + + def __init__(self, hass): + """Initialise the sensor services.""" + self._hass = hass + + async def async_scan_for_task_lists(self, call): # pylint: disable=unused-argument + """Scan for new task lists.""" + for config in self._hass.data[DOMAIN]: + config = self._hass.data[DOMAIN][config] + todo_sensor = config.get(CONF_TODO_SENSORS) + if todo_sensor and CONF_ACCOUNT in config and todo_sensor.get(CONF_ENABLED): + todos = config[CONF_ACCOUNT].tasks() + + todolists = await self._hass.async_add_executor_job(todos.list_folders) + track = todo_sensor.get(CONF_TRACK_NEW) + for todo in todolists: + update_task_list_file( + config, + todo, + self._hass, + track, + ) diff --git a/custom_components/o365/classes/teamssensor.py b/custom_components/o365/classes/teamssensor.py index 8d7aef9..8caace5 100644 --- a/custom_components/o365/classes/teamssensor.py +++ b/custom_components/o365/classes/teamssensor.py @@ -12,6 +12,7 @@ ATTR_IMPORTANCE, ATTR_SUBJECT, ATTR_SUMMARY, + CONF_ACCOUNT, DOMAIN, EVENT_HA_EVENT, EVENT_SEND_CHAT_MESSAGE, @@ -28,12 +29,10 @@ class O365TeamsSensor(O365Sensor): """O365 Teams sensor processing.""" - def __init__( - self, cordinator, account, name, entity_id, config, entity_type, unique_id - ): + def __init__(self, cordinator, name, entity_id, config, entity_type, unique_id): """Initialise the Teams Sensor.""" super().__init__(cordinator, config, name, entity_id, entity_type, unique_id) - self.teams = account.teams() + self.teams = self._config[CONF_ACCOUNT].teams() @property def icon(self): @@ -44,11 +43,10 @@ def icon(self): class O365TeamsStatusSensor(O365TeamsSensor, SensorEntity): """O365 Teams sensor processing.""" - def __init__(self, coordinator, account, name, entity_id, config, unique_id): + def __init__(self, coordinator, name, entity_id, config, unique_id): """Initialise the Teams Sensor.""" super().__init__( coordinator, - account, name, entity_id, config, @@ -60,14 +58,11 @@ def __init__(self, coordinator, account, name, entity_id, config, unique_id): class O365TeamsChatSensor(O365TeamsSensor, SensorEntity): """O365 Teams Chat sensor processing.""" - def __init__( - self, coordinator, account, name, entity_id, config, unique_id, enable_update - ): + def __init__(self, coordinator, name, entity_id, config, unique_id): """Initialise the Teams Chat Sensor.""" super().__init__( - coordinator, account, name, entity_id, config, SENSOR_TEAMS_CHAT, unique_id + coordinator, name, entity_id, config, SENSOR_TEAMS_CHAT, unique_id ) - self.enable_update = enable_update @property def extra_state_attributes(self): diff --git a/custom_components/o365/const.py b/custom_components/o365/const.py index 1617e4f..ae88de3 100644 --- a/custom_components/o365/const.py +++ b/custom_components/o365/const.py @@ -82,6 +82,7 @@ class EventResponse(Enum): CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" # nosec CONF_CONFIG_TYPE = "config_type" +CONF_COORDINATOR = "coordinator" CONF_DEVICE_ID = "device_id" CONF_DOWNLOAD_ATTACHMENTS = "download_attachments" CONF_DUE_HOURS_BACKWARD_TO_GET = "due_start_offset" @@ -89,6 +90,8 @@ class EventResponse(Enum): CONF_EMAIL_SENSORS = "email_sensor" CONF_ENABLE_UPDATE = "enable_update" CONF_ENTITIES = "entities" +CONF_ENTITY_KEY = "entity_key" +CONF_ENTITY_TYPE = "entity_type" CONF_EXCLUDE = "exclude" CONF_FAILED_PERMISSIONS = "failed_permissions" CONF_GROUPS = "groups" @@ -98,23 +101,29 @@ class EventResponse(Enum): CONF_HTML_BODY = "html_body" CONF_IMPORTANCE = "importance" CONF_IS_UNREAD = "is_unread" +CONF_KEYS = "keys" CONF_MAIL_FOLDER = "folder" CONF_MAIL_FROM = "from" CONF_MAX_ITEMS = "max_items" CONF_MAX_RESULTS = "max_results" +CONF_O365_MAIL_FOLDER = "mail_folder" CONF_PERMISSIONS = "permissions" +CONF_QUERY = "query" CONF_QUERY_SENSORS = "query_sensors" CONF_SEARCH = "search" +CONF_SENSOR_CONF = "sensor_conf" CONF_SHARED_MAILBOX = "shared_mailbox" CONF_SHOW_COMPLETED = "show_completed" CONF_STATUS_SENSORS = "status_sensors" CONF_SUBJECT_CONTAINS = "subject_contains" CONF_SUBJECT_IS = "subject_is" +CONF_TODO = "todo" CONF_TODO_SENSORS = "todo_sensors" CONF_TRACK = "track" CONF_TRACK_NEW_CALENDAR = "track_new_calendar" CONF_TRACK_NEW = "track_new" CONF_TASK_LIST_ID = "task_list_id" +CONF_TASK_LIST = "task_list" CONF_URL = "url" CONST_CONFIG_TYPE_DICT = "dict" CONST_CONFIG_TYPE_LIST = "list" @@ -198,7 +207,7 @@ class EventResponse(Enum): SENSOR_AUTO_REPLY = "auto_reply" SENSOR_ENTITY_ID_FORMAT = "sensor.{}" -SENSOR_MAIL = "inbox" +SENSOR_EMAIL = "inbox" SENSOR_TEAMS_STATUS = "teams_status" SENSOR_TEAMS_CHAT = "teams_chat" SENSOR_TODO = "todo" diff --git a/custom_components/o365/coordinator.py b/custom_components/o365/coordinator.py new file mode 100644 index 0000000..fa471cf --- /dev/null +++ b/custom_components/o365/coordinator.py @@ -0,0 +1,460 @@ +"""Sensor processing.""" +import functools as ft +import logging +from datetime import datetime, timedelta + +from homeassistant.const import CONF_ENABLED, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import dt +from requests.exceptions import HTTPError + +from .classes.mailsensor import build_inbox_query, build_query_query +from .classes.taskssensor import O365TasksSensorSensorServices, build_todo_query +from .const import ( + ATTR_AUTOREPLIESSETTINGS, + ATTR_CHAT_ID, + ATTR_CHAT_TYPE, + ATTR_CONTENT, + ATTR_DATA, + ATTR_ERROR, + ATTR_FROM_DISPLAY_NAME, + ATTR_IMPORTANCE, + ATTR_MEMBERS, + ATTR_STATE, + ATTR_SUBJECT, + ATTR_SUMMARY, + ATTR_TASK_ID, + ATTR_TASKS, + ATTR_TOPIC, + CONF_ACCOUNT, + CONF_ACCOUNT_NAME, + CONF_AUTO_REPLY_SENSORS, + CONF_CHAT_SENSORS, + CONF_DOWNLOAD_ATTACHMENTS, + CONF_EMAIL_SENSORS, + CONF_ENABLE_UPDATE, + CONF_ENTITY_KEY, + CONF_ENTITY_TYPE, + CONF_MAIL_FOLDER, + CONF_MAX_ITEMS, + CONF_O365_MAIL_FOLDER, + CONF_QUERY, + CONF_QUERY_SENSORS, + CONF_SENSOR_CONF, + CONF_STATUS_SENSORS, + CONF_TASK_LIST, + CONF_TASK_LIST_ID, + CONF_TODO, + CONF_TODO_SENSORS, + CONF_TRACK, + DOMAIN, + EVENT_HA_EVENT, + LEGACY_ACCOUNT_NAME, + SENSOR_AUTO_REPLY, + SENSOR_EMAIL, + SENSOR_ENTITY_ID_FORMAT, + SENSOR_TEAMS_CHAT, + SENSOR_TEAMS_STATUS, + SENSOR_TODO, + YAML_TASK_LISTS, +) +from .schema import TASK_LIST_SCHEMA +from .utils.filemgmt import build_config_file_path, build_yaml_filename, load_yaml_file + +_LOGGER = logging.getLogger(__name__) + + +class O365SensorCordinator(DataUpdateCoordinator): + """O365 sensor data update coordinator.""" + + def __init__(self, hass, config): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="O365 Sensors", + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=30), + ) + self._config = config + self._account = config[CONF_ACCOUNT] + self._account_name = config[CONF_ACCOUNT_NAME] + self._keys = [] + self._data = {} + self._zero_date = datetime(1, 1, 1, 0, 0, 0, tzinfo=dt.DEFAULT_TIME_ZONE) + self._chat_members = {} + + async def async_setup_entries(self): + """Do the initial setup of the entities.""" + email_keys = await self._async_email_sensors() + query_keys = await self._async_query_sensors() + status_keys = self._status_sensors() + chat_keys = self._chat_sensors() + todo_keys = await self._async_todo_sensors() + auto_reply_entities = await self._async_auto_reply_sensors() + self._keys = ( + email_keys + + query_keys + + chat_keys + + status_keys + + todo_keys + + auto_reply_entities + ) + return self._keys + + async def _async_email_sensors(self): + email_sensors = self._config.get(CONF_EMAIL_SENSORS, []) + keys = [] + _LOGGER.debug("Email sensor setup: %s ", self._account_name) + for sensor_conf in email_sensors: + name = sensor_conf[CONF_NAME] + _LOGGER.debug( + "Email sensor setup: %s, %s", + self._account_name, + name, + ) + if mail_folder := await self._async_get_mail_folder( + sensor_conf, CONF_EMAIL_SENSORS + ): + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}", + CONF_SENSOR_CONF: sensor_conf, + CONF_O365_MAIL_FOLDER: mail_folder, + CONF_NAME: name, + CONF_ENTITY_TYPE: SENSOR_EMAIL, + CONF_QUERY: build_inbox_query(mail_folder, sensor_conf), + } + + keys.append(new_key) + return keys + + async def _async_query_sensors(self): + query_sensors = self._config.get(CONF_QUERY_SENSORS, []) + keys = [] + for sensor_conf in query_sensors: + if mail_folder := await self._async_get_mail_folder( + sensor_conf, CONF_QUERY_SENSORS + ): + name = sensor_conf.get(CONF_NAME) + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}", + CONF_SENSOR_CONF: sensor_conf, + CONF_O365_MAIL_FOLDER: mail_folder, + CONF_NAME: name, + CONF_ENTITY_TYPE: SENSOR_EMAIL, + CONF_QUERY: build_query_query(mail_folder, sensor_conf), + } + keys.append(new_key) + return keys + + def _status_sensors(self): + status_sensors = self._config.get(CONF_STATUS_SENSORS, []) + keys = [] + for sensor_conf in status_sensors: + name = sensor_conf.get(CONF_NAME) + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{name}_{self._account_name}", + CONF_NAME: name, + CONF_ENTITY_TYPE: SENSOR_TEAMS_STATUS, + } + + keys.append(new_key) + return keys + + def _chat_sensors(self): + chat_sensors = self._config.get(CONF_CHAT_SENSORS, []) + keys = [] + for sensor_conf in chat_sensors: + name = sensor_conf.get(CONF_NAME) + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{name}_{self._account_name}", + CONF_NAME: name, + CONF_ENTITY_TYPE: SENSOR_TEAMS_CHAT, + CONF_ENABLE_UPDATE: sensor_conf.get(CONF_ENABLE_UPDATE), + } + + keys.append(new_key) + return keys + + async def _async_todo_sensors(self): + todo_sensors = self._config.get(CONF_TODO_SENSORS) + keys = [] + if todo_sensors and todo_sensors.get(CONF_ENABLED): + sensor_services = O365TasksSensorSensorServices(self.hass) + await sensor_services.async_scan_for_task_lists(None) + + yaml_filename = build_yaml_filename(self._config, YAML_TASK_LISTS) + yaml_filepath = build_config_file_path(self.hass, yaml_filename) + task_dict = load_yaml_file( + yaml_filepath, CONF_TASK_LIST_ID, TASK_LIST_SCHEMA + ) + task_lists = list(task_dict.values()) + keys = await self._async_todo_entities(task_lists) + + return keys + + async def _async_todo_entities(self, task_lists): + keys = [] + tasks = self._account.tasks() + for tasklist in task_lists: + track = tasklist.get(CONF_TRACK) + if not track: + continue + + task_list_id = tasklist.get(CONF_TASK_LIST_ID) + if self._account_name != LEGACY_ACCOUNT_NAME: + name = f"{tasklist.get(CONF_NAME)} {self._account_name}" + else: + name = tasklist.get(CONF_NAME) + try: + todo = ( + await self.hass.async_add_executor_job( # pylint: disable=no-member + ft.partial( + tasks.get_folder, + folder_id=task_list_id, + ) + ) + ) + + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{task_list_id}_{self._account_name}", + CONF_TODO: todo, + CONF_NAME: name, + CONF_TASK_LIST: tasklist, + CONF_ENTITY_TYPE: SENSOR_TODO, + } + + keys.append(new_key) + except HTTPError: + _LOGGER.warning( + "Task list not found for: %s - Please remove from O365_tasks_%s.yaml", + name, + self._account_name, + ) + return keys + + async def _async_auto_reply_sensors(self): + auto_reply_sensors = self._config.get(CONF_AUTO_REPLY_SENSORS, []) + keys = [] + for sensor_conf in auto_reply_sensors: + name = sensor_conf.get(CONF_NAME) + new_key = { + CONF_ENTITY_KEY: self._build_entity_id(name), + CONF_UNIQUE_ID: f"{name}_{self._account_name}", + CONF_NAME: name, + CONF_ENTITY_TYPE: SENSOR_AUTO_REPLY, + } + + keys.append(new_key) + return keys + + async def _async_get_mail_folder(self, sensor_conf, sensor_type): + """Get the configured folder.""" + mailbox = self._account.mailbox() + _LOGGER.debug("Get mail folder: %s", sensor_conf.get(CONF_NAME)) + if mail_folder_conf := sensor_conf.get(CONF_MAIL_FOLDER): + return await self._async_get_configured_mail_folder( + mail_folder_conf, mailbox, sensor_type + ) + + return mailbox.inbox_folder() + + async def _async_get_configured_mail_folder( + self, mail_folder_conf, mailbox, sensor_type + ): + mail_folder = mailbox + for folder in mail_folder_conf.split("/"): + mail_folder = await self.hass.async_add_executor_job( + ft.partial( + mail_folder.get_folder, + folder_name=folder, + ) + ) + if not mail_folder: + _LOGGER.error( + "Folder - %s - not found from %s config entry - %s - entity not created", + folder, + sensor_type, + mail_folder_conf, + ) + return None + + return mail_folder + + async def _async_update_data(self): + _LOGGER.debug("Doing sensor update for: %s", self._account_name) + + for key in self._keys: + entity_type = key[CONF_ENTITY_TYPE] + if entity_type == SENSOR_EMAIL: + await self._async_email_update(key) + elif entity_type == SENSOR_TODO: + await self._async_todos_update(key) + elif entity_type == SENSOR_TEAMS_CHAT: + await self._async_teams_chat_update(key) + elif entity_type == SENSOR_TEAMS_STATUS: + await self._async_teams_status_update(key) + elif entity_type == SENSOR_AUTO_REPLY: + await self._async_auto_reply_update(key) + + return self._data + + async def _async_email_update(self, key): + """Update code.""" + sensor_conf = key[CONF_SENSOR_CONF] + download_attachments = sensor_conf.get(CONF_DOWNLOAD_ATTACHMENTS) + max_items = sensor_conf.get(CONF_MAX_ITEMS, 5) + mail_folder = key[CONF_O365_MAIL_FOLDER] + entity_key = key[CONF_ENTITY_KEY] + query = key[CONF_QUERY] + + data = await self.hass.async_add_executor_job( # pylint: disable=no-member + ft.partial( + mail_folder.get_messages, + limit=max_items, + query=query, + download_attachments=download_attachments, + ) + ) + self._data[entity_key] = {ATTR_DATA: data} + + async def _async_teams_status_update(self, key): + """Update state.""" + entity_key = key[CONF_ENTITY_KEY] + if data := await self.hass.async_add_executor_job( + self._account.teams().get_my_presence + ): + self._data[entity_key] = {ATTR_STATE: data.activity} + + async def _async_teams_chat_update(self, key): + entity_key = key[CONF_ENTITY_KEY] + state = None + data = [] + self._data[entity_key] = {} + extra_attributes = {} + chats = await self.hass.async_add_executor_job( + ft.partial(self._account.teams().get_my_chats, limit=20) + ) + for chat in chats: + if chat.chat_type == "unknownFutureValue": + continue + if not state: + messages = await self.hass.async_add_executor_job( + ft.partial(chat.get_messages, limit=10) + ) + state, extra_attributes = self._process_chat_messages(messages) + + if not key[CONF_ENABLE_UPDATE]: + if state: + break + continue + + memberlist = await self._async_get_memberlist(chat) + chatitems = { + ATTR_CHAT_ID: chat.object_id, + ATTR_CHAT_TYPE: chat.chat_type, + ATTR_MEMBERS: ",".join(memberlist), + } + if chat.chat_type == "group": + chatitems[ATTR_TOPIC] = chat.topic + + data.append(chatitems) + + self._data[entity_key] = ( + {ATTR_STATE: state} | extra_attributes | {ATTR_DATA: data} + ) + + def _process_chat_messages(self, messages): + state = None + extra_attributes = {} + for message in messages: + if not state and message.content != "": + state = message.created_date + extra_attributes = { + ATTR_FROM_DISPLAY_NAME: message.from_display_name, + ATTR_CONTENT: message.content, + ATTR_CHAT_ID: message.chat_id, + ATTR_IMPORTANCE: message.importance, + ATTR_SUBJECT: message.subject, + ATTR_SUMMARY: message.summary, + } + break + return state, extra_attributes + + async def _async_get_memberlist(self, chat): + if chat.object_id in self._chat_members and chat.chat_type != "oneOnOne": + return self._chat_members[chat.object_id] + members = await self.hass.async_add_executor_job(chat.get_members) + memberlist = [member.display_name for member in members] + self._chat_members[chat.object_id] = memberlist + return memberlist + + async def _async_todos_update(self, key): + """Update state.""" + entity_key = key[CONF_ENTITY_KEY] + if entity_key in self._data: + error = self._data[entity_key][ATTR_ERROR] + else: + self._data[entity_key] = {ATTR_TASKS: {}, ATTR_STATE: 0} + error = False + data, error = await self._async_todos_update_query(key, error) + if not error: + self._data[entity_key][ATTR_DATA] = data + + self._data[entity_key][ATTR_ERROR] = error + + async def _async_todos_update_query(self, key, error): + data = None + todo = key[CONF_TODO] + full_query = build_todo_query(key, todo) + name = key[CONF_NAME] + + try: + data = await self.hass.async_add_executor_job( # pylint: disable=no-member + ft.partial(todo.get_tasks, batch=100, query=full_query) + ) + if error: + _LOGGER.info("Task list reconnected for: %s", name) + error = False + except HTTPError: + if not error: + _LOGGER.error( + "Task list not found for: %s - Has it been deleted?", + name, + ) + error = True + + return data, error + + async def _async_auto_reply_update(self, key): + """Update state.""" + entity_key = key[CONF_ENTITY_KEY] + if data := await self.hass.async_add_executor_job( + self._account.mailbox().get_settings + ): + self._data[entity_key] = { + ATTR_STATE: data.automaticrepliessettings.status.value, + ATTR_AUTOREPLIESSETTINGS: data.automaticrepliessettings, + } + + def _build_entity_id(self, name): + """Build and entity ID.""" + return async_generate_entity_id( + SENSOR_ENTITY_ID_FORMAT, + name, + hass=self.hass, + ) + + def _raise_event(self, event_type, task_id, time_type, task_datetime): + self.hass.bus.fire( + f"{DOMAIN}_{event_type}", + {ATTR_TASK_ID: task_id, time_type: task_datetime, EVENT_HA_EVENT: False}, + ) + _LOGGER.debug("%s - %s - %s", event_type, task_id, task_datetime) diff --git a/custom_components/o365/manifest.json b/custom_components/o365/manifest.json index 2bcaeee..0457c29 100644 --- a/custom_components/o365/manifest.json +++ b/custom_components/o365/manifest.json @@ -15,5 +15,5 @@ "O365==2.0.28", "BeautifulSoup4>=4.10.0" ], - "version": "v4.4.2" + "version": "v4.4.3b1" } \ No newline at end of file diff --git a/custom_components/o365/repairs.py b/custom_components/o365/repairs.py index ae595f4..5f65d1b 100644 --- a/custom_components/o365/repairs.py +++ b/custom_components/o365/repairs.py @@ -153,7 +153,7 @@ async def _async_validate_response(self, user_input): if not permissions: errors[CONF_URL] = "minimum_permissions" - do_setup( + await do_setup( self.hass, self._conf, self._account, diff --git a/custom_components/o365/sensor.py b/custom_components/o365/sensor.py index cbdb661..d171bd0 100644 --- a/custom_components/o365/sensor.py +++ b/custom_components/o365/sensor.py @@ -1,86 +1,47 @@ """Sensor processing.""" -import functools as ft + import logging -from copy import deepcopy -from datetime import datetime, timedelta -from operator import itemgetter -from homeassistant.const import CONF_ENABLED, CONF_NAME +from homeassistant.const import CONF_ENABLED, CONF_NAME, CONF_UNIQUE_ID from homeassistant.helpers import entity_platform -from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util import dt -from requests.exceptions import HTTPError -from .classes.mailsensor import O365AutoReplySensor, O365EmailSensor, O365QuerySensor -from .classes.taskssensor import O365TasksSensor +from .classes.mailsensor import O365AutoReplySensor, O365MailSensor +from .classes.taskssensor import O365TasksSensor, O365TasksSensorSensorServices from .classes.teamssensor import O365TeamsChatSensor, O365TeamsStatusSensor from .const import ( - ATTR_ATTRIBUTES, - ATTR_AUTOREPLIESSETTINGS, - ATTR_CHAT_ID, - ATTR_CHAT_TYPE, - ATTR_COMPLETED, - ATTR_CONTENT, - ATTR_CREATED, - ATTR_DATA, - ATTR_ERROR, - ATTR_FROM_DISPLAY_NAME, - ATTR_IMPORTANCE, - ATTR_MEMBERS, - ATTR_STATE, - ATTR_SUBJECT, - ATTR_SUMMARY, - ATTR_TASK_ID, - ATTR_TASKS, - ATTR_TOPIC, CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, - CONF_EMAIL_SENSORS, + CONF_COORDINATOR, CONF_ENABLE_UPDATE, - CONF_MAIL_FOLDER, + CONF_ENTITY_KEY, + CONF_ENTITY_TYPE, + CONF_KEYS, CONF_PERMISSIONS, - CONF_QUERY_SENSORS, - CONF_STATUS_SENSORS, - CONF_TASK_LIST_ID, + CONF_SENSOR_CONF, + CONF_TASK_LIST, + CONF_TODO, CONF_TODO_SENSORS, - CONF_TRACK, - CONF_TRACK_NEW, DOMAIN, - EVENT_COMPLETED_TASK, - EVENT_HA_EVENT, - EVENT_NEW_TASK, - LEGACY_ACCOUNT_NAME, PERM_MINIMUM_CHAT_WRITE, PERM_MINIMUM_MAILBOX_SETTINGS, PERM_MINIMUM_TASKS_WRITE, SENSOR_AUTO_REPLY, - SENSOR_ENTITY_ID_FORMAT, - SENSOR_MAIL, + SENSOR_EMAIL, SENSOR_TEAMS_CHAT, SENSOR_TEAMS_STATUS, SENSOR_TODO, - YAML_TASK_LISTS, ) from .schema import ( AUTO_REPLY_SERVICE_DISABLE_SCHEMA, AUTO_REPLY_SERVICE_ENABLE_SCHEMA, CHAT_SERVICE_SEND_MESSAGE_SCHEMA, - TASK_LIST_SCHEMA, TASK_SERVICE_COMPLETE_SCHEMA, TASK_SERVICE_DELETE_SCHEMA, TASK_SERVICE_NEW_SCHEMA, TASK_SERVICE_UPDATE_SCHEMA, ) -from .utils.filemgmt import ( - build_config_file_path, - build_yaml_filename, - load_yaml_file, - update_task_list_file, -) -from .utils.utils import get_email_attributes _LOGGER = logging.getLogger(__name__) @@ -100,441 +61,67 @@ async def async_setup_platform( if not is_authenticated: return False - coordinator = O365SensorCordinator(hass, conf) - entities = await coordinator.async_setup_entries() - await coordinator.async_config_entry_first_refresh() - async_add_entities(entities, False) - await _async_setup_register_services(hass, conf) - - return True - - -class O365SensorCordinator(DataUpdateCoordinator): - """O365 sensor data update coordinator.""" - - def __init__(self, hass, config): - """Initialize my coordinator.""" - super().__init__( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="O365 Sensors", - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=30), - ) - self._config = config - self._account = config[CONF_ACCOUNT] - self._account_name = config[CONF_ACCOUNT_NAME] - self._entities = [] - self._data = {} - self._zero_date = datetime(1, 1, 1, 0, 0, 0, tzinfo=dt.DEFAULT_TIME_ZONE) - self._chat_members = {} - - async def async_setup_entries(self): - """Do the initial setup of the entities.""" - email_entities = await self._async_email_sensors() - query_entities = await self._async_query_sensors() - status_entities = self._status_sensors() - chat_entities = self._chat_sensors() - todo_entities = await self._async_todo_sensors() - auto_reply_entities = await self._async_auto_reply_sensors() - self._entities = ( - email_entities - + query_entities - + status_entities - + chat_entities - + todo_entities - + auto_reply_entities - ) - return self._entities - - async def _async_email_sensors(self): - email_sensors = self._config.get(CONF_EMAIL_SENSORS, []) - entities = [] - _LOGGER.debug("Email sensor setup: %s ", self._account_name) - for sensor_conf in email_sensors: - name = sensor_conf[CONF_NAME] - _LOGGER.debug( - "Email sensor setup: %s, %s", - self._account_name, - name, - ) - if mail_folder := await self._async_get_mail_folder( - sensor_conf, CONF_EMAIL_SENSORS - ): - entity_id = self._build_entity_id(name) - unique_id = f"{mail_folder.folder_id}_{self._account_name}" - emailsensor = O365EmailSensor( - self, - self._config, - sensor_conf, - mail_folder, - name, - entity_id, - unique_id, - ) - _LOGGER.debug( - "Email sensor added: %s, %s", - self._account_name, - name, + coordinator = conf[CONF_COORDINATOR] + sensorentities = [] + for key in conf[CONF_KEYS]: + if key[CONF_ENTITY_TYPE] in SENSOR_EMAIL: + sensorentities.append( + O365MailSensor( + coordinator, + conf, + key[CONF_SENSOR_CONF], + key[CONF_NAME], + key[CONF_ENTITY_KEY], + key[CONF_UNIQUE_ID], ) - entities.append(emailsensor) - return entities - - async def _async_query_sensors(self): - query_sensors = self._config.get(CONF_QUERY_SENSORS, []) - entities = [] - for sensor_conf in query_sensors: - if mail_folder := await self._async_get_mail_folder( - sensor_conf, CONF_QUERY_SENSORS - ): - name = sensor_conf.get(CONF_NAME) - entity_id = self._build_entity_id(name) - unique_id = f"{mail_folder.folder_id}_{self._account_name}" - querysensor = O365QuerySensor( - self, - self._config, - sensor_conf, - mail_folder, - name, - entity_id, - unique_id, - ) - entities.append(querysensor) - return entities - - def _status_sensors(self): - status_sensors = self._config.get(CONF_STATUS_SENSORS, []) - entities = [] - for sensor_conf in status_sensors: - name = sensor_conf.get(CONF_NAME) - entity_id = self._build_entity_id(name) - unique_id = f"{name}_{self._account_name}" - teams_status_sensor = O365TeamsStatusSensor( - self, self._account, name, entity_id, self._config, unique_id ) - entities.append(teams_status_sensor) - return entities - - def _chat_sensors(self): - chat_sensors = self._config.get(CONF_CHAT_SENSORS, []) - entities = [] - for sensor_conf in chat_sensors: - name = sensor_conf.get(CONF_NAME) - enable_update = sensor_conf.get(CONF_ENABLE_UPDATE) - entity_id = self._build_entity_id(name) - unique_id = f"{name}_{self._account_name}" - teams_chat_sensor = O365TeamsChatSensor( - self, - self._account, - name, - entity_id, - self._config, - unique_id, - enable_update, - ) - entities.append(teams_chat_sensor) - return entities - - async def _async_todo_sensors(self): - todo_sensors = self._config.get(CONF_TODO_SENSORS) - entities = [] - if todo_sensors and todo_sensors.get(CONF_ENABLED): - sensor_services = SensorServices(self.hass) - await sensor_services.async_scan_for_task_lists(None) - - yaml_filename = build_yaml_filename(self._config, YAML_TASK_LISTS) - yaml_filepath = build_config_file_path(self.hass, yaml_filename) - task_dict = load_yaml_file( - yaml_filepath, CONF_TASK_LIST_ID, TASK_LIST_SCHEMA - ) - task_lists = list(task_dict.values()) - entities = await self._async_todo_entities(task_lists, self._config) - - return entities - - async def _async_todo_entities(self, task_lists, config): - entities = [] - tasks = self._account.tasks() - for task in task_lists: - track = task.get(CONF_TRACK) - if not track: - continue - - task_list_id = task.get(CONF_TASK_LIST_ID) - if self._account_name != LEGACY_ACCOUNT_NAME: - name = f"{task.get(CONF_NAME)} {self._account_name}" - else: - name = task.get(CONF_NAME) - try: - todo = ( - await self.hass.async_add_executor_job( # pylint: disable=no-member - ft.partial( - tasks.get_folder, - folder_id=task_list_id, - ) - ) - ) - entity_id = self._build_entity_id(name) - unique_id = f"{task_list_id}_{self._account_name}" - todo_sensor = O365TasksSensor( - self, todo, name, task, config, entity_id, unique_id - ) - entities.append(todo_sensor) - except HTTPError: - _LOGGER.warning( - "Task list not found for: %s - Please remove from O365_tasks_%s.yaml", - name, - self._account_name, + elif key[CONF_ENTITY_TYPE] == SENSOR_TODO: + sensorentities.append( + O365TasksSensor( + coordinator, + key[CONF_TODO], + key[CONF_NAME], + key[CONF_TASK_LIST], + conf, + key[CONF_ENTITY_KEY], + key[CONF_UNIQUE_ID], ) - return entities - - async def _async_auto_reply_sensors(self): - auto_reply_sensors = self._config.get(CONF_AUTO_REPLY_SENSORS, []) - entities = [] - for sensor_conf in auto_reply_sensors: - name = sensor_conf.get(CONF_NAME) - entity_id = self._build_entity_id(name) - unique_id = f"{name}_{self._account_name}" - auto_reply_sensor = O365AutoReplySensor( - self, name, entity_id, self._config, unique_id - ) - entities.append(auto_reply_sensor) - return entities - - async def _async_get_mail_folder(self, sensor_conf, sensor_type): - """Get the configured folder.""" - mailbox = self._account.mailbox() - _LOGGER.debug("Get mail folder: %s", sensor_conf.get(CONF_NAME)) - if mail_folder_conf := sensor_conf.get(CONF_MAIL_FOLDER): - return await self._async_get_configured_mail_folder( - mail_folder_conf, mailbox, sensor_type ) - - return mailbox.inbox_folder() - - async def _async_get_configured_mail_folder( - self, mail_folder_conf, mailbox, sensor_type - ): - mail_folder = mailbox - for folder in mail_folder_conf.split("/"): - mail_folder = await self.hass.async_add_executor_job( - ft.partial( - mail_folder.get_folder, - folder_name=folder, + elif key[CONF_ENTITY_TYPE] == SENSOR_TEAMS_CHAT: + sensorentities.append( + O365TeamsChatSensor( + coordinator, + key[CONF_NAME], + key[CONF_ENTITY_KEY], + conf, + key[CONF_UNIQUE_ID], ) ) - if not mail_folder: - _LOGGER.error( - "Folder - %s - not found from %s config entry - %s - entity not created", - folder, - sensor_type, - mail_folder_conf, + elif key[CONF_ENTITY_TYPE] == SENSOR_TEAMS_STATUS: + sensorentities.append( + O365TeamsStatusSensor( + coordinator, + key[CONF_NAME], + key[CONF_ENTITY_KEY], + conf, + key[CONF_UNIQUE_ID], ) - return None - - return mail_folder - - async def _async_update_data(self): - _LOGGER.debug("Doing sensor update for: %s", self._account_name) - for entity in self._entities: - if entity.entity_type == SENSOR_MAIL: - await self._async_email_update(entity) - elif entity.entity_type == SENSOR_TEAMS_STATUS: - await self._async_teams_status_update(entity) - elif entity.entity_type == SENSOR_TEAMS_CHAT: - await self._async_teams_chat_update(entity) - elif entity.entity_type == SENSOR_TODO: - await self._async_todos_update(entity) - elif entity.entity_type == SENSOR_AUTO_REPLY: - await self._async_auto_reply_update(entity) - - return self._data - - async def _async_email_update(self, entity): - """Update code.""" - data = await self.hass.async_add_executor_job( # pylint: disable=no-member - ft.partial( - entity.mail_folder.get_messages, - limit=entity.max_items, - query=entity.query, - download_attachments=entity.download_attachments, ) - ) - attrs = await self.hass.async_add_executor_job( # pylint: disable=no-member - self._get_attributes, data, entity - ) - attrs.sort(key=itemgetter("received"), reverse=True) - self._data[entity.entity_key] = { - ATTR_STATE: len(attrs), - ATTR_ATTRIBUTES: {ATTR_DATA: attrs}, - } - - def _get_attributes(self, data, entity): - return [ - get_email_attributes(x, entity.download_attachments, entity.html_body) - for x in data - ] - - async def _async_teams_status_update(self, entity): - """Update state.""" - if data := await self.hass.async_add_executor_job(entity.teams.get_my_presence): - self._data[entity.entity_key] = {ATTR_STATE: data.activity} - - async def _async_teams_chat_update(self, entity): - """Update state.""" - state = None - data = [] - self._data[entity.entity_key] = {} - extra_attributes = {} - chats = await self.hass.async_add_executor_job( - ft.partial(entity.teams.get_my_chats, limit=20) - ) - for chat in chats: - if chat.chat_type == "unknownFutureValue": - continue - if not state: - messages = await self.hass.async_add_executor_job( - ft.partial(chat.get_messages, limit=10) + elif key[CONF_ENTITY_TYPE] == SENSOR_AUTO_REPLY: + sensorentities.append( + O365AutoReplySensor( + coordinator, + key[CONF_NAME], + key[CONF_ENTITY_KEY], + conf, + key[CONF_UNIQUE_ID], ) - state, extra_attributes = self._process_chat_messages(messages) - - if not entity.enable_update: - if state: - break - continue - - memberlist = await self._async_get_memberlist(chat) - chatitems = { - ATTR_CHAT_ID: chat.object_id, - ATTR_CHAT_TYPE: chat.chat_type, - ATTR_MEMBERS: ",".join(memberlist), - } - if chat.chat_type == "group": - chatitems[ATTR_TOPIC] = chat.topic - - data.append(chatitems) - - self._data[entity.entity_key] = ( - {ATTR_STATE: state} | extra_attributes | {ATTR_DATA: data} - ) - - def _process_chat_messages(self, messages): - state = None - extra_attributes = {} - for message in messages: - if not state and message.content != "": - state = message.created_date - extra_attributes = { - ATTR_FROM_DISPLAY_NAME: message.from_display_name, - ATTR_CONTENT: message.content, - ATTR_CHAT_ID: message.chat_id, - ATTR_IMPORTANCE: message.importance, - ATTR_SUBJECT: message.subject, - ATTR_SUMMARY: message.summary, - } - break - return state, extra_attributes - - async def _async_get_memberlist(self, chat): - if chat.object_id in self._chat_members and chat.chat_type != "oneOnOne": - return self._chat_members[chat.object_id] - members = await self.hass.async_add_executor_job(chat.get_members) - memberlist = [member.display_name for member in members] - self._chat_members[chat.object_id] = memberlist - return memberlist - - async def _async_todos_update(self, entity): - """Update state.""" - if entity.entity_key in self._data: - error = self._data[entity.entity_key][ATTR_ERROR] - else: - self._data[entity.entity_key] = {ATTR_TASKS: {}, ATTR_STATE: 0} - error = False - data, error = await self._async_todos_update_query(entity, error) - if not error: - tasks = list(data) - self._data[entity.entity_key][ATTR_TASKS] = tasks - self._data[entity.entity_key][ATTR_STATE] = len(tasks) - task_last_completed = self._zero_date - task_last_created = self._zero_date - for task in tasks: - if task.completed and task.completed > entity.task_last_completed: - self._raise_event( - EVENT_COMPLETED_TASK, - task.task_id, - ATTR_COMPLETED, - task.completed, - ) - if task.completed > task_last_completed: - task_last_completed = task.completed - if task.created and task.created > entity.task_last_created: - self._raise_event( - EVENT_NEW_TASK, task.task_id, ATTR_CREATED, task.created - ) - if task.created > task_last_created: - task_last_created = task.created - - if task_last_completed > self._zero_date: - entity.task_last_completed = task_last_completed - if task_last_created > self._zero_date: - entity.task_last_created = task_last_created - - self._data[entity.entity_key][ATTR_ERROR] = error - - async def _async_todos_update_query(self, entity, error): - data = None - full_query = deepcopy(entity.query) - if entity.start_offset: - start = dt.utcnow() + timedelta(hours=entity.start_offset) - full_query.chain("and").on_attribute("due").greater_equal( - start.strftime("%Y-%m-%dT%H:%M:%S") - ) - if entity.end_offset: - end = dt.utcnow() + timedelta(hours=entity.end_offset) - full_query.chain("and").on_attribute("due").less_equal( - end.strftime("%Y-%m-%dT%H:%M:%S") ) - try: - data = await self.hass.async_add_executor_job( # pylint: disable=no-member - ft.partial(entity.todo.get_tasks, batch=100, query=full_query) - ) - if error: - _LOGGER.info("Task list reconnected for: %s", entity.name) - error = False - except HTTPError: - if not error: - _LOGGER.error( - "Task list not found for: %s - Has it been deleted?", - entity.name, - ) - error = True - - return data, error - - async def _async_auto_reply_update(self, entity): - """Update state.""" - if data := await self.hass.async_add_executor_job(entity.mailbox.get_settings): - self._data[entity.entity_key] = { - ATTR_STATE: data.automaticrepliessettings.status.value, - ATTR_AUTOREPLIESSETTINGS: data.automaticrepliessettings, - } - - def _build_entity_id(self, name): - """Build and entity ID.""" - return async_generate_entity_id( - SENSOR_ENTITY_ID_FORMAT, - name, - hass=self.hass, - ) + async_add_entities(sensorentities, True) + await _async_setup_register_services(hass, conf) - def _raise_event(self, event_type, task_id, time_type, task_datetime): - self.hass.bus.fire( - f"{DOMAIN}_{event_type}", - {ATTR_TASK_ID: task_id, time_type: task_datetime, EVENT_HA_EVENT: False}, - ) - _LOGGER.debug("%s - %s - %s", event_type, task_id, task_datetime) + return True async def _async_setup_register_services(hass, config): @@ -553,7 +140,7 @@ async def _async_setup_task_services(hass, config, perms): ): return - sensor_services = SensorServices(hass) + sensor_services = O365TasksSensorSensorServices(hass) hass.services.async_register( DOMAIN, "scan_for_task_lists", sensor_services.async_scan_for_task_lists ) @@ -618,29 +205,3 @@ async def _async_setup_mailbox_services(config, perms): AUTO_REPLY_SERVICE_DISABLE_SCHEMA, "auto_reply_disable", ) - - -class SensorServices: - """Sensor Services.""" - - def __init__(self, hass): - """Initialise the sensor services.""" - self._hass = hass - - async def async_scan_for_task_lists(self, call): # pylint: disable=unused-argument - """Scan for new task lists.""" - for config in self._hass.data[DOMAIN]: - config = self._hass.data[DOMAIN][config] - todo_sensor = config.get(CONF_TODO_SENSORS) - if todo_sensor and CONF_ACCOUNT in config and todo_sensor.get(CONF_ENABLED): - todos = config[CONF_ACCOUNT].tasks() - - todolists = await self._hass.async_add_executor_job(todos.list_folders) - track = todo_sensor.get(CONF_TRACK_NEW) - for todo in todolists: - update_task_list_file( - config, - todo, - self._hass, - track, - ) diff --git a/custom_components/o365/setup.py b/custom_components/o365/setup.py index 2ed92df..bccda6c 100644 --- a/custom_components/o365/setup.py +++ b/custom_components/o365/setup.py @@ -9,8 +9,10 @@ CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, CONF_CONFIG_TYPE, + CONF_COORDINATOR, CONF_EMAIL_SENSORS, CONF_ENABLE_UPDATE, + CONF_KEYS, CONF_PERMISSIONS, CONF_QUERY_SENSORS, CONF_STATUS_SENSORS, @@ -18,9 +20,10 @@ CONF_TRACK_NEW_CALENDAR, DOMAIN, ) +from .coordinator import O365SensorCordinator -def do_setup(hass, config, account, account_name, conf_type, perms): +async def do_setup(hass, config, account, account_name, conf_type, perms): """Run the setup after we have everything configured.""" email_sensors = config.get(CONF_EMAIL_SENSORS, []) query_sensors = config.get(CONF_QUERY_SENSORS, []) @@ -48,6 +51,12 @@ def do_setup(hass, config, account, account_name, conf_type, perms): hass.data[DOMAIN] = {} hass.data[DOMAIN][account_name] = account_config + coordinator = O365SensorCordinator(hass, account_config) + keys = await coordinator.async_setup_entries() + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][account_name][CONF_KEYS] = keys + hass.data[DOMAIN][account_name][CONF_COORDINATOR] = coordinator + _load_platforms(hass, account_name, config, account_config)