From 0184872e15b357489edfc56e1d11520fd187ba50 Mon Sep 17 00:00:00 2001 From: RogerSelwyn Date: Thu, 19 May 2022 16:14:39 +0100 Subject: [PATCH] Add Chat Sensor --- README.md | 23 ++++++-- custom_components/o365/__init__.py | 56 +++++++------------ custom_components/o365/const.py | 8 +++ custom_components/o365/schema.py | 8 +++ custom_components/o365/sensor.py | 86 ++++++++++++++++++++++++++++-- custom_components/o365/utils.py | 14 +++-- 6 files changed, 145 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 6571ba0..0206b42 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This integration enables: 2. Getting emails from your inbox. 3. Sending emails via the notify.o365_email service. 4. Getting presence from Teams (not for personal accounts) +5. Getting the latest chat message from Teams (not for personal accounts) This project would not be possible without the wonderful [python-o365 project](https://github.com/O365/python-o365). @@ -46,9 +47,12 @@ To allow authentication you first need to register your application at Azure App If you are creating an email_sensor or a query_sensor you will need: * Mail.Read - *Read access to user mail* - If you are creating an status_sensor you will need: + If you are creating a status_sensor you will need: * Presence.Read - *Read user's presence information* (**Not for personal accounts**) + If you are creating a chat_sensor you will need: + * Chat.Read - *Read user chat messages* (**Not for personal accounts**) + If ['enable_update'](#primary-method) is set to True, (it defaults to False for multi-account installs and True for other installs so as not to break existing installs), then the following permissions are also required (you can always remove permissions later): * Calendars.ReadWrite - *Read and write user calendars* * Mail.ReadWrite - *Read and write access to user mail* @@ -92,8 +96,10 @@ o365: has_attachment: True max_items: 2 is_unread: True - status_sensors: # Cannot be used for personal accounts - - name: "User Teams Status" + status_sensors: # Cannot be used for personal accounts + - name: "User Teams Status" + chat_sensors: # Cannot be used for personal accounts + - name: "User Chat" - account_name: Account2 client_secret: "xx.xxxxxxxxxxxxxxxxxxxxxxxxxxxx" client_id: "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx" @@ -118,8 +124,10 @@ o365: has_attachment: True max_items: 2 is_unread: True - status_sensors: # Cannot be used for personal accounts - - name: "User Teams Status" + status_sensors: # Cannot be used for personal accounts + - name: "User Teams Status" + chat_sensors: # Cannot be used for personal accounts + - name: "User Chat" ``` ### Configuration variables @@ -179,6 +187,11 @@ Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. +#### chat_sensors (not for personal accounts) +Key | Type | Required | Description +-- | -- | -- | -- +`name` | `string` | `True` | The name of the sensor. + ## Authentication _**NOTE:** The default authentication method has changed from version 3.2.0. The default is now to use the method which does not require access to your HA instance from the internet. If you previously did not set alt_auth_flow or had it set to False, please set it to True. This will only impact people re-authenticating._ diff --git a/custom_components/o365/__init__.py b/custom_components/o365/__init__.py index ad03ea2..fc47f7a 100644 --- a/custom_components/o365/__init__.py +++ b/custom_components/o365/__init__.py @@ -10,43 +10,22 @@ from homeassistant.helpers.network import get_url from O365 import Account, FileSystemTokenBackend -from .const import ( - AUTH_CALLBACK_NAME, - AUTH_CALLBACK_PATH_ALT, - AUTH_CALLBACK_PATH_DEFAULT, - CONF_ACCOUNT, - CONF_ACCOUNT_NAME, - CONF_ACCOUNTS, - CONF_ALT_AUTH_FLOW, - CONF_ALT_AUTH_METHOD, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_CONFIG_TYPE, - CONF_EMAIL_SENSORS, - CONF_ENABLE_UPDATE, - CONF_QUERY_SENSORS, - CONF_STATUS_SENSORS, - CONF_TRACK_NEW, - CONFIGURATOR_DESCRIPTION_ALT, - CONFIGURATOR_DESCRIPTION_DEFAULT, - CONFIGURATOR_FIELDS, - CONFIGURATOR_LINK_NAME, - CONFIGURATOR_SUBMIT_CAPTION, - CONST_CONFIG_TYPE_DICT, - CONST_CONFIG_TYPE_LIST, - CONST_PRIMARY, - DEFAULT_CACHE_PATH, - DEFAULT_NAME, - DOMAIN, -) +from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH_ALT, + AUTH_CALLBACK_PATH_DEFAULT, CONF_ACCOUNT, + CONF_ACCOUNT_NAME, CONF_ACCOUNTS, CONF_ALT_AUTH_FLOW, + CONF_ALT_AUTH_METHOD, CONF_CHAT_SENSORS, CONF_CLIENT_ID, + CONF_CLIENT_SECRET, CONF_CONFIG_TYPE, CONF_EMAIL_SENSORS, + CONF_ENABLE_UPDATE, CONF_QUERY_SENSORS, + CONF_STATUS_SENSORS, CONF_TRACK_NEW, + CONFIGURATOR_DESCRIPTION_ALT, + CONFIGURATOR_DESCRIPTION_DEFAULT, CONFIGURATOR_FIELDS, + CONFIGURATOR_LINK_NAME, CONFIGURATOR_SUBMIT_CAPTION, + CONST_CONFIG_TYPE_DICT, CONST_CONFIG_TYPE_LIST, + CONST_PRIMARY, DEFAULT_CACHE_PATH, DEFAULT_NAME, DOMAIN) from .schema import LEGACY_SCHEMA, MULTI_ACCOUNT_SCHEMA -from .utils import ( - build_config_file_path, - build_minimum_permissions, - build_requested_permissions, - build_token_filename, - validate_permissions, -) +from .utils import (build_config_file_path, build_minimum_permissions, + build_requested_permissions, build_token_filename, + validate_permissions) _LOGGER = logging.getLogger(__name__) @@ -101,6 +80,7 @@ def do_setup(hass, config, account, account_name, conf_type): email_sensors = config.get(CONF_EMAIL_SENSORS, []) query_sensors = config.get(CONF_QUERY_SENSORS, []) status_sensors = config.get(CONF_STATUS_SENSORS, []) + chat_sensors = config.get(CONF_CHAT_SENSORS, []) enable_update = config.get(CONF_ENABLE_UPDATE, True) account_config = { @@ -108,6 +88,7 @@ def do_setup(hass, config, account, account_name, conf_type): CONF_EMAIL_SENSORS: email_sensors, CONF_QUERY_SENSORS: query_sensors, CONF_STATUS_SENSORS: status_sensors, + CONF_CHAT_SENSORS: chat_sensors, CONF_ENABLE_UPDATE: enable_update, CONF_TRACK_NEW: config.get(CONF_TRACK_NEW, True), CONF_ACCOUNT_NAME: config.get(CONF_ACCOUNT_NAME, ""), @@ -136,6 +117,7 @@ def _load_platforms(hass, account_name, config, account_config): len(account_config[CONF_EMAIL_SENSORS]) > 0 or len(account_config[CONF_QUERY_SENSORS]) > 0 or len(account_config[CONF_STATUS_SENSORS]) > 0 + or len(account_config[CONF_CHAT_SENSORS]) > 0 ): hass.async_create_task( discovery.async_load_platform( @@ -238,7 +220,7 @@ def _get_auth_method(conf, account_name): if alt_flow is False: _auth_deprecated_message(account_name, True) return True - return bool(alt_method is True) + return alt_method is True def _auth_deprecated_message(account_name, method_value): diff --git a/custom_components/o365/const.py b/custom_components/o365/const.py index 4944827..ac3b4b4 100644 --- a/custom_components/o365/const.py +++ b/custom_components/o365/const.py @@ -16,11 +16,15 @@ class EventResponse(Enum): ATTR_BODY = "body" ATTR_CALENDAR_ID = "calendar_id" ATTR_CATEGORIES = "categories" +ATTR_CHAT_ID = "chat_id" +ATTR_CONTENT = "content" ATTR_EMAIL = "email" ATTR_END = "end" ATTR_ENTITY_ID = "entity_id" ATTR_EVENT_ID = "event_id" +ATTR_FROM_DISPLAY_NAME = "from_display_name" ATTR_IS_ALL_DAY = "is_all_day" +ATTR_IMPORTANCE = "importance" ATTR_LOCATION = "location" ATTR_MESSAGE_IS_HTML = "message_is_html" ATTR_PHOTOS = "photos" @@ -30,6 +34,7 @@ class EventResponse(Enum): ATTR_SHOW_AS = "show_as" ATTR_START = "start" ATTR_SUBJECT = "subject" +ATTR_SUMMARY = "summary" ATTR_TYPE = "type" ATTR_ZIP_ATTACHMENTS = "zip_attachments" ATTR_ZIP_NAME = "zip_name" @@ -72,6 +77,7 @@ class EventResponse(Enum): CONF_MAIL_FOLDER = "folder" CONF_MAIL_FROM = "from" CONF_MAX_ITEMS = "max_items" +CONF_CHAT_SENSORS = "chat_sensors" CONF_STATUS_SENSORS = "status_sensors" CONF_QUERY_SENSORS = "query_sensors" CONF_SUBJECT_CONTAINS = "subject_contains" @@ -104,6 +110,7 @@ class EventResponse(Enum): PERM_CALENDARS_READ_SHARED = "Calendars.Read.Shared" PERM_CALENDARS_READWRITE = "Calendars.ReadWrite" PERM_CALENDARS_READWRITE_SHARED = "Calendars.ReadWrite.Shared" +PERM_CHAT_READ = "Chat.Read" PERM_MAIL_READ = "Mail.Read" PERM_MAIL_READ_SHARED = "Mail.Read.Shared" PERM_MAIL_READWRITE = "Mail.ReadWrite" @@ -113,6 +120,7 @@ class EventResponse(Enum): PERM_OFFLINE_ACCESS = "offline_access" PERM_PRESENCE_READ = "Presence.Read" PERM_USER_READ = "User.Read" +PERM_MINIMUM_CHAT = [PERM_CHAT_READ, []] PERM_MINIMUM_PRESENCE = [PERM_PRESENCE_READ, []] PERM_MINIMUM_USER = [PERM_USER_READ, []] PERM_MINIMUM_MAIL = [ diff --git a/custom_components/o365/schema.py b/custom_components/o365/schema.py index 6e5b796..06df93d 100644 --- a/custom_components/o365/schema.py +++ b/custom_components/o365/schema.py @@ -41,6 +41,7 @@ CONF_ALT_AUTH_FLOW, CONF_ALT_AUTH_METHOD, CONF_CAL_ID, + CONF_CHAT_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DEVICE_ID, @@ -81,6 +82,11 @@ vol.Required(CONF_NAME): cv.string, } ) +CHAT_SENSOR = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + } +) QUERY_SENSOR = vol.Schema( { vol.Required(CONF_NAME): cv.string, @@ -107,6 +113,7 @@ vol.Optional(CONF_EMAIL_SENSORS): [EMAIL_SENSOR], vol.Optional(CONF_QUERY_SENSORS): [QUERY_SENSOR], vol.Optional(CONF_STATUS_SENSORS): [STATUS_SENSOR], + vol.Optional(CONF_CHAT_SENSORS): [CHAT_SENSOR], } ) MULTI_ACCOUNT_SCHEMA = vol.Schema( @@ -124,6 +131,7 @@ vol.Optional(CONF_EMAIL_SENSORS): [EMAIL_SENSOR], vol.Optional(CONF_QUERY_SENSORS): [QUERY_SENSOR], vol.Optional(CONF_STATUS_SENSORS): [STATUS_SENSOR], + vol.Optional(CONF_CHAT_SENSORS): [CHAT_SENSOR], } ] ) diff --git a/custom_components/o365/sensor.py b/custom_components/o365/sensor.py index 2e66593..4e032c2 100644 --- a/custom_components/o365/sensor.py +++ b/custom_components/o365/sensor.py @@ -8,8 +8,15 @@ from homeassistant.helpers.entity import Entity from .const import ( + ATTR_CHAT_ID, + ATTR_CONTENT, + ATTR_FROM_DISPLAY_NAME, + ATTR_IMPORTANCE, + ATTR_SUBJECT, + ATTR_SUMMARY, CONF_ACCOUNT, CONF_ACCOUNT_NAME, + CONF_CHAT_SENSORS, CONF_DOWNLOAD_ATTACHMENTS, CONF_EMAIL_SENSORS, CONF_HAS_ATTACHMENT, @@ -44,16 +51,17 @@ async def async_setup_platform( if not is_authenticated: return False - await _async_unread_sensors(hass, account, add_entities, conf) + await _async_email_sensors(hass, account, add_entities, conf) await _async_query_sensors(hass, account, add_entities, conf) _status_sensors(account, add_entities, conf) + _chat_sensors(account, add_entities, conf) return True -async def _async_unread_sensors(hass, account, add_entities, conf): - unread_sensors = conf.get(CONF_EMAIL_SENSORS, []) - for sensor_conf in unread_sensors: +async def _async_email_sensors(hass, account, add_entities, conf): + email_sensors = conf.get(CONF_EMAIL_SENSORS, []) + for sensor_conf in email_sensors: if mail_folder := await hass.async_add_executor_job( _get_mail_folder, account, sensor_conf, CONF_EMAIL_SENSORS ): @@ -78,6 +86,13 @@ def _status_sensors(account, add_entities, conf): add_entities([teams_status_sensor], True) +def _chat_sensors(account, add_entities, conf): + chat_sensors = conf.get(CONF_CHAT_SENSORS, []) + for sensor_conf in chat_sensors: + teams_chat_sensor = O365TeamsChatSensor(account, sensor_conf) + add_entities([teams_chat_sensor], True) + + def _get_mail_folder(account, sensor_conf, sensor_type): """Get the configured folder.""" mailbox = account.mailbox() @@ -239,3 +254,66 @@ async def async_update(self): """Update state.""" data = await self.hass.async_add_executor_job(self._teams.get_my_presence) self._state = data.activity + + +class O365TeamsChatSensor(Entity): + """O365 Teams Chat sensor processing.""" + + def __init__(self, account, conf): + """Initialise the Teams Chat Sensor.""" + self._teams = account.teams() + self._name = conf.get(CONF_NAME) + self._state = None + self._from_display_name = None + self._content = None + self._chat_id = None + self._importance = None + self._subject = None + self._summary = None + + @property + def name(self): + """Sensor name.""" + return self._name + + @property + def state(self): + """Sensor state.""" + return self._state + + @property + def extra_state_attributes(self): + """Return entity specific state attributes.""" + attributes = { + ATTR_FROM_DISPLAY_NAME: self._from_display_name, + ATTR_CONTENT: self._content, + ATTR_CHAT_ID: self._chat_id, + ATTR_IMPORTANCE: self._importance, + } + if self._subject: + attributes[ATTR_SUBJECT] = self._subject + if self._summary: + attributes[ATTR_SUMMARY] = self._summary + return attributes + + async def async_update(self): + """Update state.""" + state = None + chats = await self.hass.async_add_executor_job(self._teams.get_my_chats) + for chat in chats: + messages = await self.hass.async_add_executor_job( + ft.partial(chat.get_messages, limit=10) + ) + for message in messages: + if not state and message.content != "": + state = message.created_date + self._from_display_name = message.from_display_name + self._content = message.content + self._chat_id = message.chat_id + self._importance = message.importance + self._subject = message.subject + self._summary = message.summary + break + if state: + break + self._state = state diff --git a/custom_components/o365/utils.py b/custom_components/o365/utils.py index c0f8fd1..5a6d787 100644 --- a/custom_components/o365/utils.py +++ b/custom_components/o365/utils.py @@ -17,6 +17,7 @@ from .const import ( CONF_ACCOUNT_NAME, CONF_CAL_ID, + CONF_CHAT_SENSORS, CONF_CONFIG_TYPE, CONF_DEVICE_ID, CONF_EMAIL_SENSORS, @@ -31,9 +32,11 @@ DOMAIN, PERM_CALENDARS_READ, PERM_CALENDARS_READWRITE, + PERM_CHAT_READ, PERM_MAIL_READ, PERM_MAIL_SEND, PERM_MINIMUM_CALENDAR, + PERM_MINIMUM_CHAT, PERM_MINIMUM_MAIL, PERM_MINIMUM_PRESENCE, PERM_MINIMUM_USER, @@ -62,11 +65,14 @@ def build_minimum_permissions(config): email_sensors = config.get(CONF_EMAIL_SENSORS, []) query_sensors = config.get(CONF_QUERY_SENSORS, []) status_sensors = config.get(CONF_STATUS_SENSORS, []) + chat_sensors = config.get(CONF_CHAT_SENSORS, []) minimum_permissions = [PERM_MINIMUM_USER, PERM_MINIMUM_CALENDAR] if len(email_sensors) > 0 or len(query_sensors) > 0: minimum_permissions.append(PERM_MINIMUM_MAIL) if len(status_sensors) > 0: minimum_permissions.append(PERM_MINIMUM_PRESENCE) + if len(chat_sensors) > 0: + minimum_permissions.append(PERM_MINIMUM_CHAT) return minimum_permissions @@ -76,11 +82,9 @@ def build_requested_permissions(config): email_sensors = config.get(CONF_EMAIL_SENSORS, []) query_sensors = config.get(CONF_QUERY_SENSORS, []) status_sensors = config.get(CONF_STATUS_SENSORS, []) + chat_sensors = config.get(CONF_CHAT_SENSORS, []) enable_update = config.get(CONF_ENABLE_UPDATE, True) - scope = [ - PERM_OFFLINE_ACCESS, - PERM_USER_READ, - ] + scope = [PERM_OFFLINE_ACCESS, PERM_USER_READ] if enable_update: scope.extend((PERM_MAIL_SEND, PERM_CALENDARS_READWRITE)) else: @@ -89,6 +93,8 @@ def build_requested_permissions(config): scope.append(PERM_MAIL_READ) if len(status_sensors) > 0: scope.append(PERM_PRESENCE_READ) + if len(chat_sensors) > 0: + scope.append(PERM_CHAT_READ) return scope