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)