Skip to content

Commit

Permalink
Add Chat Sensor
Browse files Browse the repository at this point in the history
  • Loading branch information
RogerSelwyn committed May 19, 2022
1 parent b8a35af commit 0184872
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 50 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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._

Expand Down
56 changes: 19 additions & 37 deletions custom_components/o365/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -101,13 +80,15 @@ 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 = {
CONF_ACCOUNT: account,
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, ""),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand Down
8 changes: 8 additions & 0 deletions custom_components/o365/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 = [
Expand Down
8 changes: 8 additions & 0 deletions custom_components/o365/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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],
}
]
)
Expand Down
86 changes: 82 additions & 4 deletions custom_components/o365/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
):
Expand All @@ -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()
Expand Down Expand Up @@ -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 != "<systemEventMessage/>":
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
Loading

0 comments on commit 0184872

Please sign in to comment.