Skip to content

Commit

Permalink
Merge pull request #177 from RogerSelwyn/todo_entity
Browse files Browse the repository at this point in the history
feat: Implementation of support for Todo entity in HA 2023.11
  • Loading branch information
RogerSelwyn authored Nov 2, 2023
2 parents 1dac9b7 + bb4193c commit a5fc882
Show file tree
Hide file tree
Showing 21 changed files with 608 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import voluptuous as vol
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from ..const import ATTR_DATA, ATTR_STATE, CONF_PERMISSIONS
from ..const import ATTR_DATA, CONF_PERMISSIONS


class O365Sensor(CoordinatorEntity):
class O365Entity(CoordinatorEntity):
"""O365 generic Sensor class."""

_attr_should_poll = False
Expand All @@ -30,11 +30,6 @@ def entity_key(self):
"""Entity Key property."""
return self._entity_id

@property
def native_value(self):
"""Sensor state."""
return self.coordinator.data[self.entity_key][ATTR_STATE]

@property
def unique_id(self):
"""Entity unique id."""
Expand Down
6 changes: 3 additions & 3 deletions custom_components/o365/classes/mailsensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
SENSOR_EMAIL,
)
from ..utils.utils import clean_html, get_email_attributes
from .sensorentity import O365Sensor
from .entity import O365Entity


class O365MailSensor(O365Sensor, SensorEntity):
class O365MailSensor(O365Entity, SensorEntity):
"""O365 generic Mail Sensor class."""

def __init__(self, coordinator, config, sensor_conf, name, entity_id, unique_id):
Expand Down Expand Up @@ -74,7 +74,7 @@ def _get_attributes(self, data):
]


class O365AutoReplySensor(O365Sensor, SensorEntity):
class O365AutoReplySensor(O365Entity, SensorEntity):
"""O365 Auto Reply sensor processing."""

def __init__(self, coordinator, name, entity_id, config, unique_id):
Expand Down
43 changes: 6 additions & 37 deletions custom_components/o365/classes/taskssensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
from homeassistant.const import CONF_ENABLED
from homeassistant.util import dt

from ..const import (
Expand All @@ -18,13 +17,10 @@
ATTR_REMINDER,
ATTR_SUBJECT,
ATTR_TASK_ID,
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,
Expand All @@ -35,20 +31,19 @@
EVENT_UPDATE_TASK,
PERM_MINIMUM_TASKS_WRITE,
PERM_TASKS_READWRITE,
SENSOR_TODO,
TODO_TODO,
)
from ..utils.filemgmt import update_task_list_file
from .sensorentity import O365Sensor
from .entity import O365Entity

_LOGGER = logging.getLogger(__name__)


class O365TasksSensor(O365Sensor, SensorEntity):
class O365TasksSensor(O365Entity, SensorEntity):
"""O365 Tasks sensor processing."""

def __init__(self, coordinator, todo, name, task, config, entity_id, unique_id):
"""Initialise the Tasks Sensor."""
super().__init__(coordinator, config, name, entity_id, SENSOR_TODO, unique_id)
super().__init__(coordinator, config, name, entity_id, TODO_TODO, unique_id)
self.todo = todo
self._show_completed = task.get(CONF_SHOW_COMPLETED)

Expand All @@ -74,8 +69,8 @@ def extra_state_attributes(self):
return self._extra_attributes

def _handle_coordinator_update(self) -> None:
tasks = list(self.coordinator.data[self.entity_key][ATTR_DATA])
self._state = len(tasks)
tasks = self.coordinator.data[self.entity_key][ATTR_DATA]
self._state = sum(not task.completed for task in tasks)
self._extra_attributes = self._update_extra_state_attributes(tasks)

task_last_completed = self._zero_date
Expand Down Expand Up @@ -265,29 +260,3 @@ def build_todo_query(key, todo):
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,
)
10 changes: 8 additions & 2 deletions custom_components/o365/classes/teamssensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ATTR_DATA,
ATTR_FROM_DISPLAY_NAME,
ATTR_IMPORTANCE,
ATTR_STATE,
ATTR_SUBJECT,
ATTR_SUMMARY,
CONF_ACCOUNT,
Expand All @@ -21,12 +22,12 @@
SENSOR_TEAMS_CHAT,
SENSOR_TEAMS_STATUS,
)
from .sensorentity import O365Sensor
from .entity import O365Entity

_LOGGER = logging.getLogger(__name__)


class O365TeamsSensor(O365Sensor):
class O365TeamsSensor(O365Entity):
"""O365 Teams sensor processing."""

def __init__(self, cordinator, name, entity_id, config, entity_type, unique_id):
Expand All @@ -39,6 +40,11 @@ def icon(self):
"""Entity icon."""
return "mdi:microsoft-teams"

@property
def native_value(self):
"""Sensor state."""
return self.coordinator.data[self.entity_key][ATTR_STATE]


class O365TeamsStatusSensor(O365TeamsSensor, SensorEntity):
"""O365 Teams sensor processing."""
Expand Down
5 changes: 3 additions & 2 deletions custom_components/o365/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ class EventResponse(Enum):
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_OFFSET = "!!"
DOMAIN = "o365"
ENTITY_ID_FORMAT_SENSOR = "sensor.{}"
ENTITY_ID_FORMAT_TODO = "todo.{}"

EVENT_HA_EVENT = "ha_event"
EVENT_COMPLETED_TASK = "completed_task"
Expand Down Expand Up @@ -206,11 +208,10 @@ class EventResponse(Enum):
PERM_SHARED = ".Shared"

SENSOR_AUTO_REPLY = "auto_reply"
SENSOR_ENTITY_ID_FORMAT = "sensor.{}"
SENSOR_EMAIL = "inbox"
SENSOR_TEAMS_STATUS = "teams_status"
SENSOR_TEAMS_CHAT = "teams_chat"
SENSOR_TODO = "todo"
TODO_TODO = "todo"
TOKEN_FILENAME = "o365{0}.token" # nosec
TOKEN_FILE_MISSING = "missing"
YAML_CALENDARS = "{0}_calendars{1}.yaml"
Expand Down
53 changes: 35 additions & 18 deletions custom_components/o365/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from datetime import datetime, timedelta

from homeassistant.const import CONF_ENABLED, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.helpers import entity_registry
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,
Expand Down Expand Up @@ -49,17 +49,19 @@
CONF_TODO_SENSORS,
CONF_TRACK,
DOMAIN,
ENTITY_ID_FORMAT_SENSOR,
ENTITY_ID_FORMAT_TODO,
EVENT_HA_EVENT,
LEGACY_ACCOUNT_NAME,
SENSOR_AUTO_REPLY,
SENSOR_EMAIL,
SENSOR_ENTITY_ID_FORMAT,
SENSOR_TEAMS_CHAT,
SENSOR_TEAMS_STATUS,
SENSOR_TODO,
TODO_TODO,
YAML_TASK_LISTS,
)
from .schema import TASK_LIST_SCHEMA
from .todo import O365TodoEntityServices, build_todo_query
from .utils.filemgmt import build_config_file_path, build_yaml_filename, load_yaml_file

_LOGGER = logging.getLogger(__name__)
Expand All @@ -85,6 +87,7 @@ def __init__(self, hass, config):
self._data = {}
self._zero_date = datetime(1, 1, 1, 0, 0, 0, tzinfo=dt.DEFAULT_TIME_ZONE)
self._chat_members = {}
self._ent_reg = entity_registry.async_get(hass)

async def async_setup_entries(self):
"""Do the initial setup of the entities."""
Expand Down Expand Up @@ -119,7 +122,9 @@ async def _async_email_sensors(self):
sensor_conf, CONF_EMAIL_SENSORS
):
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_ENTITY_KEY: self._build_entity_id(
ENTITY_ID_FORMAT_SENSOR, name
),
CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}",
CONF_SENSOR_CONF: sensor_conf,
CONF_O365_MAIL_FOLDER: mail_folder,
Expand All @@ -140,7 +145,9 @@ async def _async_query_sensors(self):
):
name = sensor_conf.get(CONF_NAME)
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_ENTITY_KEY: self._build_entity_id(
ENTITY_ID_FORMAT_SENSOR, name
),
CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}",
CONF_SENSOR_CONF: sensor_conf,
CONF_O365_MAIL_FOLDER: mail_folder,
Expand All @@ -157,7 +164,7 @@ def _status_sensors(self):
for sensor_conf in status_sensors:
name = sensor_conf.get(CONF_NAME)
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_ENTITY_KEY: self._build_entity_id(ENTITY_ID_FORMAT_SENSOR, name),
CONF_UNIQUE_ID: f"{name}_{self._account_name}",
CONF_NAME: name,
CONF_ENTITY_TYPE: SENSOR_TEAMS_STATUS,
Expand All @@ -172,7 +179,7 @@ def _chat_sensors(self):
for sensor_conf in chat_sensors:
name = sensor_conf.get(CONF_NAME)
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_ENTITY_KEY: self._build_entity_id(ENTITY_ID_FORMAT_SENSOR, name),
CONF_UNIQUE_ID: f"{name}_{self._account_name}",
CONF_NAME: name,
CONF_ENTITY_TYPE: SENSOR_TEAMS_CHAT,
Expand All @@ -186,7 +193,7 @@ 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)
sensor_services = O365TodoEntityServices(self.hass)
await sensor_services.async_scan_for_task_lists(None)

yaml_filename = build_yaml_filename(self._config, YAML_TASK_LISTS)
Expand Down Expand Up @@ -221,17 +228,21 @@ async def _async_todo_entities(self, task_lists):
)
)
)

unique_id = f"{task_list_id}_{self._account_name}"
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_UNIQUE_ID: f"{task_list_id}_{self._account_name}",
CONF_ENTITY_KEY: self._build_entity_id(ENTITY_ID_FORMAT_TODO, name),
CONF_UNIQUE_ID: unique_id,
CONF_TODO: todo,
CONF_NAME: name,
CONF_TASK_LIST: tasklist,
CONF_ENTITY_TYPE: SENSOR_TODO,
CONF_ENTITY_TYPE: TODO_TODO,
}

keys.append(new_key)
# To be deleted in mid 2024 after majority have migtated
# to HA 2023.11 and O365 version 4.5
await self._async_delete_redundant_sensors(unique_id)

except HTTPError:
_LOGGER.warning(
"Task list not found for: %s - Please remove from O365_tasks_%s.yaml",
Expand All @@ -246,7 +257,7 @@ async def _async_auto_reply_sensors(self):
for sensor_conf in auto_reply_sensors:
name = sensor_conf.get(CONF_NAME)
new_key = {
CONF_ENTITY_KEY: self._build_entity_id(name),
CONF_ENTITY_KEY: self._build_entity_id(ENTITY_ID_FORMAT_SENSOR, name),
CONF_UNIQUE_ID: f"{name}_{self._account_name}",
CONF_NAME: name,
CONF_ENTITY_TYPE: SENSOR_AUTO_REPLY,
Expand Down Expand Up @@ -295,7 +306,7 @@ async def _async_update_data(self):
entity_type = key[CONF_ENTITY_TYPE]
if entity_type == SENSOR_EMAIL:
await self._async_email_update(key)
elif entity_type == SENSOR_TODO:
elif entity_type == TODO_TODO:
await self._async_todos_update(key)
elif entity_type == SENSOR_TEAMS_CHAT:
await self._async_teams_chat_update(key)
Expand Down Expand Up @@ -326,7 +337,7 @@ async def _async_email_update(self, key):
self._data[entity_key] = {
ATTR_DATA: await self.hass.async_add_executor_job(list, data)
}

async def _async_teams_status_update(self, key):
"""Update state."""
entity_key = key[CONF_ENTITY_KEY]
Expand Down Expand Up @@ -408,7 +419,9 @@ async def _async_todos_update(self, key):
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_DATA] = await self.hass.async_add_executor_job(
list, data
)

self._data[entity_key][ATTR_ERROR] = error

Expand Down Expand Up @@ -446,10 +459,10 @@ async def _async_auto_reply_update(self, key):
ATTR_AUTOREPLIESSETTINGS: data.automaticrepliessettings,
}

def _build_entity_id(self, name):
def _build_entity_id(self, entity_id_format, name):
"""Build and entity ID."""
return async_generate_entity_id(
SENSOR_ENTITY_ID_FORMAT,
entity_id_format,
name,
hass=self.hass,
)
Expand All @@ -460,3 +473,7 @@ def _raise_event(self, event_type, task_id, time_type, task_datetime):
{ATTR_TASK_ID: task_id, time_type: task_datetime, EVENT_HA_EVENT: False},
)
_LOGGER.debug("%s - %s - %s", event_type, task_id, task_datetime)

async def _async_delete_redundant_sensors(self, unique_id):
if entity_id := self._ent_reg.async_get_entity_id("sensor", DOMAIN, unique_id):
self._ent_reg.async_remove(entity_id)
2 changes: 1 addition & 1 deletion custom_components/o365/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"O365==2.0.28",
"BeautifulSoup4>=4.10.0"
],
"version": "v4.4.3"
"version": "v4.5.0b1"
}
Loading

0 comments on commit a5fc882

Please sign in to comment.