Repository: RogerSelwyn/O365-HomeAssistant Branch: master Commit: e1a84431db5a Files: 57 Total size: 336.1 KB Directory structure: gitextract_pg2eyhpl/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── codeql.yml │ ├── hacs.yaml │ ├── hassfest.yaml │ ├── lint.yaml │ ├── o365release.yaml │ └── stale.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── custom_components/ │ └── o365/ │ ├── __init__.py │ ├── calendar.py │ ├── classes/ │ │ ├── __init__.py │ │ ├── entity.py │ │ ├── mailsensor.py │ │ ├── permissions.py │ │ └── teamssensor.py │ ├── const.py │ ├── helpers/ │ │ ├── __init__.py │ │ ├── coordinator.py │ │ ├── migration.py │ │ └── setup.py │ ├── icons.json │ ├── manifest.json │ ├── notify.py │ ├── repairs.py │ ├── schema.py │ ├── sensor.py │ ├── services.yaml │ ├── strings.json │ ├── todo.py │ ├── translations/ │ │ ├── en.json │ │ └── sk.json │ └── utils/ │ ├── __init__.py │ ├── calendar_utils.py │ ├── filemgmt.py │ └── utils.py ├── docs/ │ ├── _config.yml │ ├── authentication.md │ ├── calendar_configuration.md │ ├── calendar_panel.md │ ├── errors.md │ ├── events.md │ ├── index.md │ ├── installation_and_configuration.md │ ├── migration.md │ ├── permissions.md │ ├── prerequisites.md │ ├── sensor.md │ ├── services.md │ ├── todo.md │ ├── todos_configuration.md │ └── token.md ├── hacs.json ├── requirements.txt └── requirements_release.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ custom: ["https://www.buymeacoffee.com/rogtp", "https://www.paypal.com/donate/?hosted_button_id=F7TGHNGH7A526"] ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ "master" ] pull_request: # The branches below must be a subset of the branches above branches: [ "master" ] schedule: - cron: '37 0 * * 3' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/hacs.yaml ================================================ name: HACS Validate on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: hacs: name: HACS Action runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v5" - name: HACS validation uses: "hacs/action@main" with: category: "integration" ================================================ FILE: .github/workflows/hassfest.yaml ================================================ name: Validate with hassfest on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: hassfest: name: hassfest Action runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v5" - uses: home-assistant/actions/hassfest@master ================================================ FILE: .github/workflows/lint.yaml ================================================ name: "Lint" on: push: pull_request: schedule: - cron: "0 0 * * *" jobs: lint: name: Lint runs-on: "ubuntu-latest" steps: - name: "Checkout the repository" uses: actions/checkout@v5 - name: "Set up Python" uses: actions/setup-python@v6 with: python-version: "3.12" cache: "pip" - name: "Install requirements" run: python3 -m pip install -r requirements_release.txt - name: "Run" run: python3 -m ruff check . ================================================ FILE: .github/workflows/o365release.yaml ================================================ name: O365 Release on: release: types: [published] jobs: release_zip_file: name: Prepare release asset runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v5" - name: Release Asset uses: "rogerselwyn/actions/release-asset@main" with: github_token: ${{ secrets.GITHUB_TOKEN }} component: o365 releasenotes: name: Prepare release notes needs: release_zip_file runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v5" - name: Release Notes uses: "rogerselwyn/actions/release-notes@main" with: github_token: ${{ secrets.GITHUB_TOKEN }} branch: master ================================================ FILE: .github/workflows/stale.yaml ================================================ name: Close inactive issues on: schedule: - cron: "30 1 * * *" jobs: close-issues: runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v10 with: days-before-issue-stale: 30 days-before-issue-close: 14 stale-issue-label: "stale" stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." days-before-pr-stale: -1 days-before-pr-close: -1 repo-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ # Created by https://www.gitignore.io/api/python,visualstudiocode # Edit at https://www.gitignore.io/?templates=python,visualstudiocode ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # Mr Developer .mr.developer.cfg .project .pydevproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ### VisualStudioCode ### .vscode/* ### VisualStudioCode Patch ### # Ignore all local history of files .history # End of https://www.gitignore.io/api/python,visualstudiocode .DS_Store ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## v5.3.5 (2025/10/17) ### 🐛 Fixes - [Include oauthlib dependency](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/febda93a41270306c887bf5bfe5e59d271792df1) - @RogerSelwyn ### ⬆️ Dependencies - [bump ruff from 0.12.8 to 0.13.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/84ef2164fdc7468b6ea98bd3cdca5bdfefdca748) - @dependabot[bot] - [bump ruff from 0.13.0 to 0.13.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5c31a77bf4e04276bbd606fe3d4c7b897985baf3) - @dependabot[bot] - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/aa94804e29e3bc944536156b2562528d9f5aa9e0) - @actions-user ### 🔖 Release - [Release v5.3.5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/57e92669d9f06ae5c50a1aa4e53b85ae70219b8e) - @RogerSelwyn ## v5.3.4 (2025/09/10) ### 🐛 Fixes - [Fix error in ToDo migration](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/444b8fa155b59af4b04bc7cb7dd74ca3b0a7eb13) - @RogerSelwyn ### 🔖 Release - [Release v5.3.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/80a75b7e567ebf8f4806022b4df348d0515efb85) - @RogerSelwyn ## v5.3.3 (2025/09/09) ### 🐛 Fixes - [Support migration to MS365 by providing QueryBuilder support](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/10e5999a1f4a33f9581e8574c9061e334cce87dd) - @RogerSelwyn ### ⬆️ Dependencies - [bump ruff from 0.11.4 to 0.12.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0ac28dd269dde7e6333b4b61749f5803c75cf541) - @dependabot[bot] - [bump ruff from 0.12.2 to 0.12.8](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8bf0fb73a70e88896611acd32dea70a9f2629411) - @dependabot[bot] - [bump actions/checkout from 4 to 5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e1e45138ccd0e042fe39986beb8ec1688af2e8e0) - @dependabot[bot] - [bump actions/stale from 9 to 10](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/061023cb9feedfd2649a62d4175481e7d428563e) - @dependabot[bot] - [bump actions/setup-python from 5 to 6](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ad6f1f1cd8e5d08baf519ec4248d29c46fed3c93) - @dependabot[bot] ### 🔖 Release - [Release v5.3.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/91696ce4a56a85458937d3cfb276e410f1a621f7) - @RogerSelwyn ## v5.3.2 (2025/06/09) ### 🐛 Fixes - [Revert part of change in v5.3.1 causing 500 error](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ed0832b517fbbfa9855323a126737bf762c0611d) - @RogerSelwyn ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/895dac826f4c96e039b15719e4aba67c9382a284) - @RogerSelwyn ### 🔖 Release - [Release v5.3.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/35cbb15664f5a7874e7ddcc1b156cc5b29f2bcf8) - @RogerSelwyn ## v5.3.1 (2025/06/09) ### 🐛 Fixes - [Fix calendar colour missing](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/387e0030dea47a435b383c5f906cec6aa3d40f15) - @RogerSelwyn ### 🔖 Release - [Release v5.3.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/90ffae10c8b4effc27b13342a9687fccdf5d5c95) - @RogerSelwyn ## v5.3.0 (2025/06/03) ### 🐛 Fixes - [Implement workaround for MS 500 error response](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/46b7ad0396732e383152c7abc655a5d640859710) - @RogerSelwyn ### ⬆️ Dependencies - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/246be98f3dec7816b9eb9056c4c8479e7a8cfc36) - @actions-user ### 🔖 Release - [Release v5.3.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/dfdcc82be4afd741a4fc9131709f66f60edcdc44) - @RogerSelwyn ## v5.2.2 (2025/05/22) ### 🧰 Maintenance - [Spelling corrections](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/fd7c237b326741288b45e9ddac74c8d8fa6b2901) - @RogerSelwyn - [Unpin MSAL](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2243ac1fce31d53e7d8ca283bda73f91149d870f) - @RogerSelwyn ### ⬆️ Dependencies - [Update ruff to 0.11.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7de7915c0b52b8d8daffc71921022fe222773bb0) - @RogerSelwyn - [Bump python-o365 to 2.1.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ef6df1a3c5eed12ce2a907d9f95a9a8cb9e44ace) - @RogerSelwyn - [Pin python-o365==2.1.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2d2c01239ce63315936702221f2386fb7abb3aa3) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/26836d84fe9c6ecf34b742b722bda37cff750396) - @actions-user ### 📚 Documentation - [Slight tweak to text since it obviously looks different for personal accounts](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/45c669faf5be0f50cb581dca6dd1e94f954efc41) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/41473d84289e386e693c6a31b86d73e75b23f755) - @RogerSelwyn - [Update migration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a20f59ce59e3afa5ce053d0c8a10d65ed8a42bd9) - @RogerSelwyn - [Update migration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b24ee36d486b456c947fca84e2b79ee2eb72acd3) - @RogerSelwyn ### 🔖 Release - [Release v5.2.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/82498e2e0874f2d3ea1f0cb9c1a902f4c59fcc9f) - @RogerSelwyn ### Other - [Update migration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1c90cc8367ecf3103b72f474b5326bb090df6e0) - @GitHubGoody - [build(deps): bump ruff from 0.11.0 to 0.11.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d26d2ad2c10685de5c60e4b49a758b7f018f25f9) - @dependabot[bot] ## v5.2.1 (2025/03/14) ### 🐛 Fixes - [Fix issue with token refresh - pin MSAL to 1.13.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/882db03e2038f421e924f05ca9696e5b8aec6c4e) - @RogerSelwyn ### ⬆️ Dependencies - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/732bc34b7fd68242713af7fbd23b3c2cda996f71) - @actions-user ### 🔖 Release - [Release v5.2.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/de3b8dd6bbb8e13da2cc08934637ecd05144b392) - @RogerSelwyn ## v5.2.0 (2025/03/10) ### 🧰 Maintenance - [Add deprecation warning for Teams](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9d94bee6818a5baf64990c2af16b7584251424e9) - @RogerSelwyn ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6c33735601e955a2ac89cafd79289ccc7ef9c1f9) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/26fa50a8fc10bfab294a2ef2f6680c0dda6b03e9) - @RogerSelwyn ### 🔖 Release - [Release v5.2.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7c154aeba04f01ec510d9a24c8be60e90a0760a0) - @RogerSelwyn ## v5.1.1 (2025/02/27) ### 🐛 Fixes - [Fix tasks not being marked as complete/uncomplete via HA](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/59343f4991543b38ec011cdfdb5e8975334748a8) - @RogerSelwyn ### 🧰 Maintenance - [Workflow ordering](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b4f7ab018a7939a450cfaa4281a054a19943b1c4) - @RogerSelwyn - [Use master branch](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f85142c2927111a8811dc9a7aa8a84381792f066) - @RogerSelwyn ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e1b6783314802e270322624c890839f118ec43ae) - @RogerSelwyn - [Update the docs](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2fc2212e2fd8eca5d4639dc6063b27fd954b677d) - @RogerSelwyn ### 🔖 Release - [Release v5.1.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e92c873929b15eeb4ee51de9f83eceb277b2b797) - @RogerSelwyn ## v5.1.0 (2025/02/18) ### 🧰 Maintenance - [Add deprecation warning for Calendar, Mail and ToDo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d13bd3838a2ebfcaf945395fbf8ed2c04d59b69d) - @RogerSelwyn ### 📚 Documentation - [Update migration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/278e708b895e68744dce92183aabc798647bcb22) - @RogerSelwyn ### 🔖 Release - [Release v5.1.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/dd0710391d0bbf4511af3c8fb069a7dddcfc183f) - @RogerSelwyn ## v5.0.0 (2025/02/17) ### 💥 Breaking Changes - [Update in support of python-o365 change to MSAL](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/81ce1774d62ac588d8e4c74680e8f09dbab7a55c) - @RogerSelwyn ### ⬆️ Dependencies - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ee9d56982feec90601000532fe6de5f551a8aa67) - @actions-user ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d54ee04480f1802e6daa891f892afddc98f96b79) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c3886352a696f61a49abcbecfb2c5571bc8fc347) - @RogerSelwyn ### 🔖 Release - [Release v5.0.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7a92af9669c2102b418f12605605b7d40c96333c) - @RogerSelwyn ## v4.9.0 (2024/11/30) ### ✨ Enhancements - [Add support for migration to MS365 integrations](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8a64b0334c6bcfffe49d77a5c3d21b5bac02b6e4) - @RogerSelwyn ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6fd5fd2fdec8baa5084f46c7129c507bb5b31d08) - @RogerSelwyn ### 🔖 Release - [Release v4.9.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/71c50bd550b8089c72ebed30f550b702706ddf0f) - @RogerSelwyn ## v4.8.7 (2024/11/23) ### 🐛 Fixes - [Ensure all calls to O365 library methods are async](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/bc3e3fddc2e0689e7ec34807777d8ade633402b7) - @RogerSelwyn - [Correct more O365 library call to async](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/40b16b759c52703424d2c1676762e4a1a7f55a7f) - @RogerSelwyn - [Fix typo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4a253b2faa2e57928a27dece7fe0c288cde92bcb) - @RogerSelwyn - [Don't update todo if status is being changed](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6187f92015aed7259a0b80f7e29429801b972231) - @RogerSelwyn - [Fix error in notification send](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2587e0c787b1519df14b1aacb2e1348e02a8385f) - @RogerSelwyn ### 🧰 Maintenance - [Bump HA dependency](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/945e2cc19fae2733d5be927ebcd11cbbe9925fbe) - @RogerSelwyn ### ⬆️ Dependencies - [Bump python-o365 to 2.0.38](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1b6480d7ab00190c95093ad834a9abcd599f7e4) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8620bdef11d046bd724d95c5acb2ed266a6b2f3d) - @actions-user ### 📚 Documentation - [Add show_body config attribute](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ad0adf40e7b9dc7bb0711671ec83789f605a85ea) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1aade71209d1f980dc6a5908196bce6cf6f62802) - @RogerSelwyn - [Update docs](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/98c83c2f1ba5f41e85aa83a10d856b04569beabb) - @RogerSelwyn ### 🔖 Release - [Release v4.8.7](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/46d5c93df441db9fba0c73b0d902516f6cf9ceb1) - @RogerSelwyn ## v4.8.5 (2024/09/04) ### 🐛 Fixes - [Fix issue of o365 library accessing token within the event loop](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/210ef0ac22131a19ff513d9170ad913f992b760e) - @RogerSelwyn ### 🧰 Maintenance - [Bring code into line with MS365 integrations](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/be75195aa51fea09e583176a539644d910c42451) - @RogerSelwyn ### 🔖 Release - [Release v4.8.5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/de9f3b0fe38041e5d195d959a9114c923965d609) - @RogerSelwyn ## v4.8.4 (2024/08/13) ### 🐛 Fixes - [Handle corrupted token file gracefully](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d3021dcb02a9951657d819a169a5802ca1d175fc) - @RogerSelwyn ### 🔖 Release - [Release v4.8.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/af1944bde46911b0a0657edb08ed29905dd94dce) - @RogerSelwyn ## v4.8.3 (2024/08/05) ### 🐛 Fixes - [Capture errors from fetching events so as not to provide empty data attributes](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c94e7c98731a1ff427aa61fc61766a2ce176680c) - @RogerSelwyn ### 🔖 Release - [Release v4.8.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/636f8716ba32542f01a5a9a615a854703843577b) - @RogerSelwyn ## v4.8.2 (2024/07/07) ### 🐛 Fixes - [Fix incorrect Due date shown for To Do](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/11e65fead77edb6f585a1f81f825ba9871499b93) - @RogerSelwyn ### 🧰 Maintenance - [Remove use of internal attribute](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3d17973987985c36dd9ca1a02f3a535c8499dc9d) - @RogerSelwyn - [Remove update of tasks yaml file from the event loop](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4133107efd74844493f0840502a7835f18cd31aa) - @RogerSelwyn ### ⬆️ Dependencies - [Bump O365 to 2.0.36](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/424b93d17e5cf731cb666f2e5d128197ae6d93ab) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3620109d36ca941e57a499fe965b059e162960be) - @actions-user ### 🔖 Release - [Release v4.8.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2b17c173219ef0f531154f0b632ca488c3bfbaef) - @RogerSelwyn ## v4.8.1 (2024/06/06) ### 🧰 Maintenance - [Move writing of calendar file outside the thread](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1279922fa84634187660a676e376ee8e5202f0bc) - @RogerSelwyn - [Remove use of `DEFAULT_TIME_ZONE` constant](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/66d876705e523d88a2d9a12dfe4aba70e966dffa) - @RogerSelwyn ### 📚 Documentation - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/35abd416e0e8bc636b0896e9b71db464c351b91f) - @RogerSelwyn ### 🔖 Release - [Release v4.8.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d9d9e237617ef008fc71c93da2607241f2f840f5) - @RogerSelwyn ## v4.8.0 (2024/05/27) 💥 Breaking Changes (Potentially) **Note** I've decided with this release to remove a whole raft of complex logic around minimum permissions. When I took this on and made permissions more granular I maintained a capability such that if you set the configuration to enable updates, but actually only granted read permissions on the Azure App the integration would still create the sensors. To be honest, this is a pain to maintain and makes the code confusing. Since it is now possible to set the permissions you require to the right level of granularity via the configuration, I've removed this code complexity. I have maintained an ability whereby if the config is set to have Read only permissions, but the Azure app has been granted ReadWrite, then the Read functionality will still work. This enables the situation where the same Azure App is used in multiple configurations with different permission sets. I've tested as thoroughly as I can, butI've set this as Beta for now, so a slow take up picks up any remaining bugs. If you get an error in your logs along the lines of `Minimum required permissions: 'Calendars.ReadWrite'. Not available in token 'o365_primary.token' for account 'primary'`, then check your configuration to ensure it matches what you intended and what your Azure App permissions enable. - [Remove complex logic around minimum permissions](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/aaffd73da7e492bc33d76af57f96fd80f9e18b38) - @RogerSelwyn The below fix may break your setup, the `enable_update` parameter at the top level was incorrectly defaulting to True, when it should be False. If you were relying on the default, you will have to add `enable_update: true` to your config. - [Correct default of calendar enable_update to False](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/64a113696ce2ebecbfdc14a495c6fc89c6b1cfa9) - @RogerSelwyn ### ✨ Enhancements - [Add ability to disable calendar](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/72d66178745267de3417154c4e7a9af07c0db21c) - @RogerSelwyn The rational for adding this enhancement is that the integration is becoming to unwieldy, so I'm moving to a point where I break it out into 3 integrations, calendar, email and other. These could all use the same azure app, but would potentially run from ui based setup (running both yaml and UI is a pain). The first one I would break out is calendar, which is I think how this integration started. So to create a painless transition, there is a need to be able to disable calendar in this integration. ### 🐛 Fixes - [Correct modify calendar schema](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/341e1ac1448adf0b419ebdeda0acbf1eca640180) - @RogerSelwyn - [Correct modify todo service](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/83d6c34cb0f2deebe564f6fa74e1c88fbd45c6c1) - @RogerSelwyn - [Fix error in repair permissions checks](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/190dc7ed36c04df21c433bb6086bbad40dd7fa40) - @RogerSelwyn - [Remove unrequired import](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d23a20158441ee50565ca928399969e030bae6c9) - @RogerSelwyn - [Fix handle failed retrieval of events](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f4ff89497fb7e593a558f81b164a0978b4eda58c) - @RogerSelwyn - [Trapping of connection error](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8b8c06deb3ecc2aa53e5e35f6dd50f11d56fd194) - @RogerSelwyn ### 🧰 Maintenance - [Remove unrequired import](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f6bdc31406d4d6015db38d4daf9a75afc0f848b1) - @RogerSelwyn - [Update requirements_release.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4636c377094671cafee1ac6da878878b06cdd058) - @RogerSelwyn - [Create lint.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4804930c0814b02f9680041aa613c83cd13a1965) - @RogerSelwyn - [Add extra debugging](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1c25de0e9264704260c2195ef47a266626eec818) - @RogerSelwyn - [Capture errors from calendar retrieval](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1cd8ae2153bbc9f84eb61aa38c154cca6092f167) - @RogerSelwyn ### 📚 Documentation - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6b62939face0088fa19f9c5da0f6f62ed407181f) - @RogerSelwyn - [Update installation_and_configuration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/dfa1cc266c1e1422b80f7cc60e4f6fa80525166c) - @RogerSelwyn ### ⬆️ Dependencies - [Bump ruff](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/baf03486f0090d48f154444ef6a3975fab1f74fb) - @RogerSelwyn ### 🔖 Release - [Release v4.8.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6bf79b739349fc2fab17515bfe0317400e0b2081) - @RogerSelwyn ## v4.7.4 (2024/05/13) ### 🐛 Fixes - [Fix broken create/update of events - Changed datetime format](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a1e55b538497dbabe4d777b18e5d7457249dd8a6) - @RogerSelwyn - [Fix error when updating event using o365 service](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/14f23a60493585b740961e2482fc262569613c20) - @RogerSelwyn ### 🧰 Maintenance - [Parallel run setup of sensors and mail](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2b23ee5313331fa9af20630eed93ae67183de2cb) - @RogerSelwyn - [Minor code re-organisation](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4e6fc50459049ce5fe36b9c7ddd0793e461a1161) - @RogerSelwyn - [Move file management calls to be outside the event loop](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3f10991569cdf538372f16891af92ba7edb21d02) - @RogerSelwyn ### 🔖 Release - [Release 4.7.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cf507c3f7cc76a6166a49b20a4764c159178e2a9) - @RogerSelwyn ## v4.7.3 (2024/05/07) ### ✨ Enhancements - [Add support to send Teams messages as HTML contents](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/224) - @pantherale0 ### 🐛 Fixes - [Fix todo creation to correctly accept reminder date/time](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cd1fcc1b9b25bbb828087c9b44ef1b3ef891fa3f) - @RogerSelwyn ### 🔖 Release - [Release v4.7.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c0a3a184d65772173a2ad61e41f1db1c8216b936) - @RogerSelwyn ## v4.7.2 (2024/05/06) ### 🐛 Fixes - [Handle situation where chat member has no display_name](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2cc463d097ed6fc74c5dc8713542b3d9557378d2) - @RogerSelwyn ### 🧰 Maintenance - [Update hacs.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a2dd88024aa8dcabd2a2b24d8b1e80e8f3c53365) - @RogerSelwyn ### 🔖 Release - [Release v4.7.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5e01e9e8a10dce13fe1854f7734dd8aeb0cce6ad) - @RogerSelwyn ## v4.7.1 (2024/03/25) ### 🐛 Fixes - [Enable clearing of ToDo description from HA ToDo panel](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/68660eb213844476dca3cc0fd334ba7aef439dff) - @RogerSelwyn ### 🔖 Release - [Release v4.7.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b10a7afdee132599793f2d122fd2e559d2eb5305) - @RogerSelwyn ## v4.7.0 (2024/03/06) ### ✨ Enhancements - [Add flag status to emails](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/caa14025f05620813a30982141ce09ba92637ede) - @RogerSelwyn - [Add support for updating user Teams status](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/293bc5236c1a1684458cf4ad8f87b740086c919e) - @RogerSelwyn - [Add ability to monitor another user's Teams status](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5dac2f832667173c1b69a328370e99fbb457a0e8) - @RogerSelwyn - [Add support for icons.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/76588df288fd8155fef97fe7775cbd965de3846e) - @RogerSelwyn - [Add set user preferred status service](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/481caad2743554767eef3afa734bd6b79a7fbe52) - @RogerSelwyn - [Add task status as a status attribute](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/429d30425f21b53bb3bbeb0237f7bc9485ad2eab) - @RogerSelwyn ### 🐛 Fixes - [Add missing service description](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a27db7b416df4c896f22e4aec73fc74fdd34234a) - @RogerSelwyn - [Add missing CONFIG_SCHEMA](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4523fbcfd74948727f9c92f7e5e367fdad4a17d7) - @RogerSelwyn - [Fix display of auto-reply state](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/52a40821f636db3208ab6641f63c8ce87dd1f632) - @RogerSelwyn - [Fix hassfest error in icons.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9d528fc94047273e8c14d7523bf37e16b0ef4dfe) - @RogerSelwyn ### 🔨 Maintenance - [Remove `Integration` from name](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b566ce149aa589bf13cc94e1379e9788632d6828) - @RogerSelwyn ### ⬆️ Dependencies - [Bump O365 to 2.0.33](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/335e36d1cbfee43d6ae23a06f7f7c20988513cb9) - @RogerSelwyn - [Bump O365 to 2.0.34](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ad1a625244c705e9922120321797dca09266ee0e) - @actions-user ### 📚 Documentation - [Add documentation for `track_new` for To-Do lists](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f9cc1b8f2dead8306ff372e534b5167fadb1fa3f) - @RogerSelwyn - [Update todo.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1f22bc5a5581bce7f9030d4c8f8cab62d7093f30) - @uSlackr - [Update todo.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/735abbfe73fe0544b5c5834555a835f19b109a6b) - @RogerSelwyn - [Update services documentation for user status setting](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ab554291c38d91fcf7be8f21da2fc9064cddebc1) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ac43e4c3d7eb982a651ab73507aee0d6c7a700ed) - @RogerSelwyn - [Add example for exclude attribute](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b14c4140ece7696e2e814f656f82111221da944d) - @RogerSelwyn ### 🔖 Release - [Release v4.7.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/64dfb15d9d851040ab862146da3886de8c7f1948) - @RogerSelwyn ## v4.6.2 (2024/01/21) ### 🐛 Fixes - [Fix error in sending event for new_todo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/42858754edaa7aa232aa8df90d9e8e05ca813c13) - @RogerSelwyn ### 🔖 Release - [Release v4.6.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d4b621d692e384b34721eeee4e8fcc30b32bcf05) - @RogerSelwyn ## v4.6.1 (2024/01/06) ### 🐛 Fixes - [Fix unique_id error for new email sensor](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8a110221822207b634ff71d222d7cb068a707925) - @RogerSelwyn ### 🔖 Release - [Release v4.6.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/594a0cec04006685e7e016e01288bba111125929) - @RogerSelwyn ## v4.6.0 (2024/01/06) ### 💥 Breaking Changes - [Remove support for deprecated legacy config method](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b57bbe30cb418eb304450c013d17af4a52e72d03) - @RogerSelwyn ### ✨ Enhancements - [Add support for updating events via HA Calendar pane](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c321f976e9ac5c4cb5a9f02881fa4ba30a36cc6d) - @RogerSelwyn ### 🐛 Fixes - [Ensure uniqueness of email sensor unique_id](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c02f9ba9997d482a16e897b3aff96595496dfdbc) - @RogerSelwyn ### 🔨 Maintenance - [Remove redundant code](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d35f39b849b9e275eb9aa5e1226368b4c3c6bb26) - @RogerSelwyn ### 📚 Documentation - [Update docs for removal of legacy config format](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/491f03721ca5e2fbd542cc0817d547fa15908c0e) - @RogerSelwyn ### 🔖 Release - [Release v4.6.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/96d10959aee4affd9a5abea310c7d1069544b195) - @RogerSelwyn ## v4.5.4 (2024/01/04) ### ✨ Enhancements - [Add ability to supress body in email sensors](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e7570b111bea6542d3d82eaf3f235175bc194bf4) - @RogerSelwyn ### 🔨 Maintenance - [Hide pylint abstract-method](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4a1df9baa5ecff4230cfcb07c82b5d714356e898) - @RogerSelwyn ### 🔖 Release - [Release v4.5.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/56e1840488d86648a28ae88b42ca845c33fdf5e1) - @RogerSelwyn ## v4.5.3 (2024/01/03) ### 🐛 Fixes - [Fix non-update of description and due date](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/dda07d4d9a5172c1d7adf4f92de55b985678faf6) - @RogerSelwyn - [Fix non-display of description and due date](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e0d93b25fd3decc7d1e973e922321d6585d9ddbf) - @RogerSelwyn ### 🔖 Release - [Release v4.5.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/15f9b247c25112a21ccd3e1012ec31c515547579) - @RogerSelwyn ## v4.5.2 (2024/01/01) # 💥 Breaking Changes - Requires HA 2023.12 **This release adds support for the new ToDo entity in HA 2023.11. However it also removes the equivalent sensor entity previously created.** **This release adds support for the new ServiceValidationError in HA 2023.12.** - [Task services, service attributes and events renamed to todo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d9a208291963a7e0a03a548b8b3708ae335d7922) - @RogerSelwyn - [Implementation of support for Todo entities released in HA 2023.11](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/177) - @RogerSelwyn - [Use new ServiceValidationError available in 2023.12](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4292e46f54cca6993420f21536f1e49c29f20ac3) - @RogerSelwyn ### ✨ Enhancements - [Add support for due date and description setting in HA ToDo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e624bd5dcc7bcc5e56e3ddf3b318a860acb95c06) - @RogerSelwyn ### 🐛 Fixes - [Enable install onto older versions of HA](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/86b0e81054459e29146668a2b15070176de0f1f8) - @RogerSelwyn - [Fix setup issue v4.4.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0be5cd6028e50788cb0b0bcfeda8662fca62b46b) - @RogerSelwyn - [Fix color attribute handling for group calendars](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4d640d02fc542880ed2f81d1ca1d5a46d79b421f) - @RogerSelwyn ### 🔨 Maintenance - [Delete sensor entities that have been replaced by ToDo entities](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/80ba0f6b5291d6694eefcc76a0949565b2350c25) - @RogerSelwyn - [Custom icon no longer needed for ToDo](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f5db5bc9014316a19a6178b691a4427422e29f3c) - @RogerSelwyn - [Break out email into separate coordinator for performance](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3dc89a0b191718a7fc513acd87bc886dc2b366e3) - @RogerSelwyn - [Show datetime selector for reminder on To Do](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/18af3ce9ffe8f61b667857affcf1a92b4cba47ac) - @RogerSelwyn - [Remove linting errors](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8fa0c923a1a343d82641666818c60e40bff83e4d) - @RogerSelwyn - [Clarify attribute naming for future maintainability](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e0d73eece639034c9646186940d31585bae19368) - @RogerSelwyn - [Add warning to highlight permission differences](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/29fadf061d0bcf3548763f8697c752baf6aee187) - @RogerSelwyn - [Update sk.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2be07eba3d8576bb34490e46c8fd4cf94d2397b2) - @misa1515 - [json file formatting](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0750ea0b222c42d7a098089d6a8ea2889f111b81) - @RogerSelwyn ### 📚 Documentation - [Update todo docs](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/978c9984a7403bca22b4627faa87e65e68569014) - @RogerSelwyn ### 🔖 Release - [Release 4.5.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d2c96f153e3a7031c0420c99d0afc2182665d800) - @RogerSelwyn ## Past Changes
Changes 2023 ## v4.4.4 (2023/11/03) ### 🐛 Fixes - [Fix setup issue v4.4.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4bf6f6774b5b89201b4e81ed79f75909d226414b) - @RogerSelwyn ### 🔖 Release - [Bump to v4.4.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/36b69937c038c88b5a09da7363a35056800043e5) - @RogerSelwyn ## v4.4.3 (2023/11/02) ### 🐛 Fixes - [Fix invalid task handling (as a result of code re-org)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b91b1fd319319bca703614c6de14154ecccba14d) - @RogerSelwyn - [Fix error in mail retrieval (as a result of code re-org)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/73f705494505ab3a8213604623023d2529dc2831) - @RogerSelwyn - [Fix incorrect sensor setup (as a result of code re-org)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/219367856d00407011a9327f484267a8b7b8558a) - @RogerSelwyn ### 🔨 Maintenance - [Mark colors as unrecorded](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/db068bb0f2a2cbcea3475021fbf85a3adeaac21a) - @RogerSelwyn - [Major coordinator re-organisation to enable simpler maintenance](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/175) - @RogerSelwyn - [Add warning indicating corrupt token](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3da9de825dc24fe1a55c52aa63d02d9ecbcc8d9c) - @RogerSelwyn ### 📚 Documentation - [Update installation_and_configuration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/94a32a385735f9daf6fbfff8f91375603ce657a4) - @the-smart-home-maker ### 🔖 Release - [Bump to v4.4.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/90797b381806b692c293cd6138e4dfb6c79d75de) - @RogerSelwyn ## v4.4.2 (2023/10/29) ### ✨ Enhancements - [Add color attributes to calendar](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b2622a29e17576b4ffdc56625adb3d0906af69ad) - @RogerSelwyn ### 🔨 Maintenance - [Reduce attributes stored in recorder](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/bd47a103ccb9c2fe2d9b238317a0ecd34456be79) - @RogerSelwyn - [Imports sorted](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4079894631f1156c925af91c3ba307ca589bffaf) - @RogerSelwyn - [Limit data attribute recording for all sensor entities](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/193b255f44662361ebd125e167f930d893370c87) - @RogerSelwyn - [Remove linting error](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/94004753d4bbdbeb35c0908cafeb48f22335e303) - @RogerSelwyn ### 📚 Documentation - [Update sensor.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/93c08d4c9c7ddc05edf354d83bc63e1242845f84) - @RogerSelwyn ### 🔖 Release - [Bump to v4.4.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/41a31b00523852af37810b5432c4f75a891dc634) - @RogerSelwyn ## v4.4.1 (2023/10/06) ### ✨ Enhancements - [Slovak translation - Thanks!!](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/76cf7030f69e226b03beabafd3c16f25219fd38b) - @misa1515 ### 🔨 Maintenance - [Bump to v4.4.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3ec08ca74fe00c93546af698a381734e178e3ac8) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7faded291068b194a09c104e870f6e3adc7af8bd) - @actions-user ### ⬆️ Depenencies - [Bump python-o365 to 2.0.28](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c8bfc090644ca5216c2a2586f2ec7ea8d69d7417) - @RogerSelwyn - ## v4.4.0 (2023/10/05) ### ✨ Enhancements - [Add basic calendar permission support](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0739e510afc336639544dd52d2eac4aa1d4107e0) - @RogerSelwyn - [Update calendar entity quicker after add/change/delete](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7036b2c08fbc29a61286baae48809c1868fdd4f6) - @RogerSelwyn ### 🐛 Fixes - [Logger fix](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/07d20115f725dd89c38bd430c7053d4079d759b2) - @RogerSelwyn - [Fix issue with deleting events using service](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d39de0390022e105cfd2d3bc59d87757bfe00384) - @RogerSelwyn - [Fix token filename creation](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3169f9d065e517f6faf285f0c891552a0af14422) - @RogerSelwyn - [Fix calendar entity not updating after last event delete](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3b3c1f664d80c72845ec9282db1492b4e42eb8cb) - @RogerSelwyn ### 🔨 Maintenance - [Restructure permission code for maintainability](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2270f0359a8cd396bc8b3b1c66c27b6ad37489ce) - @RogerSelwyn - [Minor code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/04a7c8bc596118e5a4a12b336d4b3994b0afffe2) - @RogerSelwyn - [Remove redundant check for file location](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1c535a0e0e0d25f1b8adf401c4572935dde61731) - @RogerSelwyn - [Further refactoring](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0f6075282d8d8706930b5803d6eccf84c2e35b1d) - @RogerSelwyn - [Update dependabot.yml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/824e6a270b782b095c459005cee70176c5c36425) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/372c0c890e08e2b89e977e02c0e6e286d57e9c9f) - @actions-user - [Pull all permissions methods into Permissions class](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/882eddf229b951d9c096293b959b45b816bf534b) - @RogerSelwyn - [Bump actions/checkout from 2 to 4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e6dc0816067c0f8351a635e3a1787cb7b1ec00a4) - @dependabot[bot] ### 📚 Documentation - [Add clarification on events for external task closure](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/33edd56d84fa6c70f58dc46ff954318073725bf1) - @RogerSelwyn - [Make it clear that Client Secret Value is required, not ID](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/55e7930664493622f44c36e1ec1b44a3bd60bdac) - @RogerSelwyn ## v4.3.4 (2023/08/21) ### Maintenance - [Added extra validation in support of issue 155](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/87ee2d11c2c1d85b97eec369ccf9c7b3a17dcb00) - @RogerSelwyn - [Update __init__.py](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/04e5a5b5cb10849fa91c41e22ff12312a3693fa7) - @RogerSelwyn - [Refine messaging for authentication/token errors](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6a65d11cbb07101e43bcabc6e95286f23fe9b2a9) - @RogerSelwyn - [Bump to v4.3.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9f5a6ec708c8f35dd08a2c5aa7d4c2b93d6013e9) - @RogerSelwyn ### Documentation - [Adjust installation restart steps for 2023.6.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/48e1c5abd72030e1a7739405c0c9fd7933a299fc) - @lunmay - [Added possible teams status, from microsoft graph documentation, fix typo on user permissions table](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cac42f4a355efd01f1675aec7a367d84cec6b406) - @fixtse - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/02d85719fd98ef2794a7de70d2089d4b484c2cf3) - @RogerSelwyn ## v4.3.3 (2023/06/14) ### Enhancements - [Add capture of expired client secret error](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8324414889fb2be3b3c7142705a54d0bbf4ff3cf) - @RogerSelwyn ### Maintenance - [Break up utilities for readability](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2fee043a6d0496f6a3a06f2ed4432c40b61afaa0) - @RogerSelwyn - [Code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/39ecebf34190a300c1639f5bfc624253628d89a6) - @RogerSelwyn - [Code split up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/06f72a17693114bf6ce979428e857dafaefad670) - @RogerSelwyn - [Revert "Code split up"](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ace798c7afb90a868ddac91f88e317a51cfb60cd) - @RogerSelwyn - [Remove redundant utils.py](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9adb83b62b81c04c8551404dd363bdee9891e2ff) - @RogerSelwyn - [Bump to v4.3.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7e7b3a31223c7a13d2db8b05aa989751a9605ed5) - @RogerSelwyn ### Documentation - [Add chat event docs](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7dc9b86306b52bb0150ed543a2632461e34282a8) - @RogerSelwyn - [Add re-authentication info](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d0115eaf92cbdc18db91956ec26ebdc3ff00a89f) - @RogerSelwyn ## v4.3.2 (2023/05/30) ### Enhancements - [Enable filter for upcoming tasks](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9860cf2138b3ebbf78efdec930785074877f5600) - @RogerSelwyn ### Maintenance - [Bump to v4.3.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5b68110108d1b350a23e0e8c082c3d8f97fcf78b) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/724da269980141b5b8bd3e12d9b88ac26ef37e63) - @actions-user ## v4.3.1 (2023/05/30) ### Enhancements - [Add ability to send chat message](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/07c8a848bb73a6968ba36297af20860de2dd6e7c) - @RogerSelwyn ### Fixes - [Spelling correction](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d3cad5c38dde37c3b3978efb31236fa02fba83a8) - @RogerSelwyn ### Maintenance - [Convert strings to constants](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3d77e080406dd805ea9ed34a40b417db5a68a986) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7c6fda46e384721134612b466aad48eb289c3249) - @actions-user ## v4.3.0 (2023/05/22) ### Enhancements - [Add support for shared mailboxes](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/138) - @jgrieger1 / @RogerSelwyn ### Maintenance - [Bump to v4.3.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a34675b599bd2e3f5fd78ca8eccbfddd3e477001) - @RogerSelwyn ### Documentation - [Update documentation for shared mailboxes](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2bf828be64b544f53b44b47fc796a131d17092f8) - @RogerSelwyn - [Update permissions.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f1bb670ee692503a170f4410b0ef2b16bb27d774) - @RogerSelwyn - [Update permissions.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f695a1b898941a5f44f88d669b384e63e972069f) - @RogerSelwyn ## v4.2.12 (2023/05/08) ### Fixes - [Fix error when no events returned to calendar view](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f16328c30c2e869f6c1846e93056f3a957ce5828) - @RogerSelwyn ### Maintenance - [Bump to v4.2.12](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6dd5783d945ebc840fc80e6bc16cf960b9b1145d) - @RogerSelwyn ## v4.2.11 (2023/05/07) ### Fixes - [Fix incorrect creation of auto_reply services](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/243612d77fe02c55166c76fa9d9c523acaf2cfd0) - @RogerSelwyn - [Apply consistent sorting and return date instead of datetime for all day events](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/19b6e7cda822d69f52a45e8932a7bb3b4fbeeefe) - @RogerSelwyn - [(Correct) Update schema.py](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/78bf4ea42a9d0151a72b3d904450f5087a799c5c) - @RogerSelwyn ### Maintenance - [General code improvements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e3f42d17d040c59839cac5631a9721f4b7dfcc4b) - @RogerSelwyn - [More code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/64a29a560da92c2500a8dcb8cde6809ad884200f) - @RogerSelwyn - [Re-organise code](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a6e3e8217d484f093b8f4306cf980ce39fc01bf9) - @RogerSelwyn - [Bump to v4.2.11](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8f773c2df7bc925d4a928f08d7e381844a6e2649) - @RogerSelwyn ### Documentation - [[Typo] Fix for o365_calendars_convetted](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/fd4655ba47330cadd570d837b940d2590c1ac87b) - @rdeveen ## v4.2.10 (2023/04/24) ### Enhancements - [Add service to mark task complete/incomplete](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6b6f1da3f0f022a9b9fed77e624b3d29b3729bc4) - @RogerSelwyn ### Fixes - [Handle errors raised by core CalendarEvent](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f194d4d930eabc74963bfc001635c9053489ce7a) - @RogerSelwyn ### Maintenance - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4d3d14bc472807b567594413f6e8d53b26f5a377) - @RogerSelwyn - [Bump to v4.2.10](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2a9f2d827a03dd9b51d9639d436c2c7cb0fa4e47) - @RogerSelwyn ## v4.2.9 (2023/04/03) ### Enhancements - [Improve performance by reducing retrieved data](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/822c7ab445ec52e2996f73a0b1ed6e86cddacd76) - @RogerSelwyn - [Add permission granularity for ToDo Tasks](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/73cd2609544af6454dc470e8e5ffe150ac677c66) - @RogerSelwyn ### Maintenance - [Minor code improvements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4dccb9ec1302570dab89de4f8e7a7a92409cc56c) - @RogerSelwyn - [Update events.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c2d0a835790861f575b7e7e25db17a40c4cd2a00) - @RogerSelwyn - [Bump to v4.2.9](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/36ec710bccab06c37612df027a0cdd401a5389ba) - @RogerSelwyn ## v4.2.8 (2023/03/18) ### Enhancements - [Migrate authorization to Repair UI](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0c6fc8c4dfe5b84d7f9cf1c43a7be6f8e3acb0fb) - @RogerSelwyn ### Maintenance - [Update docs for installation problems](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6dc8bfc39a01ba91bda48ff47d4538503561fc24) - @RogerSelwyn - [Update prerequisites.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9af30e5ca0065bba47ae4cfe85d6293db6c0dacf) - @RogerSelwyn - [Remove redundant constants](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d59b3345a799e84648bae9cc83740c306e758149) - @RogerSelwyn - [Fix issue with validating token](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c874c1d3aff79bec96072f5a62c63c9054d8907e) - @RogerSelwyn - [Code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f95012fb0f15a03baaea28def97d9f97a4beaa5e) - @RogerSelwyn - [Bump to v4.2.8](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/76f0c8e73dd6019f938cf072b59318e20b77fb8c) - @RogerSelwyn - [Sourcery refactoring](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c20a93d3acf175dd5b1b0ec3cc38583a7d7ede53) - @RogerSelwyn ## v4.2.7 (2023/03/12) ### Fixes - [Fix issue with use of subdirectories in attachments and photos](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/417eb5c4eb8a72a6f0a13beb5404fa680ecd0987) - @RogerSelwyn ### Maintenance - [Bump to 4.2.7](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5524fc8acf8be8f6f2624a8fe48aae6f572e3615) - @RogerSelwyn ## v4.2.6 (2023/03/07) ### Enhancements - [Enable recurrence delete via calendar UI](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/028c2763e245ca7edcabbae858e041cd39e6771b) - @RogerSelwyn ### Maintenance - [Add information on calendar panel](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/af1c4b0598fa2517288a1b3d2d63ca0699e5a1ef) - @RogerSelwyn - [Bump to v4.2.6](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c3fb444afeaab1b965fc8fd5ce4291f51bc68c3a) - @RogerSelwyn ## v4.2.5 (2023/03/07) ### Enhancements - [Add ability to add/delete events via calendar UI](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0d078174809c991cf24ed7412e028fb62e730b85) - @RogerSelwyn ### Maintenance - [Bump to v4.2.5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/216d4dc1f00e628665cbbd2189cc9fc8d0292182) - @RogerSelwyn ## v4.2.4 (2023/02/23) ### Enhancements - [Add option to output email as html](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5981450b1f4a9f13c3e0a32b6ce7c6abdd9e5abc) - @RogerSelwyn ### Maintenance - [Update hassfest.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6cc77d6794b053e7594ce4f7b92f617ef61ea41c) - @RogerSelwyn - [Update hacs.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/58464bca85162b685b483c4a4b51e6df0fee971b) - @RogerSelwyn - [Update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/950cce020d532ce9bfe7a835e3e03865add61ba4) - @RogerSelwyn - [Bump to v4.2.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0a68bdd4b1f6183a47cd38fe6220a5d24b10104b) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/50a4e2f274c67bc7cf73025c6c45eb6e6be16887) - @actions-user ## v4.2.3 (2023/02/19) ### Enhancements - [Add event raising for task/calendar actions. Also add ability to show completed tasks](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/037ec04f22cbf67892e82651d82744d8747a278d) - @RogerSelwyn ### Maintenance - [Bump to v4.2.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0a9aebe0be07172116ecad1f6570e2d1ae84be0e) - @RogerSelwyn ## v4.2.2 (2023/02/09) ### Enhancements - [Add importance as option for send email](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/08da2c0774f98bd6fb7abde10f97d3f523df4423) - @RogerSelwyn ### Maintenance - [Bump to v4.2.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/910942819a14a14d65b33d396dcf852ad0031dfe) - @RogerSelwyn - [Update service details](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/30f2eafd9aac16e0c001b0b90cc8f26a41d24de6) - @RogerSelwyn ## v4.2.1 (2023/02/09) ### Enhancements - [Add regex support to calendar excludes](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7cd1d397ad9b7dd577cec3898df8339c061292df) - @RogerSelwyn ### Maintenance - [Bump to v4.2.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/686e0332259a8fb1ae0609d1ebda65895b88859b) - @RogerSelwyn ## v4.2.0 (2023/02/08) ### Enhancements - [Add ability to exclude events containing string from list](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ebe861eede9da2ffceaf5c0cc702468c1f9f46b8) - @RogerSelwyn ### Maintenance - [Bump o365 from 2.0.25 to 2.0.26](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/21d8e16d99f04368bed641036c69fdd2a2f69896) - @dependabot[bot] - [Bump to 4.2.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/fdc2e46ff27fd8ec09f7415d7ba952311eb82e88) - @RogerSelwyn - [Bump O365 to 2.0.26](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/10b037e8ac54c5371291f49d43b5dffff255968d) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b632dc60711739bf1d978bcaf11df85aeae1fc26) - @actions-user ## v4.1.2 (2023/01/22) ### Fixes - [Fix unique_id for new calendars](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7f76812cb43ae000d32e32981fcbec77d87f74ff) - @RogerSelwyn ### Maintenance - [Bump to v4.1.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d26ba9c4ce6cb8783b29eceb5334ab8d2f6e9b6b) - @RogerSelwyn ## v4.1.1 (2023/01/22) ### Fixes - [Fix for non-unique unique_ids](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2f685bed0af709a157fe1fde0adc4a10bc4b417a) - @RogerSelwyn ### Maintenance - [Update sensor.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a0a28231c747faba123973d147c7e4ced9ae335a) - @RogerSelwyn - [Bump to v4.1.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9e9bedcf0e0db85719909a38d29fdad16fb1973c) - @RogerSelwyn ## v4.1.0 (2023/01/17) ### Enhancements - [Add existing auto reply settings display](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/84132fabac2144d2bed7ce080f91de6c17781a5c) - @RogerSelwyn ### Maintenance - [Bump to v4.1.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2cbbf3edf27437a8adb24c5f2661ef073dcf4e5b) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/63c89fc4054ae35d4f57748b7d9e01511b4a942a) - @actions-user ## v4.0.8 (2023/01/10) ### Enhancements - [Added sensor for targeting auto-reply](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/bdb4e20055fbaafdcf6e04b98e5ab2621c3e67b4) - @RogerSelwyn ### Maintenance - [Update index.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ffe0491077819e92499c616f1483edf637fb97dd) - @RogerSelwyn - [Add documentation for Tasks and new security permissions](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5ab0dae2be4e3a4b50d8f393b050a943f25909fd) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a97c49be9904302db29b9f832c0e3bc3f237f6e8) - @RogerSelwyn - [Added separate permissions page](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3022f20d80cba0aec37ac1fad114d8f3d919b485) - @RogerSelwyn - [Correction](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a7c1d4095e3fb1aab0b406b33991bbf3d5685e04) - @RogerSelwyn - [Update sensor.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/38967f9a0b8d29e039110bb98627d8e4024cd833) - @RogerSelwyn - [Update docs](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3ff2927d13bb908ce9aec5a11611eb19edc9bde8) - @RogerSelwyn - [Bump to v4.0.8](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a5164a6ab20ed932530ce13b3c8d2d63ebe48aca) - @RogerSelwyn
Changes 2022 ## v4.0.7 (2022/12/22) ### Fixes - [Fix notify service name for converted accounts](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cd6b14c6c908ab67e3c7b7aaf4316243ffc10ba7) - @RogerSelwyn ### Maintenance - [Update prerequisites.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e51d714dc4ac4d0a94097f9d0fdd63b23f2d02a5) - @RogerSelwyn - [Update prerequisites.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/faa1f88c6a9314096266b2bc9dba2089f02bb122) - @RogerSelwyn - [Update errors.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/898067cbd375028fd8c24b8e1c78e727cd84c8a4) - @RogerSelwyn - [Bump to v4.0.7](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ca9631e50f300f3de7d2731b36e4a15e0da722c8) - @RogerSelwyn ## v4.0.6 (2022/12/18) ### Fixes - [Fix Todo Entity Names](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/27b81b0bceccd64c356b1cc737b81cb67994e87a) - @RogerSelwyn - [Fix Todo entity name for converted accounts](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/05ecf1a7d8489bba61676789bd90722770e42e06) - @RogerSelwyn ## v4.0.5 (2022/12/18) ### Enhancements - [Add unique_id](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7386123d30a4a81d303f0db9965309d28b8e118d) - @RogerSelwyn ### Maintenance - [Update services.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2a5feb6dcbbf96b4eb8b08a2f41bad29e1c1d4ff) - @RogerSelwyn - [Update index.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4e1ab05be189838177bba63279fdaf544563d69e) - @RogerSelwyn - [Bump to v4.0.5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d2dd978365665862500daf3cfb2a7ffacaedb376) - @RogerSelwyn - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6bbf74c5378cf38dabb8dedc2fde7744248e708a) - @RogerSelwyn - [Correct unique_id](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/316adcb1aa27826fe13cd1582daa4ef8f773a870) - @RogerSelwyn ## v4.0.4 (2022/12/14) ### Enhancements - Add update/delete task services - Add enable/disable auto reply services - Add data co-ordinator to reduce parallel calls to O365 api - Improve quality of service calls. Provide proper inputs via UI. ### Maintenance - Break out sensors into separate class files for simplification. - Change name of module to `Office 365` rather than `Office 365 Calendar`
Full list of changes - [Split out sensors](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ee1a2307612ebff87e7bdeda370be791d3894fa2) - @RogerSelwyn - [Incorporate conflicting changes](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/54cb00217701a70f62e4b890facb5316f42f9b24) - @RogerSelwyn - [Remove blank lines](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cc2573bdab052c891d5d64d1b8d9b03f06d255d0) - @RogerSelwyn - [Refit previous changes](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/acd9ab231d76a1567e26c83d9cb871b8967638e6) - @RogerSelwyn - ['Refactored by Sourcery'](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/adb49403c8b5aa3351fd604fa8b2002c3f6b2532) - @None - [Convert to inclusion as a platform service on Inbox and Query sensor](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c2ea6093ba85597c7bec006927f77358fcded76e) - @RogerSelwyn - [Remove necessary parameter](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/09721ad4a47f6d54d200f202ae88ad86e78b1f88) - @RogerSelwyn - [Change name of module since it it no longer just a calendar](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4a0bf8c1090aca3459bace40a492c4c70027247d) - @RogerSelwyn - [Fix setup of services](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/71611761ef34bcda2c5d78c362d86c4071f814af) - @RogerSelwyn - [Add description](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1f72337aef6b82b14bdbc2137b534b8030f6c6d) - @RogerSelwyn - [Minor tweaks](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e630bbcf9806bdb35c446b9befb83b7a4100825c) - @RogerSelwyn - [Add services for update/delete task](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7f550969697b62e8e576b6391977ad1df74bad47) - @RogerSelwyn - [Extend autoreply capabilities](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ed678c1cedd9d7d06bead73f1a5c0c261399915e) - @RogerSelwyn - [Improve quality of service calls via UI](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1d107b61234868061e35c37288549275a53f5e4e) - @RogerSelwyn - [Bump to v4.0.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b9d4332cdfdaba8095c2f43710650b0c888751c2) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8aa0158478b47dbc9a4fa7da5cf56f0238a7795a) - @actions-user
## v4.0.3 (2022/12/08) ### Enhancements - [Add reminder to tasks, and shorten due to just date](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6d4adc8ac65021afa3361c9a749772fca5caeffe) - @RogerSelwyn ### Maintenance - [Bump to v4.0.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4214142f776aa9d20938a6ff9bdfaa378e9a94be) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d367c573d6ccfe8a1fa0cd1fd8291b0ad797da25) - @actions-user ## v4.0.2 (2022/12/07) ### Fixes - [Fix issue with converted config creation](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/df4c83e4e3f5f4c745cc0b723531501851e4991f) - @RogerSelwyn - [Fix issue with downloading attachments](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5b90d62ea90734de323f050ffe361450ef986464) - @RogerSelwyn ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7379ba32cd1a9f1fb885f151666560a9f71ba983) - @RogerSelwyn - [Update index.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d98c8e017e9a48735a258acf39d36825e22d8ee5) - @RogerSelwyn - [Bump to v4.0.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4671b763d721273159dca8c4058f2ba02a65af26) - @RogerSelwyn ## v4.0.1 (2022/12/03) #### Breaking Changes - `alt_auth_flow` has been removed as a valid configuration parameter. This has been deprecated for 6 months. See [here](https://rogerselwyn.github.io/O365-HomeAssistant/authentication.html) for details of how to configure `alt_auth_method` to meet your needs. - `calendar_id` is no longer supported as a parameter in service calls. `entity_id` should be used instead. Overall the changes to service calls in this release improve validation and should make it clearer when calling the service as to what a problem might be if one occurs. It also significantly simplifies the code which will benefit future changes. - The location of the o365 token and o365_calendar.yaml files have been moved under the `o365_storage` directory. This helps to group the various o365 files in one place. If you are backing up your configuration to a public GitHub, you will need to change your `.gitignore`. ### Deprecations - The Secondary/Legacy method of configuration has been marked as deprecated and will be removed in a future release. See [here](https://rogerselwyn.github.io/O365-HomeAssistant/legacy_migration.html) for more details on how to perform the migration to the Primary method. ### Enhancements - Meaningful icons have been added to all sensors. Thanks to @rdeveen for prompting the change. - Tasks/Todo sensors can be enabled. See [Configuration](https://rogerselwyn.github.io/O365-HomeAssistant/installation_and_configuration.html) for details. ### Fixes - [Fix folder parameter usage](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8b5c7b982aca10c9bcf09a0f0fd6f9523c66ea4e) - @RogerSelwyn - [Fix incorrect service name created after conversion](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/41ccbdeebbe4ed700aa582de3be05cbca25ce22c) - @RogerSelwyn
Full list of changes - [Tidy up file storage location](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6dbf28a5cda2f5d3beef7fa3fc5e3c8e6184ffb9) - @RogerSelwyn - [Rename](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/81da6cc7e4ea9c46cb1d128aa1306710cbf57a61) - @rdeveen - [Add ATTR_ICON](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5bc5afae286608b7b0528c89c60feb55af272edb) - @rdeveen - [Add const ATTR_ICON](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/069f3fb135546b1ba2f77ca90618aa4f3a06b965) - @rdeveen - [Change Icon](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/daa327f377b74a055d3966e8b31fee6fc5536b0b) - @rdeveen - [Use Icon property (#1)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8bd25027b83deb9633bafd386b966ab1db551dbf) - @rdeveen - [Add icons for chats and inbox](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9e6fd7d2e207d60fce8f0e7d8423dc60fb48eaec) - @RogerSelwyn - [Initial draft (awaiting O365 update)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/fd2d642a811f96841953635836af6488a38abffc) - @RogerSelwyn - [Code simplification](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2067cf72e5f1fb91fa7a5fb341481984fcc182bb) - @RogerSelwyn - [Improve service validation and remove deprecated calendar_id](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0abd3a46f2e827a5dacf2147e7c1073f50a1103f) - @RogerSelwyn - [Move schema to schema.py](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ae31ca548646d622316285b03ce13b5c9d9c2525) - @RogerSelwyn - [Change service errors to vol.Invalid](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f477f4d000b6620dd2b4520226b366d94e214b90) - @RogerSelwyn - [Update services.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4be969c9f781668a3418314f68d791a72767d882) - @RogerSelwyn - [Update index.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/596558175b0116c3099ab73c38064585ae46b26c) - @RogerSelwyn - [Update and rename sensor_layout.md to sensor.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f980c3da65eb392c91413b63abb47661fb33c1c7) - @RogerSelwyn - [Update sensor_layout.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0550bdc0d5132a08e00a28bcbf3761980add125d) - @RogerSelwyn - [Update installation_and_configuration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f011f015abd90d23b0f584acb9d1b76f64c21ee7) - @RogerSelwyn - [Update calendar_configuration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0a2b426a880511f1598b220ea49b5306f1eeca34) - @RogerSelwyn - [Rename title to subject in line with O365 module.](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f1c920a964c594d2b110a17e996c94a4c9685e22) - @RogerSelwyn - [Add parallel_updates to restrict number of calls](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0924c2904cf0d4217ea60117b3852809b0403110) - @RogerSelwyn - [Add error catch and bump O365 to 2.0.22](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1ccb274a34f780a6d634def370e20f941a092177) - @RogerSelwyn - [Tweak error message](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/198a4e79d802befb096d7c8d95dea35746dfdd27) - @RogerSelwyn - [Bump to v4.0.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7f2b649f40efcfc1103c36f041dfada24ea59bc8) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/da7f5832ea8d4f9a37a7e94674a02f7784f63f4a) - @actions-user - [Fix event create for group calendars](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5c273a6396e946b810b771ec696c88d2fdd37f60) - @RogerSelwyn - [Change release to v4.0.0 Beta 1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5c5ff38e4290293534bc900eabcc560372751d31) - @RogerSelwyn - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ebd24ad2784adea04f053b44474524f959e729ad) - @RogerSelwyn - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/be6447f909f2c37462a6c7fd8f345099ab51de4e) - @RogerSelwyn - [Make tasks_lists configureable](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1afa7c6141e942a8b15dc01d528f9ee93420a291) - @RogerSelwyn - [Bump to v4.0.0.b2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8d46eafad84d60783a50da0a27f2ffa09e1e002f) - @RogerSelwyn - [Remove deprecated alt_auth_flow config parameter](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a22d6f6f92b2e082630d3ba710273dac89269bd9) - @RogerSelwyn - [Add deprecation warning for secondary configuration.](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ddfd2785ea27a58f0357f2dc6a8a407bbb8c2bd1) - @RogerSelwyn - [Add more support for legacy account migration](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d28efc3e1602b9264f2bc387547b64de6b328dbe) - @RogerSelwyn - [At repair description text](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/890a181b3a9ca6d5ed743c510bb54ec39d23c893) - @RogerSelwyn - [Bump to v4.0.0b3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b12801a612b90f2d940152bdb2950e6f5cc2e267) - @RogerSelwyn - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4c94d8c0f59522f07979f02e707bd992617d5999) - @RogerSelwyn - [Create codeql.yml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cf37942079ea6afc3e1d1d15508c30721dcea9fa) - @RogerSelwyn - [Fix folder parameter usage](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8b5c7b982aca10c9bcf09a0f0fd6f9523c66ea4e) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1eb935d098d66f0b19b6ee1ebc18101afcf6fe82) - @actions-user - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d054a4671e64e41b9bcb352caade9dec5a0e2e61) - @RogerSelwyn - [Fix incorrect service name created after conversion](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/41ccbdeebbe4ed700aa582de3be05cbca25ce22c) - @RogerSelwyn - [Bump to v4.0.0b5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/442fcef341149558a862fae5e1127bef34002696) - @RogerSelwyn - [Bump to v4.0.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4dacdd9aeca9c01990dca2dc56a5c16f3ae96e34) - @RogerSelwyn
## v3.3.0 (2022/11/10) ### Enhancements - [Add ability to read group calendars](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6ba872fea5478bc0727abf559585ca6950039e3d) - @RogerSelwyn ### Maintenance - [Bump to v3.3.0 Alpha 1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4e9828cc53080da96f247b363da85c53af734147) - @RogerSelwyn - [Sourcery code recommendations](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f3d5391c522932a746c4e2f4db23e5b5c7338a05) - @RogerSelwyn - [Sourcery code improvements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1be5c9ddec9572d79dd2f80740f360ff27e37863) - @RogerSelwyn - [Bump to v3.3.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b25edce41358d3ca2b6dce0aba21d4e8937ee6de) - @RogerSelwyn ## v3.2.3 (2022/11/05) ### Enhancements - [Add ability to send for delegated user](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b9c18bc4918415085f092089321cfae76d2bf501) - @RogerSelwyn ### Maintenance - [Update installation_and_configuration.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/53457035eec5bf0b586d17ff7ea934875ab57f34) - @RogerSelwyn - [Update errors.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/52b12fc9a2f6433d056535285cbf6987ad2c8fa0) - @PuffinRub - [Bump to v.3.2.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/465d2143843e8a5ee9da7ba202f879c9781cf96c) - @RogerSelwyn ## v3.2.2 (2022/09/26) ### Enhancements - [Moved documentation to GitHub page](https://rogerselwyn.github.io/O365-HomeAssistant/) - @RogerSelwyn - [Return line breaks where available](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8f445edfaf7edec48e887ca5f44d730e6525b133) - @RogerSelwyn ### Maintenance - [Make account type not optional](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e8135254ec6cc0499fcf18c7a2dc9e52268173df) - @spookyuser - [Bump o365 to 2.0.20](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/63569ea4ed7a4481678da6a7f70e50e56b8e4400) - @RogerSelwyn - [Code cleanup](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1c9086554daf5177094271164974e1822d22c753) - @RogerSelwyn - [Bump o365 module to 2.0.21](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/174b5e2501055188d7ff09414b63a9d71a311c70) - @RogerSelwyn - [Bump to v3.2.2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/168d981c2a8dcf59ad536abc16702592508a2ff1) - @RogerSelwyn ## v3.2.1 (2022/05/26) ### Enhancements - [Add filtering on body](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b593646ca2c073929cdc7025fd59391df149ea4c) - @RogerSelwyn ### Maintenance - [Remove unnecessary BCC](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/57b2839a94f16085fa13094f37d7f1c9f0d2f1d1) - @RogerSelwyn - [Update readme](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/57) - @GitHubGoody - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3bd203e9bce7f0da3f70bdb9401eb9ce468a4b88) - @RogerSelwyn - [Remove domains key from hacs.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2066cb3b60a40ec9aa181b5404c5b927ec4ab33d) - @RogerSelwyn - [Bump o365 to 2.0.19](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/880950c2228a11730700a268fc648fa48a972385) - @RogerSelwyn - [Bump to v3.2.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4541f66e4dfa81acce7be8f5d906ecaf4f291804) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9065037f974f1bb09e6368261fbe4223a7765da0) - @actions-user ## v3.2.0 (2022/05/19) ### Breaking change - [Change default auth method](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/efb3cf2b00d2d32df53691e3c82eb87d077ec814) - @RogerSelwyn ### Enhancements - [Add Chat Sensor](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0184872e15b357489edfc56e1d11520fd187ba50) - @RogerSelwyn ### Maintenance - [Update authentication info](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7c3c1e0aabb8f709925aa99357e2bba5a3f07e25) - @RogerSelwyn - [Add deprecation warning and change alt_auth config parameter](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7feef161c715b9799a39be18165c90a392b54c85) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4d69ccd0467470fab0e6ccc5cc42626ee3345faa) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/218443692e8762d25ab7a26723159ce632019cf6) - @RogerSelwyn - [Create stale.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3a93714c918658850840d0e16b3e989c9150e7dc) - @RogerSelwyn - [Bump to v3.2.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/62091cb924abbcafd283ca7a5aad47d4e7e5c790) - @RogerSelwyn ## v3.1.1 (2022/05/06) ### Fixes - [Fix error on device update](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d5ed3cc98e95cdebfae334cb44b7009a5463f09e) - @RogerSelwyn ### Maintenance - [Bump to v3.1.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9caac0960f198f4c24c75fac5d94f6b96cc8ffda) - @RogerSelwyn ## v3.1.0 (2022/05/05) ### Enhancements - [Move setup_platform to async](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/dc8974b83858b7f4e401e0e0e36b6c0c15c3bf99) - @RogerSelwyn - [Move calls to o365 async](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f9dfedd42f70c03f49760ba6fba8adcce4fd2cdc) - @RogerSelwyn ### Fixes - [Fix issue with photo embedding](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d0eff81b6d6fa39246ed37113a300b4a88288a73) - @RogerSelwyn ### Maintenance - [Use CalendarEntity instead of CalendarEventDevice](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/712daf59e92436664f75e4a3a053f408243abb16) - @RogerSelwyn - [Rename device to entity](https://github.com/RogerSelwyn/O365-HomeAssistant/cpmmit/fcab07dc520e9e09c876ea3c6e1ecc81b83ea67b) - @RogerSelwyn - [Bump to v3.1.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0d1032c8855ed439c28e32f3c8267bbf5b0badf6) - @RogerSelwyn - [Sourcery recommended code change](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3f086fe7184276698d0e9d685a99b81debba89fd) - @RogerSelwyn ## v3.0.1 (2022/05/02) ### Fixes - [Fix photo embedding](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2b26133711c3f87e614b13c66799f9a7ff164f0c) - @RogerSelwyn ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b5fb1f35dad975d3a939b4ec71a8c2e436d3c279) - @RogerSelwyn - [Updated README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/912272b3c276a71b2944f728d4d18325864a589b) - @GitHubGoody - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7566dc035955c087a7e23f9ca69a20698fc288d8) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e95504f2bc74abbdb4ae62b0ac6e460e0ec6b01f) - @RogerSelwyn - [Create FUNDING.yml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4b7b9038b8aaacd76d27a403d3c79f09f08876af) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3962a234aa2800e412af63df496f48f264803846) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8e78e2423f199f75abdacbc137c95629e301d032) - @actions-user ## v3.0.0 (2022/04/17) ### Enhancements - Support for multiple accounts - Reduced permissions requirements for multiple accounts style config - Enable use of Entity_ID instead of Calendar_ID for service calls (mandatory for multi-account) - Complete list of changes - [#26](https://github.com/RogerSelwyn/O365-HomeAssistant/pull/26) ## v2.4.1 (2022/03/30) ### Fixes - [Fix validation of service data and improve attachment handling](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/adbe61de8e8bd0b6b4f91fc8f400d519e8072bdd) - @RogerSelwyn - [Fix for breaking change in HA](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4d616e96e8f6c59a8a208e07641261dbac736fdb) - @RogerSelwyn - [Correct handling for DST](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f7bcf29ec621b7fbc474f7d6663f8e633218ba8c) - @RogerSelwyn ### Maintenance - [Sourcery code improvements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3983c3ab9a84f52a4213922f625ed66309a1d568) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d98a34d248faa04c1615924b5324d4efe1bed243) - @RogerSelwyn - [Bump to v2.4.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8e1f585def6ee1ab433e69438d47e69ed79eb511) - @RogerSelwyn ## v2.4.0 (2022/03/07) **Note:** This release has a radical change to the permissions structure to reduce the scope of the permissions requested. To further reduce the permissions please set 'enables_update' to False in your configuration. This will disable the various update services and remove the request for write access to calendars and send access to mail. ### Enhancements - [Initial change to permissions](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0d3eeb064745b0d70a75893ea31b2cbf76200500) - @RogerSelwyn - [Add 'enable_update' switch so update capability can be disabled.](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8ceccffec922caf95fdb82ee2d94813e4118b6e8) - @RogerSelwyn ### Fixes - [Remove extraneous error](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f0adb5067c6fde20c6d4b750f6c9018685695d2f) - @RogerSelwyn ### Maintenance - [Move more of calendar and sensor to async](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/becf8454a60aab3cc50a3c38ff92715a064f3263) - @RogerSelwyn - [Remove the already deprecated YAML Calendar configuration](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6cb631078b628e6bf4dd0b67adb2232ef6430f39) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1bbc3e4627d66a4b62a147b0a7a65a702d41d5c2) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1832b3094a7064e1652df34a1042dbb752210e5b) - @RogerSelwyn - [Code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4ffaf6779057b0a694a00e89467c679a48128cfa) - @RogerSelwyn - [Bump to v2.4.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/9d2cb2744daa06798a1a846497bef91f60851f5b) - @RogerSelwyn ## v2.3.1 (2022/02/12) ### Enhancements - [Add ability to stop dowloading of attachments (to increase performance)](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/37ad1b7cb6b6aeb1de0b7a17216719311a9b5407) - @RogerSelwyn ### Maintenance - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/28c7da2c7d2e2de5bd57e8219592e2f4f6eb4bce) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/55360e0c2a559501304c5c8fe83b4a58a25def30) - @RogerSelwyn - [Minor code tidy up](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a14564d46e8c6ce70e1f4f2f6c0acc0dafd8dd34) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8d75bc665389feb4f9ab55ea664f374853974e0c) - @RogerSelwyn - [Bump to v2.3.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/32eb8d0140bf5763d365ec36a45ee02f09a77b73) - @RogerSelwyn ## v2.3.0 (2022/02/11) ### Enhancements - [Add Teams Presence Sensor](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a92a0a66cc140bca33fcbc81593e52579d1efa21) - @RogerSelwyn ### Fixes - [Fix storing of o365_calendars.yaml to store/retrieve from config directory](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7ccc053beb6b0aed88d4005570ec7ff2e2241e35) - @RogerSelwyn - [Fix storing of token in the config directory](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/42a2f3fa38d433b00d8271ce1eef8ed434ea2d3a) - @RogerSelwyn ### Maintenance - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/323b3396c61817f4f354c938a068c1d72ebc0bb1) - @RogerSelwyn - [Bump O365 to 2.0.18.1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/57dc3ded8231cd0243f5c9757d74af4746681efb) - @RogerSelwyn - [Code tidy up to remove redundant code](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1e95e1dc00dcfa6a3ddbb936c7748adb4418b240) - @RogerSelwyn - [Pylint code improvements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4e5f2f7a19cc193a448a7e8c3ffa40d8d35964d0) - @RogerSelwyn - [Code simplification from sourcery](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5734711729d017a08e19f86b120049314d3e99d0) - @RogerSelwyn - [Bump to v2.3.0](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3fd836647092e0d1f1c8de5e8508dda71e345326) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/55a592bfad9dba11b3ed0caaf456a8d8842d5015) - @actions-user ## v2.2.8 (2022/02/02) ### Maintenance - [Bump o365 from 2.0.16 to 2.0.17](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/66f669314668130d78c81568f5edad520c50b456) - @dependabot[bot] - [Update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d465d61c1bb3aef551f35b1acbf262668389871d) - @RogerSelwyn - [Bump to v2.2.9](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b236ab93c2594f5af6f8cb1e88019af0b071dec3) - @RogerSelwyn ## v2.2.8 (2022/01/19) ### Enhancements - [Add importance as query filter](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/cf1055bca377c2dc42fd5337693b88fc305a4b0e) - @RogerSelwyn ### Fixes - [Fix issue with no events retrieved if none in next 24h](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/80e99833d7aada408911c7ac7d878530f62a83a4) - @RogerSelwyn - [#13](https://github.com/RogerSelwyn/O365-HomeAssistant/issues/13) - [Fix error with filter not including receivedDateTime](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/26fcb30be5be22c84f66b7e8297ae66776eeacbe) - @RogerSelwyn ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ca2b3190c6fd80c904a7129c530471f0709a768a) - @RogerSelwyn - [Documenation clarifications](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/aab090a53b65e01cde91aebd01dfe7c406d5587a) - @uSlackr - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4cfb23620b7466dd38278496c8bd1d705b0466a5) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d1d231192ea7f0a870be8730afbf38ef58c20dcc) - @RogerSelwyn - [Bump to 2.2.8 Beta 1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c5cef0214a8db234bdfc0f0a193f3fa9a277f775) - @RogerSelwyn - [Revert "Bump to 2.2.8 Beta 1"](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/19829afe5bdfcc441e775fb334115f4eb90a3439) - @RogerSelwyn - [Bump to 2.2.8 Beta 1](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/beafec4c46923a1aa290aed5a02374bd41d567aa) - @RogerSelwyn - [Bump to 2.2.8 Beta 2](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/726ac8c03dac1e49beb0cd7869b506c69f07cc5d) - @RogerSelwyn - [Simplify Code](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/14a1f1945a2147d2e1633a785f0d761c323ac578) - @RogerSelwyn - [Remove duplicate code](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b917935c6f572f43b2045ff3cff30bcc3514a104) - @RogerSelwyn - [Bump to 2.2.8 Beta 3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1c3a4f3e034f6a554f4fd032933db7deab8e6307) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c8d28ff0da03651e7d2ce964a235b3467f6a70a1) - @actions-user
Changes 2021 ## v2.2.7 (2021/12/12) ### Fixes - [Fix device_state_attributes warning](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/4ba056b01c8dcc824cd39256d0751009ce49740a) - @RogerSelwyn ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/25a9e248362042daa0fad8541ea77f17c82f9a59) - @RogerSelwyn - [Bump to 2.2.7](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/674d398e5af1c92764e270c15e6a28d085c9715a) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/296fc51bad89cc0f57ad32c04e0f9bcbd91d8530) - @actions-user ## v2.2.6 (2021/09/28) ### Fixes - [Fix incorrect handling of all days events](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/a8d02bf63e727f27e32ac88f3295e4ba2df3643c) - @RogerSelwyn - [#6](https://github.com/RogerSelwyn/O365-HomeAssistant/issues/6) ### Maintenance - [Remove unrequired iot_class](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d56fcf68832979a82df7de4de732f417c438f409) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/1ea49358593dddf588c261075ddb8365a58c8e20) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/03a9de89f00a1718840c1b9df397d10b4609b686) - @actions-user - [Handle beta releases](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/24202179acaba12bfd49423e286717aaf5830e98) - @RogerSelwyn - [Update to use rogerselwyn/actions](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ae2b49790fbc462e2562b00d15408515190ef411) - @RogerSelwyn - [Correct step name in release](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ddd085f8f8db38b84c7c42ad8b7d6598afd65bed) - @RogerSelwyn - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1d575622660e866a2a743c9e08426d9c82be469) - @actions-user ## v2.2.5 (2021/09/13) ### Fixes - [Prefer external url for authentication over internal](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/5a949bb78b20c80e69994537c9774b614f2e3e09) - @RogerSelwyn - [#5](https://github.com/RogerSelwyn/O365-HomeAssistant/issues/5) ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6c1f569be2d846748c9ec7039704b0537462555f) - @RogerSelwyn - [Bump o365 from 2.0.15 to 2.0.16](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/01f4124e4f2a6b99d24cfdead8f3f3376e0da8cc) - @dependabot[bot] - [Bump to 2.2.5](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1957443ade157614a84492e953905e718d318a7) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/76c1fc94b654c02e5bd7eef02d72750c308bf8c1) - @actions-user ## v2.2.4 (2021/09/12) ### Maintenance - [Update for recommendations by sourcery](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/475cc4de0a5624dc9e320afaca44c16e6b184663) - @RogerSelwyn - [Code recommendations from codefactor](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d290dadfe7e67e53bb3bb15a7e04d3b892168f31) - @RogerSelwyn - [Bump to 2.2.4](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7315271fed296d60c67f51848aac808c0167181c) - @RogerSelwyn ## v2.2.3 (2021/09/10) ### Maintenance - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2aaad7bcb86ddd5c700c59b171c2866f6e6b7c3b) - @RogerSelwyn - [Create dependabot.yml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/601fce70844b150ea5c8bf1d1f654a8e35e52e99) - @RogerSelwyn - [Correct dependency versions](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/48d0634123cb30ad878b8c2565af911760e964ec) - @RogerSelwyn - [Bump to v2.2.3](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/792872bdf9db8adf8fb9028f83d10068a6871cfa) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/973c13e500a55d6d7b11856f3c2cb85dd376e1bc) - @actions-user ## v2.2.2 (2021/09/09) ### Maintenance - [Change code owner](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/c31333fd0b9fd8115f92a776e025b9d78bc5b07b) - @RogerSelwyn - [Update update_version.py](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e47746b0055a11870b4bd106681cf3c201d9451c) - @RogerSelwyn - [Update CHANGELOG.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/6326d079b97c52f91d60f9de2a6b96894a56402c) - @RogerSelwyn - [Correct hacs.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/61edff5bcb2a32463d8c3d32ae5bd7791ced912a) - @RogerSelwyn ## v2.2.1 (2021/09/07) ### Fixes - [Fix issue with authentication I/O within the event loop](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/2dfa7859c1a29f775965b3d73b323093bb8848db) - @RogerSelwyn ### Maintenance - [Deconstrain requirements](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3a546ec608eb07bfdca22645481911ad41a8b43b) - @RogerSelwyn - [Correct version](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7631c0a4a64f55d198a4b0cda1ba7db2bad766d8) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/f4690f8a133c652cdaa15f95c62f6d15bf7d4c03) - @actions-user ## v2.2.0 (2021/09/06) ### Maintenance - [Updated to remove deprecation warning on base_url use](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/afb9e98203c405cfdb41b98c3bb46aedc946279f) - @RogerSelwyn - [Fix for all day_event](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/debf019e085d445fae1749f31c872592bd32ad7d) - @PTST - [Now actually implements the offsets](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3fc4c9af68435156e77e03cf0aaf97bfbd2d20a8) - @PTST - [Black formatting](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d61a26d607ff080b880d02854de4476cbeec9587) - @PTST - [Update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/619c733475e5814048f2c76edf7d39d1fcbaab08) - @RogerSelwyn - [Create o365release.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b1182ff33b58a1c02217cc43bc75fedaf0314f70) - @RogerSelwyn - [Create pushpull.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0a8579b265c123c0dbaac5351561ba90d0ec68f4) - @RogerSelwyn - [Create pushpull.yaml](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ecd897fb82385776f516a9d7a725303f1932e13d) - @RogerSelwyn - [Move](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/ab2ed7b96927b54da4ea3f9871c643b9a8205680) - @RogerSelwyn - [Update .gitignore](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/881f10baf9e727cf719ab26814d80736204b52b5) - @RogerSelwyn - [Add management components](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/0087c426a2613ad037a3ac078e5776cf2b8ece55) - @RogerSelwyn - [Update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/b53202150473153c6f94ad833784fe14bfa03e01) - @RogerSelwyn - [Hassfest corrections](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/7a3afd87b4d103377c573e3f9b399d671fc3c432) - @RogerSelwyn - [Auto update requirements.txt](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/8a1633a88ea1ba803b8cddc265dad6a561c51f17) - @actions-user - [Auto update manifest.json](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/e0421b126d600503a97174e511078ddf804c3331) - @actions-user - [Split workflows](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/14ab44459088a7c76d29f7a11e9fbb3858128256) - @RogerSelwyn - [Update README.md](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/262b82f9648cb705eefd37b9ccbab7edbed77b02) - @RogerSelwyn
Earlier ### Maintenance - [Updated to remove deprecation warning on base_url use](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/afb9e98203c405cfdb41b98c3bb46aedc946279f) - @RogerSelwyn - [Fix for all day_event](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/debf019e085d445fae1749f31c872592bd32ad7d) - @PTST - [Now actually implements the offsets](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/3fc4c9af68435156e77e03cf0aaf97bfbd2d20a8) - @PTST - [Black formatting](https://github.com/RogerSelwyn/O365-HomeAssistant/commit/d61a26d607ff080b880d02854de4476cbeec9587) - @PTST
================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Patrick Toft Steffensen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Validate with hassfest](https://github.com/RogerSelwyn/O365-HomeAssistant/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/RogerSelwyn/O365-HomeAssistant/actions/workflows/hassfest.yaml) [![HACS Validate](https://github.com/RogerSelwyn/O365-HomeAssistant/actions/workflows/hacs.yaml/badge.svg)](https://github.com/RogerSelwyn/O365-HomeAssistant/actions/workflows/hacs.yaml) [![CodeFactor](https://www.codefactor.io/repository/github/rogerselwyn/o365-homeassistant/badge)](https://www.codefactor.io/repository/github/rogerselwyn/o365-homeassistant) [![Downloads for latest release](https://img.shields.io/github/downloads/RogerSelwyn/O365-HomeAssistant/latest/total.svg)](https://github.com/RogerSelwyn/O365-HomeAssistant/releases/latest) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/RogerSelwyn/O365-HomeAssistant/total?label=downloads%40all) [![GitHub release](https://img.shields.io/github/v/release/RogerSelwyn/O365-HomeAssistant)](https://github.com/RogerSelwyn/O365-HomeAssistant/releases/latest) [![maintained](https://img.shields.io/maintenance/no/2025.svg)](#) [![maintainer](https://img.shields.io/badge/maintainer-%20%40RogerSelwyn-blue.svg)](https://github.com/RogerSelwyn) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) [![Community Forum](https://img.shields.io/badge/community-forum-brightgreen.svg)](https://community.home-assistant.io/t/office-365-calendar-access) # Deprecation Notice: The Calendar, Mail Notification, Teams and To Do entities and services are now deprecated in the O365 integration. Development on O365 has now stopped unless a change is required to support migration to the MS365 integrations. Details on how to migrate to the new MS365 integrations can be found here - [Migration](https://rogerselwyn.github.io/O365-HomeAssistant/migration.html) # Note: Support for this ended on 5th November with the release of 2025.11. I have already created integrations which will replace it - MS365-Calendar/Mail/Teams/ToDo, these can now be found on HACS. Many will have seen a warning in the logs - `Detected that custom integration 'o365' uses 'async_config_entry_first_refresh', which is only supported for coordinators with a config entry and will stop working in Home Assistant 2025.11` - this further forces the EOL due to a change in HA Core, which means that the integration will cease to work after the 2025.11 HA release without really significant re-work. Because this integration is setup via YAML, it does not have a `config_entry`, the replacements listed above do. [O365 --> MS365 - A potential big change - your views needed](https://github.com/RogerSelwyn/O365-HomeAssistant/discussions/234) # Office 365 Integration for Home Assistant *This is a fork of the original integration by @PTST which has now been archived.* This integration enables: 1. Getting and creating calendar events 1. Getting emails from your inbox using one of two available sensor types (e-mail and query) 1. Sending emails via the notify.o365_email service 1. Getting presence from Teams (not for personal accounts) 1. Getting the latest chat message from Teams (not for personal accounts) 1. Getting and creating To-Do tasks 1. Setting Auto Reply/Out of Office response This project would not be possible without the wonderful [python-o365 project](https://github.com/O365/python-o365). ## [Buy Me A Beer 🍻](https://buymeacoffee.com/rogtp) I work on this integration because I like things to work well for myself and others. Whilst I have now made significant changes to the integration, it would not be as it stands today without the major work to create it put in by @PTST. Please don't feel you are obligated to donate, but of course it is appreciated. Buy Me A Coffee Donate with PayPal # Documentation The full documentation is available here - [O365 Documentation](https://rogerselwyn.github.io/O365-HomeAssistant/) Nice video from fixtse showing how to install the O365 integration to Home Assistant. Also providing some Lovelace cards for displaying content from O365 - [O365 Card for Home Assistant](https://github.com/fixtse/o365-card) # Migration Details on how to migration to the new MS365 integrations can be found here - [Migration](https://rogerselwyn.github.io/O365-HomeAssistant/migration.html) ================================================ FILE: custom_components/o365/__init__.py ================================================ """Main initialisation code.""" import json import logging import voluptuous as vol import yaml from homeassistant.const import CONF_ENABLED from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from O365 import Account, FileSystemTokenBackend from oauthlib.oauth2.rfc6749.errors import InvalidClientError from .classes.permissions import Permissions from .const import ( CONF_ACCOUNT, CONF_ACCOUNT_CONF, CONF_ACCOUNT_NAME, CONF_ACCOUNTS, CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_CONFIG_TYPE, CONF_FAILED_PERMISSIONS, CONF_GROUPS, CONF_SHARED_MAILBOX, CONF_STATUS_SENSORS, CONF_TODO_SENSORS, CONST_CONFIG_TYPE_LIST, CONST_PRIMARY, CONST_UTC_TIMEZONE, DOMAIN, TOKEN_FILE_CORRUPTED, TOKEN_FILE_MISSING, TOKEN_FILE_OUTDATED, ) from .helpers.migration import MigrationServices from .helpers.setup import do_setup from .schema import MULTI_ACCOUNT_SCHEMA CONFIG_SCHEMA = vol.Schema({DOMAIN: MULTI_ACCOUNT_SCHEMA}, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the O365 platform.""" _LOGGER.debug("Startup") conf = config.get(DOMAIN, {}) accounts = MULTI_ACCOUNT_SCHEMA(conf)[CONF_ACCOUNTS] conf_type = CONST_CONFIG_TYPE_LIST for account in accounts: await _async_setup_account(hass, account, conf_type) await _async_setup_migration_service(hass, conf) _LOGGER.debug("Finish") return True async def _async_setup_account(hass, account_conf, conf_type): credentials = ( account_conf.get(CONF_CLIENT_ID), account_conf.get(CONF_CLIENT_SECRET), ) account_name = account_conf.get(CONF_ACCOUNT_NAME, CONST_PRIMARY) main_resource = account_conf.get(CONF_SHARED_MAILBOX) _LOGGER.debug("Validate shared") if not _validate_shared_schema(account_name, main_resource, account_conf): return _LOGGER.debug("Permissions setup") perms = Permissions(hass, account_conf, conf_type) permissions, failed_permissions = await perms.async_check_authorizations() account, is_authenticated = await hass.async_add_executor_job( _try_authentication, perms, credentials, main_resource ) if is_authenticated and permissions is True: _LOGGER.debug("do setup") check_token = await _async_check_token(hass, account, account_name) if check_token: await do_setup( hass, account_conf, account, is_authenticated, account_name, conf_type, perms, ) else: await _async_authorization_repair( hass, account_conf, account, account_name, conf_type, failed_permissions, permissions, ) def _try_authentication(perms, credentials, main_resource): _LOGGER.debug("Setup token") token_backend = FileSystemTokenBackend( token_path=perms.token_path, token_filename=perms.token_filename, ) _LOGGER.debug("Setup account") account = Account( credentials, token_backend=token_backend, timezone=CONST_UTC_TIMEZONE, main_resource=main_resource, ) try: return account, account.is_authenticated except json.decoder.JSONDecodeError: return account, False async def _async_check_token(hass, account, account_name): try: await hass.async_add_executor_job(account.get_current_user_data) return True except InvalidClientError as err: if "client secret" in err.description and "expired" in err.description: _LOGGER.warning( "Client Secret expired for account: %s. Create new Client Secret in Azure App.", account_name, ) else: _LOGGER.warning( "Token error for account: %s. Error - %s", account_name, err.description ) return False except RuntimeError as err: if "Refresh token operation failed: invalid_grant" in err.args: _LOGGER.warning( "Token has expired for account: '%s'. " + "Please delete token, reboot and re-authenticate.", account_name, ) return False elif "Refresh token operation failed: invalid_client" in err.args: _LOGGER.warning( "Invalid Client ID for account: '%s'. " + "Please delete token, reboot and re-authenticate.", account_name, ) return False raise err def _validate_shared_schema(account_name, main_account, config): if not main_account: return True error = False if config.get(CONF_STATUS_SENSORS, None): _LOGGER.error("Status sensor not allowed for shared account: %s", account_name) error = True if config.get(CONF_CHAT_SENSORS, None): _LOGGER.error("Chat sensor not allowed for shared account: %s", account_name) error = True if ( config.get(CONF_TODO_SENSORS, None) and config.get(CONF_TODO_SENSORS)[CONF_ENABLED] ): _LOGGER.error("Todo sensors not allowed for shared account: %s", account_name) error = True if config.get(CONF_GROUPS, None): _LOGGER.error("Groups not allowed for shared account: %s", account_name) error = True if config.get(CONF_AUTO_REPLY_SENSORS, None): _LOGGER.error( "AutoReply sensor not allowed for shared account: %s", account_name ) error = True return not error async def _async_authorization_repair( hass, account_conf, account, account_name, conf_type, failed_permissions, token_missing, ): base_message = f"requesting authorization for account: {account_name}" if token_missing == TOKEN_FILE_MISSING: message = "No token file found;" elif token_missing == TOKEN_FILE_CORRUPTED: message = "Token file corrupted;" elif token_missing == TOKEN_FILE_OUTDATED: message = "Token file is outdated, it has been deleted;" else: message = "Token doesn't have all required permissions;" _LOGGER.warning("%s %s", message, base_message) data = { CONF_ACCOUNT_CONF: account_conf, CONF_ACCOUNT: account, CONF_ACCOUNT_NAME: account_name, CONF_CONFIG_TYPE: conf_type, CONF_FAILED_PERMISSIONS: failed_permissions, } # Register a repair issue async_create_issue( hass, DOMAIN, "authorization", data=data, is_fixable=True, # learn_more_url=url, severity=IssueSeverity.ERROR, translation_key="authorization", translation_placeholders={ CONF_ACCOUNT_NAME: account_name, }, ) async def _async_setup_migration_service(hass, config): migration_services = MigrationServices(hass, config) hass.services.async_register( DOMAIN, "migrate_config", migration_services.async_migrate_config ) class _IncreaseIndent(yaml.Dumper): def increase_indent(self, flow=False, indentless=False): return super(_IncreaseIndent, self).increase_indent(flow, False) ================================================ FILE: custom_components/o365/calendar.py ================================================ """Main calendar processing.""" import functools as ft import logging import re from copy import deepcopy from datetime import date, datetime, timedelta from operator import attrgetter from typing import Any from homeassistant.components.calendar import ( EVENT_DESCRIPTION, EVENT_END, EVENT_RRULE, EVENT_START, EVENT_SUMMARY, CalendarEntity, CalendarEntityFeature, CalendarEvent, extract_offset, is_offset_reached, ) from homeassistant.const import CONF_NAME from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_platform from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import dt as dt_util from requests.exceptions import HTTPError, RetryError from O365.utils.query import ( # pylint: disable=no-name-in-module, import-error QueryBuilder, ) from .const import ( ATTR_ALL_DAY, ATTR_COLOR, ATTR_DATA, ATTR_EVENT_ID, ATTR_HEX_COLOR, ATTR_OFFSET, CALENDAR_ENTITY_ID_FORMAT, CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_CAL_ID, CONF_CAL_IDS, CONF_CONFIG_TYPE, CONF_DEVICE_ID, CONF_ENABLE_UPDATE, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOURS_BACKWARD_TO_GET, CONF_HOURS_FORWARD_TO_GET, CONF_IS_AUTHENTICATED, CONF_MAX_RESULTS, CONF_PERMISSIONS, CONF_SEARCH, CONF_TRACK, CONF_TRACK_NEW_CALENDAR, CONST_CONFIG_TYPE_LIST, CONST_GROUP, DEFAULT_OFFSET, DOMAIN, EVENT_CREATE_CALENDAR_EVENT, EVENT_HA_EVENT, EVENT_MODIFY_CALENDAR_EVENT, EVENT_MODIFY_CALENDAR_RECURRENCES, EVENT_REMOVE_CALENDAR_EVENT, EVENT_REMOVE_CALENDAR_RECURRENCES, EVENT_RESPOND_CALENDAR_EVENT, LEGACY_ACCOUNT_NAME, PERM_CALENDARS_READWRITE, YAML_CALENDARS_FILENAME, EventResponse, ) from .schema import ( CALENDAR_SERVICE_CREATE_SCHEMA, CALENDAR_SERVICE_MODIFY_SCHEMA, CALENDAR_SERVICE_REMOVE_SCHEMA, CALENDAR_SERVICE_RESPOND_SCHEMA, YAML_CALENDAR_DEVICE_SCHEMA, ) from .utils.calendar_utils import ( add_call_data_to_event, format_event_data, get_end_date, get_hass_date, get_start_date, ) from .utils.filemgmt import ( async_update_calendar_file, build_config_file_path, build_yaml_filename, load_yaml_file, ) from .utils.utils import clean_html _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, add_entities, discovery_info=None): # pylint: disable=unused-argument """Set up the O365 platform.""" if discovery_info is None: return None account_name = discovery_info[CONF_ACCOUNT_NAME] conf = hass.data[DOMAIN][account_name] account = conf[CONF_ACCOUNT] is_authenticated = conf[CONF_IS_AUTHENTICATED] if not is_authenticated: return False update_supported = bool( conf[CONF_ENABLE_UPDATE] and conf[CONF_PERMISSIONS].validate_authorization(PERM_CALENDARS_READWRITE) ) cal_ids = await _async_setup_add_entities( hass, account, add_entities, conf, update_supported ) hass.data[DOMAIN][account_name][CONF_CAL_IDS] = cal_ids await _async_setup_register_services(hass, account, update_supported) _LOGGER.warning( "The O365 Calendar sensors are now deprecated - please migrate to MS365 Calendar " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) return True async def _async_setup_add_entities( hass, account, add_entities, conf, update_supported ): yaml_filename = build_yaml_filename(conf, YAML_CALENDARS_FILENAME) yaml_filepath = build_config_file_path(hass, yaml_filename) calendars = await hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_CAL_ID, YAML_CALENDAR_DEVICE_SCHEMA ) cal_ids = {} for cal_id, calendar in calendars.items(): for entity in calendar.get(CONF_ENTITIES): if not entity[CONF_TRACK]: continue entity_id = _build_entity_id(hass, entity, conf) device_id = entity["device_id"] try: cal = O365CalendarEntity( account, cal_id, entity, entity_id, device_id, conf, update_supported, ) await cal.data.async_calendar_data_init(hass) except HTTPError: _LOGGER.warning( "No permission for calendar, please remove - Name: %s; Device: %s;", entity[CONF_NAME], entity[CONF_DEVICE_ID], ) continue cal_ids[entity_id] = cal_id add_entities([cal], True) return cal_ids def _build_entity_id(hass, entity, conf): account_name = conf[CONF_ACCOUNT_NAME] entity_suffix = ( f"_{account_name}" if ( conf[CONF_CONFIG_TYPE] == CONST_CONFIG_TYPE_LIST and account_name != LEGACY_ACCOUNT_NAME ) else "" ) return generate_entity_id( CALENDAR_ENTITY_ID_FORMAT, f"{entity.get(CONF_DEVICE_ID)}{entity_suffix}", hass=hass, ) async def _async_setup_register_services(hass, account, update_supported): platform = entity_platform.async_get_current_platform() calendar_services = CalendarServices(hass, account) await calendar_services.async_scan_for_calendars(None) if update_supported: platform.async_register_entity_service( "create_calendar_event", CALENDAR_SERVICE_CREATE_SCHEMA, "async_create_calendar_event", ) platform.async_register_entity_service( "modify_calendar_event", CALENDAR_SERVICE_MODIFY_SCHEMA, "async_modify_calendar_event", ) platform.async_register_entity_service( "remove_calendar_event", CALENDAR_SERVICE_REMOVE_SCHEMA, "async_remove_calendar_event", ) platform.async_register_entity_service( "respond_calendar_event", CALENDAR_SERVICE_RESPOND_SCHEMA, "async_respond_calendar_event", ) hass.services.async_register( DOMAIN, "scan_for_calendars", calendar_services.async_scan_for_calendars ) class O365CalendarEntity(CalendarEntity): """O365 Calendar Event Processing.""" _unrecorded_attributes = frozenset((ATTR_DATA, ATTR_COLOR, ATTR_HEX_COLOR)) def __init__( self, account, calendar_id, entity, entity_id, device_id, config, update_supported, ): """Initialise the O365 Calendar Event.""" self._config = config self._account = account self._start_offset = entity.get(CONF_HOURS_BACKWARD_TO_GET) self._end_offset = entity.get(CONF_HOURS_FORWARD_TO_GET) self._event = {} self._name = f"{entity.get(CONF_NAME)}" self.entity_id = entity_id self._offset_reached = False self._data_attribute = [] self.data = self._init_data(calendar_id, entity) self._calendar_id = calendar_id self._device_id = device_id if update_supported: self._attr_supported_features = ( CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT | CalendarEntityFeature.UPDATE_EVENT ) self._error = None def _init_data(self, calendar_id, entity): max_results = entity.get(CONF_MAX_RESULTS) search = entity.get(CONF_SEARCH) exclude = entity.get(CONF_EXCLUDE) return O365CalendarData( self._account, self.entity_id, calendar_id, search, exclude, max_results, ) @property def extra_state_attributes(self): """Extra state attributes.""" attributes = { ATTR_DATA: self._data_attribute, } if hasattr(self.data.calendar, ATTR_COLOR): attributes[ATTR_COLOR] = self.data.calendar.color if hasattr(self.data.calendar, ATTR_HEX_COLOR) and self.data.calendar.hex_color: attributes[ATTR_HEX_COLOR] = self.data.calendar.hex_color if self._event: attributes[ATTR_ALL_DAY] = ( self._event.all_day if self.data.event is not None else False ) attributes[ATTR_OFFSET] = self._offset_reached return attributes @property def event(self): """Event property.""" return self._event @property def name(self): """Name property.""" return self._name @property def unique_id(self): """Entity unique id.""" return ( f"{self._calendar_id}_{self._config[CONF_ACCOUNT_NAME]}_{self._device_id}" ) async def async_get_events(self, hass, start_date, end_date): """Get events.""" return await self.data.async_get_events(hass, start_date, end_date) async def async_update(self): """Do the update.""" # Get today's event for HA Core. try: await self.data.async_update(self.hass) event = deepcopy(self.data.event) except (HTTPError, RetryError, ConnectionError) as err: self._log_error("Error getting calendar events for day", err) return if event: event.summary, offset = extract_offset(event.summary, DEFAULT_OFFSET) start = O365CalendarData.to_datetime(event.start) self._offset_reached = is_offset_reached(start, offset) # Get events for extra attributes. try: results = await self.data.async_o365_get_events( self.hass, dt_util.utcnow() + timedelta(hours=self._start_offset), dt_util.utcnow() + timedelta(hours=self._end_offset), ) except (HTTPError, RetryError, ConnectionError) as err: self._log_error("Error getting calendar events for data", err) return self._error = False if results is not None: self._data_attribute = [format_event_data(x) for x in results] self._event = event async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" start = kwargs[EVENT_START] end = kwargs[EVENT_END] is_all_day = not isinstance(start, datetime) subject = kwargs[EVENT_SUMMARY] body = kwargs.get(EVENT_DESCRIPTION) rrule = kwargs.get(EVENT_RRULE) await self.async_create_calendar_event( subject, start, end, body=body, is_all_day=is_all_day, rrule=rrule, ) async def async_update_event( self, uid: str, event: dict[str, Any], recurrence_id: str | None = None, recurrence_range: str | None = None, ) -> None: """Update an event on the calendar.""" start = event[EVENT_START] end = event[EVENT_END] is_all_day = not isinstance(start, datetime) subject = event[EVENT_SUMMARY] body = event.get(EVENT_DESCRIPTION) rrule = event.get(EVENT_RRULE) await self.async_modify_calendar_event( event_id=uid, recurrence_id=recurrence_id, recurrence_range=recurrence_range, subject=subject, start=start, end=end, body=body, is_all_day=is_all_day, rrule=rrule, ) async def async_delete_event( self, uid: str, recurrence_id: str | None = None, recurrence_range: str | None = None, ) -> None: """Delete an event on the calendar.""" await self.async_remove_calendar_event(uid, recurrence_id, recurrence_range) async def async_create_calendar_event(self, subject, start, end, **kwargs): """Create the event.""" if not self._validate_permissions("create"): return calendar = self.data.calendar event = calendar.new_event() event = add_call_data_to_event(event, subject, start, end, **kwargs) await self.hass.async_add_executor_job(event.save) self._raise_event(EVENT_CREATE_CALENDAR_EVENT, event.object_id) self.async_schedule_update_ha_state(True) async def async_modify_calendar_event( self, event_id, recurrence_id=None, recurrence_range=None, subject=None, start=None, end=None, **kwargs, ): """Modify the event.""" if not self._validate_permissions("modify"): return if self.data.group_calendar: _group_calendar_log(self.entity_id) return if recurrence_range: await self._async_update_calendar_event( recurrence_id, EVENT_MODIFY_CALENDAR_RECURRENCES, subject, start, end, **kwargs, ) else: await self._async_update_calendar_event( event_id, EVENT_MODIFY_CALENDAR_EVENT, subject, start, end, **kwargs ) async def _async_update_calendar_event( self, event_id, ha_event, subject, start, end, **kwargs ): event = await self._async_get_event_from_calendar(event_id) event = add_call_data_to_event(event, subject, start, end, **kwargs) await self.hass.async_add_executor_job(event.save) self._raise_event(ha_event, event_id) self.async_schedule_update_ha_state(True) async def async_remove_calendar_event( self, event_id, recurrence_id: str | None = None, recurrence_range: str | None = None, ): """Remove the event.""" if not self._validate_permissions("delete"): return if self.data.group_calendar: _group_calendar_log(self.entity_id) return if recurrence_range: await self._async_delete_calendar_event( recurrence_id, EVENT_REMOVE_CALENDAR_RECURRENCES ) else: await self._async_delete_calendar_event( event_id, EVENT_REMOVE_CALENDAR_EVENT ) def _log_error(self, error, err): if not self._error: _LOGGER.warning("%s - %s", error, err) self._error = True else: _LOGGER.debug("Repeat error - %s - %s", error, err) async def _async_delete_calendar_event(self, event_id, ha_event): event = await self._async_get_event_from_calendar(event_id) await self.hass.async_add_executor_job( event.delete, ) self._raise_event(ha_event, event_id) self.async_schedule_update_ha_state(True) async def async_respond_calendar_event( self, event_id, response, send_response=True, message=None ): """Respond to calendar event.""" if not self._validate_permissions("respond to"): return if self.data.group_calendar: _group_calendar_log(self.entity_id) return await self._async_send_response(event_id, response, send_response, message) self._raise_event(EVENT_RESPOND_CALENDAR_EVENT, event_id) self.async_schedule_update_ha_state(True) async def _async_send_response(self, event_id, response, send_response, message): event = await self._async_get_event_from_calendar(event_id) if response == EventResponse.Accept: await self.hass.async_add_executor_job( ft.partial(event.accept_event, message, send_response=send_response) ) elif response == EventResponse.Tentative: await self.hass.async_add_executor_job( ft.partial( event.accept_event, message, tentatively=True, send_response=send_response, ) ) elif response == EventResponse.Decline: await self.hass.async_add_executor_job( ft.partial(event.decline_event, message, send_response=send_response) ) async def _async_get_event_from_calendar(self, event_id): calendar = self.data.calendar return await self.hass.async_add_executor_job(calendar.get_event, event_id) def _validate_permissions(self, error_message): if not self._config[CONF_PERMISSIONS].validate_authorization( PERM_CALENDARS_READWRITE ): raise ServiceValidationError( translation_domain=DOMAIN, translation_key="not_authorised_to_event", translation_placeholders={ "calendar": PERM_CALENDARS_READWRITE, "error_message": error_message, }, ) return True def _raise_event(self, event_type, event_id): self.hass.bus.fire( f"{DOMAIN}_{event_type}", {ATTR_EVENT_ID: event_id, EVENT_HA_EVENT: True}, ) _LOGGER.debug("%s - %s", event_type, event_id) class O365CalendarData: """O365 Calendar Data.""" def __init__( self, account, entity_id, calendar_id, search=None, exclude=None, limit=999, ): """Initialise the O365 Calendar Data.""" self._account = account self._limit = limit self.group_calendar = calendar_id.startswith(CONST_GROUP) self.calendar_id = calendar_id self._schedule = None self.calendar = None self._search = search self._exclude = exclude self.event = None self._entity_id = entity_id self._error = False self._builder = QueryBuilder(protocol=account.protocol) async def async_calendar_data_init(self, hass): """Async init of calendar data.""" if self.group_calendar: self._schedule = None self.calendar = await hass.async_add_executor_job( ft.partial(self._account.schedule, resource=self.calendar_id) ) else: self._schedule = await hass.async_add_executor_job(self._account.schedule) self.calendar = None async def _async_get_calendar(self, hass): try: schedule = await hass.async_add_executor_job(self._account.schedule) query = self._builder.select("name", "id", "canEdit", "color", "hexColor") self.calendar = await hass.async_add_executor_job( ft.partial( schedule.get_calendar, calendar_id=self.calendar_id, query=query ) ) return True except (HTTPError, RetryError, ConnectionError) as err: _LOGGER.warning("Error getting calendar events - %s", err) return False async def async_o365_get_events(self, hass, start_date, end_date): """Get the events.""" if not self.calendar: if not await self._async_get_calendar(hass): return [] events = await self._async_calendar_schedule_get_events( hass, self.calendar, start_date, end_date ) if events is None: return None events = self._filter_events(events) events = self._sort_events(events) return events def _filter_events(self, events): lst_events = list(events) if not events or not self._exclude: return lst_events rtn_events = [] for event in lst_events: include = True for exclude in self._exclude: if re.search(exclude, event.subject): include = False if include: rtn_events.append(event) return rtn_events def _sort_events(self, events): for event in events: event.start_sort = event.start if event.is_all_day: event.start_sort = dt_util.as_utc( dt_util.start_of_local_day(event.start) ) events.sort(key=attrgetter("start_sort")) return events async def _async_calendar_schedule_get_events( self, hass, calendar_schedule, start_date, end_date ): """Get the events for the calendar.""" query = self._builder.select( "subject", "body", "start", "end", "is_all_day", "location", "categories", "sensitivity", "show_as", "attendees", "series_master_id", ) if self._search is not None: query = query & self._builder.contains("subject", self._search) # As at March 2023 not contains is not supported by Graph API # if self._exclude is not None: # query.chain("and").on_attribute("subject").negate().contains(self._exclude) try: return await hass.async_add_executor_job( ft.partial( calendar_schedule.get_events, limit=self._limit, query=query, include_recurring=True, start_recurring=self._builder.greater_equal("start", start_date), end_recurring=self._builder.less_equal("end", end_date), ) ) except (HTTPError, RetryError, ConnectionError) as err: _LOGGER.warning("Error getting calendar events - %s", err) return None async def async_get_events(self, hass, start_date, end_date): """Get the via async.""" results = await self.async_o365_get_events(hass, start_date, end_date) if not results: return [] event_list = [] for vevent in results: try: event = CalendarEvent( get_hass_date(vevent.start, vevent.is_all_day), get_hass_date(get_end_date(vevent), vevent.is_all_day), vevent.subject, clean_html(vevent.body), vevent.location["displayName"], uid=vevent.object_id, ) if vevent.series_master_id: event.recurrence_id = vevent.series_master_id event_list.append(event) except HomeAssistantError as err: _LOGGER.warning( "Invalid event found - Error: %s, Event: %s", err, vevent ) return event_list async def async_update(self, hass): """Do the update.""" start_of_day_utc = dt_util.as_utc(dt_util.start_of_local_day()) results = await self.async_o365_get_events( hass, start_of_day_utc, start_of_day_utc + timedelta(days=1), ) if not results: _LOGGER.debug( "No current event found for %s", self._entity_id, ) self.event = None return vevent = self._get_root_event(results) if vevent is None: _LOGGER.debug( "No matching event found in the %d results for %s", len(results), self._entity_id, ) self.event = None return try: self.event = CalendarEvent( get_hass_date(vevent.start, vevent.is_all_day), get_hass_date(get_end_date(vevent), vevent.is_all_day), vevent.subject, clean_html(vevent.body), vevent.location["displayName"], ) self._error = False except HomeAssistantError as err: if not self._error: _LOGGER.warning( "Invalid event found - Error: %s, Event: %s", err, vevent ) self._error = True def _get_root_event(self, results): started_event = None not_started_event = None all_day_event = None for event in results: if event.is_all_day: if not all_day_event and not self.is_finished(event): all_day_event = event continue if self.is_started(event) and not self.is_finished(event): if not started_event: started_event = event continue if ( not self.is_finished(event) and not event.is_all_day and not not_started_event ): not_started_event = event vevent = None if started_event: vevent = started_event elif all_day_event: vevent = all_day_event elif not_started_event: vevent = not_started_event return vevent @staticmethod def is_all_day(vevent): """Is it all day.""" return vevent.is_all_day @staticmethod def is_started(vevent): """Is it over.""" return dt_util.utcnow() >= O365CalendarData.to_datetime(get_start_date(vevent)) @staticmethod def is_finished(vevent): """Is it over.""" return dt_util.utcnow() >= O365CalendarData.to_datetime(get_end_date(vevent)) @staticmethod def to_datetime(obj): """To datetime.""" if isinstance(obj, datetime): date_obj = ( obj.replace(tzinfo=dt_util.get_default_time_zone()) if obj.tzinfo is None else obj ) elif isinstance(obj, date): date_obj = dt_util.start_of_local_day( dt_util.dt.datetime.combine(obj, dt_util.dt.time.min) ) elif "date" in obj: date_obj = dt_util.start_of_local_day( dt_util.dt.datetime.combine( dt_util.parse_date(obj["date"]), dt_util.dt.time.min ) ) else: date_obj = dt_util.as_local(dt_util.parse_datetime(obj["dateTime"])) return dt_util.as_utc(date_obj) class CalendarServices: """Calendar Services.""" def __init__(self, hass, account): """Initialise the calendar services.""" self._hass = hass self._account = account async def async_scan_for_calendars(self, call): # pylint: disable=unused-argument """Scan for new calendars.""" for config in self._hass.data[DOMAIN]: config = self._hass.data[DOMAIN][config] if CONF_ACCOUNT in config: schedule = await self._hass.async_add_executor_job( config[CONF_ACCOUNT].schedule ) builder = QueryBuilder(protocol=self._account.protocol) query = builder.select("name", "id", "canEdit", "color", "hexColor") calendars = await self._hass.async_add_executor_job( ft.partial(schedule.list_calendars, query=query, limit=50) ) track = config.get(CONF_TRACK_NEW_CALENDAR, True) for calendar in calendars: await async_update_calendar_file( config, calendar, self._hass, track, ) def _group_calendar_log(entity_id): raise ServiceValidationError( translation_domain=DOMAIN, translation_key="o365_group_calendar_error", translation_placeholders={ "entity_id": entity_id, }, ) ================================================ FILE: custom_components/o365/classes/__init__.py ================================================ """Initialise.""" ================================================ FILE: custom_components/o365/classes/entity.py ================================================ """Generic O465 Sensor Entity.""" from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.update_coordinator import CoordinatorEntity from ..const import ATTR_DATA, CONF_PERMISSIONS, DOMAIN class O365Entity(CoordinatorEntity): """O365 generic Sensor class.""" _attr_should_poll = False _unrecorded_attributes = frozenset((ATTR_DATA,)) def __init__(self, coordinator, config, name, entity_id, entity_type, unique_id): """Initialise the O365 Sensor.""" super().__init__(coordinator) self._config = config self._name = name self._entity_id = entity_id self.entity_type = entity_type self._unique_id = unique_id @property def name(self): """Name property.""" return self._name @property def entity_key(self): """Entity Key property.""" return self._entity_id @property def unique_id(self): """Entity unique id.""" return self._unique_id def _validate_permissions(self, required_permission, required_permission_error): if not self._config[CONF_PERMISSIONS].validate_authorization( required_permission ): raise ServiceValidationError( translation_domain=DOMAIN, translation_key="not_authorised", translation_placeholders={ "required_permission": required_permission_error, }, ) return True ================================================ FILE: custom_components/o365/classes/mailsensor.py ================================================ """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 O365.utils.query import ( # pylint: disable=no-name-in-module, import-error QueryBuilder, ) from ..const import ( ATTR_AUTOREPLIESSETTINGS, ATTR_DATA, ATTR_END, ATTR_EXTERNAL_AUDIENCE, ATTR_EXTERNALREPLY, ATTR_INTERNALREPLY, ATTR_START, ATTR_STATE, CONF_ACCOUNT, CONF_BODY_CONTAINS, CONF_DOWNLOAD_ATTACHMENTS, CONF_HAS_ATTACHMENT, CONF_HTML_BODY, CONF_IMPORTANCE, CONF_IS_UNREAD, CONF_MAIL_FROM, CONF_SHOW_BODY, CONF_SUBJECT_CONTAINS, CONF_SUBJECT_IS, DATETIME_FORMAT, PERM_MAILBOX_SETTINGS, SENSOR_AUTO_REPLY, SENSOR_EMAIL, ) from ..utils.utils import clean_html, get_email_attributes from .entity import O365Entity class O365MailSensor(O365Entity, SensorEntity): """O365 generic Mail Sensor class.""" _attr_translation_key = "mail" def __init__(self, coordinator, config, sensor_conf, name, entity_id, unique_id): """Initialise the O365 Sensor.""" 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._show_body = sensor_conf.get(CONF_SHOW_BODY) self._state = None self._extra_attributes = None self._update_status() @property def native_value(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: self._update_status() self.async_write_ha_state() def _update_status(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} def _get_attributes(self, data): return [ get_email_attributes( x, self._download_attachments, self._html_body, self._show_body ) for x in data ] class O365AutoReplySensor(O365Entity, SensorEntity): """O365 Auto Reply sensor processing.""" _attr_translation_key = "auto_reply" def __init__(self, coordinator, name, entity_id, config, unique_id): """Initialise the Auto reply Sensor.""" super().__init__( coordinator, config, name, entity_id, SENSOR_AUTO_REPLY, unique_id ) self._config = config self._account = self._config[CONF_ACCOUNT] self.mailbox = None async def async_init(self, hass): """async initialise.""" self.mailbox = await hass.async_add_executor_job(self._account.mailbox) @property def native_value(self): """Sensor state.""" return self.coordinator.data[self.entity_key][ATTR_STATE] @property def extra_state_attributes(self): """Return entity specific state attributes.""" ars = self.coordinator.data[self.entity_key][ATTR_AUTOREPLIESSETTINGS] return { ATTR_INTERNALREPLY: clean_html(ars.internal_reply_message), ATTR_EXTERNALREPLY: clean_html(ars.external_reply_message), ATTR_EXTERNAL_AUDIENCE: ars.external_audience.value, ATTR_START: ars.scheduled_startdatetime.strftime(DATETIME_FORMAT), ATTR_END: ars.scheduled_enddatetime.strftime(DATETIME_FORMAT), } async def async_auto_reply_enable( self, external_reply, internal_reply, start=None, end=None, external_audience=mailbox.ExternalAudience.ALL, ): """Enable out of office autoreply.""" if not self._validate_autoreply_permissions(): return await self.hass.async_add_executor_job( self.mailbox.set_automatic_reply, internal_reply, external_reply, start, end, external_audience, ) async def async_auto_reply_disable(self): """Disable out of office autoreply.""" if not self._validate_autoreply_permissions(): return await self.hass.async_add_executor_job(self.mailbox.set_disable_reply) def _validate_autoreply_permissions(self): return self._validate_permissions( PERM_MAILBOX_SETTINGS, "Not authorised to update auto reply - requires permission: " + f"{PERM_MAILBOX_SETTINGS}", ) async def _async_build_base_query(sensor_conf, builder): """Build base query for mail.""" download_attachments = sensor_conf.get(CONF_DOWNLOAD_ATTACHMENTS) show_body = sensor_conf.get(CONF_SHOW_BODY) html_body = sensor_conf.get(CONF_HTML_BODY) query = builder.select( "sender", "from", "subject", "receivedDateTime", "toRecipients", "ccRecipients", "has_attachments", "importance", "is_read", "flag", ) if show_body or html_body: query = query & builder.select( "body", ) if download_attachments: query = query & builder.select( "attachments", ) return query async def async_build_inbox_query(sensor_conf, builder: QueryBuilder): """Build query for email sensor.""" query = await _async_build_base_query(sensor_conf, builder) is_unread = sensor_conf.get(CONF_IS_UNREAD) if is_unread is not None: query = _add_to_query( query, builder, "equals", "IsRead", not is_unread, is_unread ) return query async def async_build_query_query(sensor_conf, builder: QueryBuilder): """Build query for query sensor.""" query = await _async_build_base_query(sensor_conf, builder) query = query & builder.orderby(("receivedDateTime", 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 = query & builder.greater_equal( "receivedDateTime", datetime.datetime(1900, 5, 1) ) query = _add_to_query(query, builder, "contains", "body", body_contains) query = _add_to_query(query, builder, "contains", "subject", subject_contains) query = _add_to_query(query, builder, "equals", "subject", subject_is) query = _add_to_query(query, builder, "equals", "hasAttachments", has_attachment) query = _add_to_query(query, builder, "equals", "from", email_from) query = _add_to_query(query, builder, "equals", "IsRead", not is_unread, is_unread) query = _add_to_query(query, builder, "equals", "importance", importance) return query def _add_to_query( query, builder: QueryBuilder, qtype, attribute_name, attribute_value, check_value=True, ): if attribute_value is None or check_value is None: return query if qtype == "ge": query = query & builder.greater_equal(attribute_name, attribute_value) if qtype == "contains": query = query & builder.contains(attribute_name, attribute_value) if qtype == "equals": query = query & builder.equals(attribute_name, attribute_value) return query ================================================ FILE: custom_components/o365/classes/permissions.py ================================================ """Permissions processes.""" import json import logging import os from copy import deepcopy from homeassistant.const import CONF_EMAIL, CONF_ENABLED from ..const import ( CONF_ACCOUNT_NAME, CONF_AUTO_REPLY_SENSORS, CONF_BASIC_CALENDAR, CONF_CHAT_SENSORS, CONF_EMAIL_SENSORS, CONF_ENABLE_CALENDAR, CONF_ENABLE_UPDATE, CONF_GROUPS, CONF_QUERY_SENSORS, CONF_SHARED_MAILBOX, CONF_STATUS_SENSORS, CONF_TODO_SENSORS, CONST_CONFIG_TYPE_LIST, O365_STORAGE_TOKEN, PERM_BASE_PERMISSIONS, PERM_CALENDARS_READ, PERM_CALENDARS_READBASIC, PERM_CALENDARS_READWRITE, PERM_CHAT_READ, PERM_CHAT_READWRITE, PERM_GROUP_READ_ALL, PERM_GROUP_READWRITE_ALL, PERM_MAIL_READ, PERM_MAIL_SEND, PERM_MAILBOX_SETTINGS, PERM_PRESENCE_READ, PERM_PRESENCE_READ_ALL, PERM_PRESENCE_READWRITE, PERM_SHARED, PERM_TASKS_READ, PERM_TASKS_READWRITE, PERM_USER_READBASIC_ALL, TOKEN_FILE_CORRUPTED, TOKEN_FILE_MISSING, TOKEN_FILE_OUTDATED, TOKEN_FILE_PERMISSIONS, TOKEN_FILENAME, ) from ..utils.filemgmt import build_config_file_path _LOGGER = logging.getLogger(__name__) class Permissions: """Class in support of building permssion sets.""" def __init__(self, hass, config, conf_type): """Initialise the class.""" self._hass = hass self._config = config self._conf_type = conf_type self._shared = PERM_SHARED if config.get(CONF_SHARED_MAILBOX) else "" self._enable_update = self._config.get(CONF_ENABLE_UPDATE, False) self._requested_permissions = [] self.token_filename = self._build_token_filename() self.token_path = build_config_file_path(self._hass, O365_STORAGE_TOKEN) self._permissions = [] @property def requested_permissions(self): """Return the required scope.""" if not self._requested_permissions: self._requested_permissions = deepcopy(PERM_BASE_PERMISSIONS) self._build_calendar_permissions() self._build_group_permissions() self._build_email_permissions() self._build_autoreply_permissions() self._build_status_permissions() self._build_chat_permissions() self._build_todo_permissions() return self._requested_permissions @property def permissions(self): """Return the permission set.""" return self._permissions async def async_check_authorizations(self): """Report on permissions status.""" self._permissions = await self._hass.async_add_executor_job( self._get_permissions ) if self._permissions in [ TOKEN_FILE_MISSING, TOKEN_FILE_CORRUPTED, TOKEN_FILE_OUTDATED, ]: return self._permissions, None failed_permissions = [] for permission in self.requested_permissions: if not self.validate_authorization(permission): failed_permissions.append(permission) if failed_permissions: _LOGGER.warning( "Minimum required permissions: '%s'. Not available in token '%s' for account '%s'.", ", ".join(failed_permissions), self.token_filename, self._config[CONF_ACCOUNT_NAME], ) return TOKEN_FILE_PERMISSIONS, failed_permissions return True, None def validate_authorization(self, permission): """Validate higher permissions.""" if permission in self.permissions: return True if self._check_higher_permissions(permission): return True resource = permission.split(".")[0] constraint = permission.split(".")[1] if len(permission) == 3 else None # If Calendar or Mail Resource then permissions can have a constraint of .Shared # which includes base as well. e.g. Calendar.Read is also enabled by Calendar.Read.Shared if not constraint and resource in ["Calendar", "Mail"]: sharedpermission = f"{deepcopy(permission)}.Shared" return self._check_higher_permissions(sharedpermission) # If Presence Resource then permissions can have a constraint of .All # which includes base as well. e.g. Presencedar.Read is also enabled by Presence.Read.All if not constraint and resource in ["Presence"]: allpermission = f"{deepcopy(permission)}.All" return self._check_higher_permissions(allpermission) return False def _check_higher_permissions(self, permission): operation = permission.split(".")[1] # If Operation is ReadBasic then Read or ReadWrite will also work # If Operation is Read then ReadWrite will also work newops = [operation] if operation == "ReadBasic": newops = newops + ["Read", "ReadWrite"] elif operation == "Read": newops = newops + ["ReadWrite"] for newop in newops: newperm = deepcopy(permission).replace(operation, newop) if newperm in self.permissions: return True return False def _build_token_filename(self): """Create the token file name.""" config_file = ( f"_{self._config.get(CONF_ACCOUNT_NAME)}" if self._conf_type == CONST_CONFIG_TYPE_LIST else "" ) return TOKEN_FILENAME.format(config_file) def _get_permissions(self): """Get the permissions from the token file.""" full_token_path = os.path.join(self.token_path, self.token_filename) if not os.path.exists(full_token_path) or not os.path.isfile(full_token_path): _LOGGER.warning("Could not locate token at %s", full_token_path) return TOKEN_FILE_MISSING try: with open(full_token_path, "r", encoding="UTF-8") as file_handle: raw = file_handle.read() permissions = next(iter(json.loads(raw)["AccessToken"].values()))[ "target" ].split() except json.decoder.JSONDecodeError as err: _LOGGER.warning("Token corrupted at %s - %s", full_token_path, err) return TOKEN_FILE_CORRUPTED except KeyError: _LOGGER.warning( "Legacy token found at %s, it has been deleted", full_token_path ) self.delete_token() return TOKEN_FILE_OUTDATED return permissions def _build_calendar_permissions(self): if not self._config.get(CONF_ENABLE_CALENDAR, True): return if self._config.get(CONF_BASIC_CALENDAR, False): if self._enable_update: _LOGGER.warning( "'enable_update' should not be true when 'basic_calendar' is true ." + "for account: %s ReadBasic used. ", self._config[CONF_ACCOUNT_NAME], ) self._requested_permissions.append(PERM_CALENDARS_READBASIC + self._shared) elif self._enable_update: self._requested_permissions.extend( (PERM_MAIL_SEND + self._shared, PERM_CALENDARS_READWRITE + self._shared) ) else: self._requested_permissions.append(PERM_CALENDARS_READ + self._shared) def _build_group_permissions(self): if self._config.get(CONF_GROUPS, False): if self._enable_update: self._requested_permissions.append(PERM_GROUP_READWRITE_ALL) else: self._requested_permissions.append(PERM_GROUP_READ_ALL) def _build_email_permissions(self): email_sensors = self._config.get(CONF_EMAIL_SENSORS, []) query_sensors = self._config.get(CONF_QUERY_SENSORS, []) if len(email_sensors) > 0 or len(query_sensors) > 0: self._requested_permissions.append(PERM_MAIL_READ + self._shared) def _build_autoreply_permissions(self): auto_reply_sensors = self._config.get(CONF_AUTO_REPLY_SENSORS, []) if len(auto_reply_sensors) > 0: self._requested_permissions.append(PERM_MAILBOX_SETTINGS) def _build_status_permissions(self): status_sensors = self._config.get(CONF_STATUS_SENSORS, []) if len(status_sensors) > 0: if any( status_sensor.get(CONF_ENABLE_UPDATE) for status_sensor in status_sensors ): self._requested_permissions.append(PERM_PRESENCE_READWRITE) else: self._requested_permissions.append(PERM_PRESENCE_READ) if any(status_sensor.get(CONF_EMAIL) for status_sensor in status_sensors): self._requested_permissions.append(PERM_PRESENCE_READ_ALL) self._requested_permissions.append(PERM_USER_READBASIC_ALL) def _build_chat_permissions(self): chat_sensors = self._config.get(CONF_CHAT_SENSORS, []) if len(chat_sensors) > 0: if chat_sensors[0][CONF_ENABLE_UPDATE]: self._requested_permissions.append(PERM_CHAT_READWRITE) else: self._requested_permissions.append(PERM_CHAT_READ) def _build_todo_permissions(self): todo_sensors = self._config.get(CONF_TODO_SENSORS, []) if todo_sensors and todo_sensors.get(CONF_ENABLED, False): if todo_sensors[CONF_ENABLE_UPDATE]: self._requested_permissions.append(PERM_TASKS_READWRITE) else: self._requested_permissions.append(PERM_TASKS_READ) def delete_token(self): """Delete the token.""" full_token_path = os.path.join(self.token_path, self.token_filename) if os.path.exists(full_token_path): os.remove(full_token_path) ================================================ FILE: custom_components/o365/classes/teamssensor.py ================================================ """O365 teams sensors.""" import functools as ft import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_NAME, CONF_EMAIL from homeassistant.exceptions import ServiceValidationError from O365.teams import ( # pylint: disable=import-error, no-name-in-module PreferredActivity, PreferredAvailability, ) from ..const import ( ATTR_ACTIVITY, ATTR_AVAILABILITY, ATTR_CHAT_ID, ATTR_CONTENT, ATTR_DATA, ATTR_FROM_DISPLAY_NAME, ATTR_IMPORTANCE, ATTR_STATE, ATTR_STATUS, ATTR_SUBJECT, ATTR_SUMMARY, CONF_ACCOUNT, CONF_CLIENT_ID, DOMAIN, EVENT_HA_EVENT, EVENT_SEND_CHAT_MESSAGE, EVENT_UPDATE_USER_PREFERRED_STATUS, EVENT_UPDATE_USER_STATUS, PERM_CHAT_READWRITE, PERM_PRESENCE_READWRITE, SENSOR_TEAMS_CHAT, SENSOR_TEAMS_STATUS, ) from .entity import O365Entity _LOGGER = logging.getLogger(__name__) class O365TeamsSensor(O365Entity): """O365 Teams sensor processing.""" _attr_translation_key = "teams" 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 = self._config[CONF_ACCOUNT].teams() self._application_id = self._config[CONF_CLIENT_ID] @property def native_value(self): """Sensor state.""" return self.coordinator.data[self.entity_key][ATTR_STATE] class O365TeamsStatusSensor(O365TeamsSensor, SensorEntity): """O365 Teams sensor processing.""" def __init__(self, coordinator, name, entity_id, config, unique_id, email): """Initialise the Teams Sensor.""" super().__init__( coordinator, name, entity_id, config, SENSOR_TEAMS_STATUS, unique_id, ) self._email = email async def async_update_user_status( self, availability, activity, expiration_duration=None ): """Update the users teams status.""" if self._email: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="not_possible", translation_placeholders={ CONF_EMAIL: self._email, }, ) if not self._validate_status_permissions(): return False status = await self.hass.async_add_executor_job( self.teams.set_my_presence, self._application_id, availability, activity, expiration_duration, ) self._raise_event( EVENT_UPDATE_USER_STATUS, {ATTR_AVAILABILITY: status.availability, ATTR_ACTIVITY: status.activity}, ) return False async def async_update_user_preferred_status( self, availability, expiration_duration=None ): """Update the users teams status.""" if self._email: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="not_possible", translation_placeholders={ CONF_EMAIL: self._email, }, ) if not self._validate_status_permissions(): return False activity = ( availability if availability != PreferredAvailability.OFFLINE else PreferredActivity.OFFWORK ) status = await self.hass.async_add_executor_job( self.teams.set_my_user_preferred_presence, availability, activity, expiration_duration, ) self._raise_event( EVENT_UPDATE_USER_PREFERRED_STATUS, {ATTR_AVAILABILITY: status.availability, ATTR_ACTIVITY: status.activity}, ) return False def _raise_event(self, event_type, status): self.hass.bus.fire( f"{DOMAIN}_{event_type}", {ATTR_NAME: self._name, ATTR_STATUS: status, EVENT_HA_EVENT: True}, ) _LOGGER.debug("%s - %s - %s", self._name, event_type, status) def _validate_status_permissions(self): return self._validate_permissions( PERM_PRESENCE_READWRITE, f"Not authorised to update status - requires permission: {PERM_PRESENCE_READWRITE}", ) class O365TeamsChatSensor(O365TeamsSensor, SensorEntity): """O365 Teams Chat sensor processing.""" def __init__(self, coordinator, name, entity_id, config, unique_id): """Initialise the Teams Chat Sensor.""" super().__init__( coordinator, name, entity_id, config, SENSOR_TEAMS_CHAT, unique_id ) @property def extra_state_attributes(self): """Return entity specific state attributes.""" attributes = { ATTR_FROM_DISPLAY_NAME: self.coordinator.data[self.entity_key][ ATTR_FROM_DISPLAY_NAME ], ATTR_CONTENT: self.coordinator.data[self.entity_key][ATTR_CONTENT], ATTR_CHAT_ID: self.coordinator.data[self.entity_key][ATTR_CHAT_ID], ATTR_IMPORTANCE: self.coordinator.data[self.entity_key][ATTR_IMPORTANCE], } if self.coordinator.data[self.entity_key][ATTR_SUBJECT]: attributes[ATTR_SUBJECT] = self.coordinator.data[self.entity_key][ ATTR_SUBJECT ] if self.coordinator.data[self.entity_key][ATTR_SUMMARY]: attributes[ATTR_SUMMARY] = self.coordinator.data[self.entity_key][ ATTR_SUMMARY ] if self.coordinator.data[self.entity_key][ATTR_DATA]: attributes[ATTR_DATA] = self.coordinator.data[self.entity_key][ATTR_DATA] return attributes async def async_send_chat_message(self, chat_id, message, content_type): """Send a message to the specified chat.""" if not self._validate_chat_permissions(): return False chats = await self.hass.async_add_executor_job(self.teams.get_my_chats) for chat in chats: if chat.object_id == chat_id: message = await self.hass.async_add_executor_job( ft.partial( chat.send_message, content=message, content_type=content_type ) ) self._raise_event(EVENT_SEND_CHAT_MESSAGE, chat_id) return True _LOGGER.warning("Chat %s not found for send message", chat_id) return False def _raise_event(self, event_type, chat_id): self.hass.bus.fire( f"{DOMAIN}_{event_type}", {ATTR_CHAT_ID: chat_id, EVENT_HA_EVENT: True}, ) _LOGGER.debug("%s - %s", event_type, chat_id) def _validate_chat_permissions(self): return self._validate_permissions( PERM_CHAT_READWRITE, f"Not authorised to send message - requires permission: {PERM_CHAT_READWRITE}", ) ================================================ FILE: custom_components/o365/const.py ================================================ """Constants.""" from enum import Enum class EventResponse(Enum): """Event response.""" Accept = "accept" # pylint: disable=invalid-name Tentative = "tentative" # pylint: disable=invalid-name Decline = "decline" # pylint: disable=invalid-name ATTR_ACTIVITY = "activity" ATTR_ALL_DAY = "all_day" ATTR_ALL_TODOS = "all_todos" ATTR_ATTACHMENTS = "attachments" ATTR_ATTENDEES = "attendees" ATTR_AUTOREPLIESSETTINGS = "autorepliessettings" ATTR_AVAILABILITY = "availability" ATTR_BODY = "body" ATTR_CATEGORIES = "categories" ATTR_CHAT_ID = "chat_id" ATTR_CHAT_TYPE = "chat_type" ATTR_COMPLETED = "completed" ATTR_CONTENT_TYPE = "content_type" ATTR_CREATED = "created" ATTR_COLOR = "color" ATTR_CONTENT = "content" ATTR_DATA = "data" ATTR_DESCRIPTION = "description" ATTR_DUE = "due" ATTR_EMAIL = "email" ATTR_END = "end" ATTR_ERROR = "error" ATTR_EVENT_ID = "event_id" ATTR_EXPIRATIONDURATION = "expiration_duration" ATTR_EXTERNAL_AUDIENCE = "external_audience" ATTR_EXTERNALREPLY = "external_reply" ATTR_FROM_DISPLAY_NAME = "from_display_name" ATTR_HEX_COLOR = "hex_color" ATTR_IS_ALL_DAY = "is_all_day" ATTR_IMPORTANCE = "importance" ATTR_INTERNALREPLY = "internal_reply" ATTR_LOCATION = "location" ATTR_MEMBERS = "members" ATTR_MESSAGE_IS_HTML = "message_is_html" ATTR_OFFSET = "offset_reached" ATTR_OVERDUE_TODOS = "overdue_todos" ATTR_PHOTOS = "photos" ATTR_REMINDER = "reminder" ATTR_RESPONSE = "response" ATTR_RRULE = "rrule" ATTR_SENDER = "sender" ATTR_SEND_RESPONSE = "send_response" ATTR_SENSITIVITY = "sensitivity" ATTR_SHOW_AS = "show_as" ATTR_START = "start" ATTR_STATE = "state" ATTR_STATUS = "status" ATTR_SUBJECT = "subject" ATTR_SUMMARY = "summary" ATTR_TODOS = "todos" ATTR_TODO_ID = "todo_id" ATTR_TOPIC = "topic" ATTR_TYPE = "type" ATTR_ZIP_ATTACHMENTS = "zip_attachments" ATTR_ZIP_NAME = "zip_name" AUTH_CALLBACK_NAME = "api:o365" AUTH_CALLBACK_PATH_ALT = "/api/o365" AUTH_CALLBACK_PATH_DEFAULT = ( "https://login.microsoftonline.com/common/oauth2/nativeclient" ) CALENDAR_ENTITY_ID_FORMAT = "calendar.{}" CONF_ACCOUNT = "account" CONF_ACCOUNTS = "accounts" CONF_ACCOUNT_CONF = "account_conf" CONF_ACCOUNT_NAME = "account_name" CONF_ALT_AUTH_METHOD = "alt_auth_method" CONF_AUTH_URL = "auth_url" CONF_AUTO_REPLY_SENSORS = "auto_reply_sensors" CONF_BASIC_CALENDAR = "basic_calendar" CONF_BODY_CONTAINS = "body_contains" CONF_CAL_ID = "cal_id" CONF_CAL_IDS = "cal_ids" CONF_CHAT_SENSORS = "chat_sensors" CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" # nosec CONF_CONFIG_TYPE = "config_type" CONF_COORDINATOR_EMAIL = "coordinator_email" CONF_COORDINATOR_SENSORS = "coordinator_sensors" CONF_DEVICE_ID = "device_id" CONF_DOWNLOAD_ATTACHMENTS = "download_attachments" CONF_DUE_HOURS_BACKWARD_TO_GET = "due_start_offset" CONF_DUE_HOURS_FORWARD_TO_GET = "due_end_offset" CONF_EMAIL_ACCOUNT = "email_account" CONF_EMAIL_SENSORS = "email_sensor" CONF_ENABLE_CALENDAR = "enable_calendar" 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" CONF_HAS_ATTACHMENT = "has_attachment" CONF_HOURS_BACKWARD_TO_GET = "start_offset" CONF_HOURS_FORWARD_TO_GET = "end_offset" CONF_HTML_BODY = "html_body" CONF_SHOW_BODY = "show_body" CONF_IMPORTANCE = "importance" CONF_IS_AUTHENTICATED = "is_authenticated" CONF_IS_UNREAD = "is_unread" CONF_KEYS_EMAIL = "keys_email" CONF_KEYS_SENSORS = "keys_sensors" 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_O365_TASK_FOLDER = "O365_task_folder" CONF_TODO_SENSORS = "todo_sensors" CONF_TRACK = "track" CONF_TRACK_NEW_CALENDAR = "track_new_calendar" CONF_TRACK_NEW = "track_new" CONF_YAML_TASK_LIST_ID = "task_list_id" CONF_YAML_TASK_LIST = "yaml_task_list" CONF_URL = "url" CONST_CONFIG_TYPE_LIST = "list" CONST_GROUP = "group:" CONST_PRIMARY = "$o365-primary$" CONST_UTC_TIMEZONE = "UTC" CONTENT_TYPES = ["text", "html"] 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_TODO = "completed_todo" EVENT_DELETE_TODO = "delete_todo" EVENT_NEW_TODO = "new_todo" EVENT_UNCOMPLETED_TODO = "uncompleted_todo" EVENT_UPDATE_TODO = "update_todo" EVENT_CREATE_CALENDAR_EVENT = "create_calendar_event" EVENT_MODIFY_CALENDAR_EVENT = "modify_calendar_event" EVENT_MODIFY_CALENDAR_RECURRENCES = "modify_calendar_recurrences" EVENT_REMOVE_CALENDAR_EVENT = "remove_calendar_event" EVENT_REMOVE_CALENDAR_RECURRENCES = "remove_calendar_recurrences" EVENT_RESPOND_CALENDAR_EVENT = "respond_calendar_event" EVENT_SEND_CHAT_MESSAGE = "send_chat_message" EVENT_UPDATE_USER_STATUS = "update_user_status" EVENT_UPDATE_USER_PREFERRED_STATUS = "update_user_preferred_status" LEGACY_ACCOUNT_NAME = "converted" O365_STORAGE = "o365_storage" O365_STORAGE_TOKEN = ".O365-token-cache" PERM_CALENDARS_READ = "Calendars.Read" PERM_CALENDARS_READBASIC = "Calendars.ReadBasic" PERM_CALENDARS_READWRITE = "Calendars.ReadWrite" PERM_CHAT_READ = "Chat.Read" PERM_CHAT_READWRITE = "Chat.ReadWrite" PERM_GROUP_READ_ALL = "Group.Read.All" PERM_GROUP_READWRITE_ALL = "Group.ReadWrite.All" PERM_MAILBOX_SETTINGS = "MailboxSettings.ReadWrite" PERM_MAIL_READ = "Mail.Read" PERM_MAIL_SEND = "Mail.Send" PERM_PRESENCE_READ = "Presence.Read" PERM_PRESENCE_READ_ALL = "Presence.Read.All" PERM_PRESENCE_READWRITE = "Presence.ReadWrite" PERM_TASKS_READ = "Tasks.Read" PERM_TASKS_READWRITE = "Tasks.ReadWrite" PERM_USER_READ = "User.Read" PERM_USER_READBASIC_ALL = "User.ReadBasic.All" PERM_SHARED = ".Shared" PERM_BASE_PERMISSIONS = [PERM_USER_READ] SENSOR_AUTO_REPLY = "auto_reply" SENSOR_EMAIL = "inbox" SENSOR_TEAMS_STATUS = "teams_status" SENSOR_TEAMS_CHAT = "teams_chat" TODO_TODO = "todo" TOKEN_FILENAME = "o365{0}.token" # nosec TOKEN_FILE_CORRUPTED = "corrupted" TOKEN_FILE_MISSING = "missing" TOKEN_FILE_OUTDATED = "outdated" TOKEN_FILE_PERMISSIONS = "permissions" YAML_CALENDARS_FILENAME = "{0}_calendars{1}.yaml" YAML_TASK_LISTS_FILENAME = "{0}_tasks{1}.yaml" DAYS = { "MO": "monday", "TU": "tuesday", "WE": "wednesday", "TH": "thursday", "FR": "friday", "SA": "saturday", "SU": "sunday", } INDEXES = { "+1": "first", "+2": "second", "+3": "third", "+4": "fourth", "-1": "last", } ================================================ FILE: custom_components/o365/helpers/__init__.py ================================================ """Initialise.""" ================================================ FILE: custom_components/o365/helpers/coordinator.py ================================================ """Sensor processing.""" import functools as ft import logging from datetime import datetime, timedelta from homeassistant.const import CONF_EMAIL, 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 as dt_util from requests.exceptions import HTTPError from O365.utils.query import ( # pylint: disable=no-name-in-module, import-error # pylint: disable=no-name-in-module, import-error QueryBuilder, ) from ..classes.mailsensor import async_build_inbox_query, async_build_query_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_TODOS, ATTR_TOPIC, CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, CONF_DOWNLOAD_ATTACHMENTS, CONF_EMAIL_ACCOUNT, CONF_EMAIL_SENSORS, CONF_ENABLE_UPDATE, CONF_ENTITY_KEY, CONF_ENTITY_TYPE, CONF_MAIL_FOLDER, CONF_MAX_ITEMS, CONF_O365_MAIL_FOLDER, CONF_O365_TASK_FOLDER, CONF_QUERY, CONF_QUERY_SENSORS, CONF_SENSOR_CONF, CONF_STATUS_SENSORS, CONF_TODO_SENSORS, CONF_TRACK, CONF_YAML_TASK_LIST, CONF_YAML_TASK_LIST_ID, DOMAIN, ENTITY_ID_FORMAT_SENSOR, ENTITY_ID_FORMAT_TODO, LEGACY_ACCOUNT_NAME, SENSOR_AUTO_REPLY, SENSOR_EMAIL, SENSOR_TEAMS_CHAT, SENSOR_TEAMS_STATUS, TODO_TODO, YAML_TASK_LISTS_FILENAME, ) from ..schema import YAML_TASK_LIST_SCHEMA from ..todo import O365TodoEntityServices, async_build_todo_query 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_util.get_default_time_zone() ) self._chat_members = {} self._ent_reg = entity_registry.async_get(hass) self._builder = QueryBuilder(protocol=self._account.protocol) async def async_setup_entries(self): """Do the initial setup of the entities.""" status_keys = await self._async_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 = chat_keys + status_keys + todo_keys + auto_reply_entities return self._keys async def _async_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: _build_entity_id( self.hass, ENTITY_ID_FORMAT_SENSOR, name ), CONF_UNIQUE_ID: f"{name}_{self._account_name}", CONF_NAME: name, CONF_ENTITY_TYPE: SENSOR_TEAMS_STATUS, CONF_EMAIL: sensor_conf.get(CONF_EMAIL), } if sensor_conf.get(CONF_EMAIL): email_account = await self.hass.async_add_executor_job( self._account.directory().get_user, sensor_conf.get(CONF_EMAIL), ) new_key[CONF_EMAIL_ACCOUNT] = email_account.object_id 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: _build_entity_id( self.hass, ENTITY_ID_FORMAT_SENSOR, 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 = O365TodoEntityServices(self.hass) await sensor_services.async_scan_for_todo_lists(None) yaml_filename = build_yaml_filename(self._config, YAML_TASK_LISTS_FILENAME) yaml_filepath = build_config_file_path(self.hass, yaml_filename) o365_task_dict = await self.hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_YAML_TASK_LIST_ID, YAML_TASK_LIST_SCHEMA, ) o365_task_lists = list(o365_task_dict.values()) keys = await self._async_todo_entities(o365_task_lists) return keys async def _async_todo_entities(self, o365_task_lists): keys = [] o365_tasks = await self.hass.async_add_executor_job(self._account.tasks) for o365_tasklist in o365_task_lists: track = o365_tasklist.get(CONF_TRACK) if not track: continue o365_task_list_id = o365_tasklist.get(CONF_YAML_TASK_LIST_ID) if self._account_name != LEGACY_ACCOUNT_NAME: name = f"{o365_tasklist.get(CONF_NAME)} {self._account_name}" else: name = o365_tasklist.get(CONF_NAME) try: o365_task = await self.hass.async_add_executor_job( # pylint: disable=no-member ft.partial( o365_tasks.get_folder, folder_id=o365_task_list_id, ) ) unique_id = f"{o365_task_list_id}_{self._account_name}" new_key = { CONF_ENTITY_KEY: _build_entity_id( self.hass, ENTITY_ID_FORMAT_TODO, name ), CONF_UNIQUE_ID: unique_id, CONF_O365_TASK_FOLDER: o365_task, CONF_NAME: name, CONF_YAML_TASK_LIST: o365_tasklist, CONF_ENTITY_TYPE: TODO_TODO, } keys.append(new_key) # To be deleted in mid 2024 after majority have migrated # to HA 2023.11 and O365 version 4.5 await _async_delete_redundant_sensors(self._ent_reg, unique_id) except HTTPError: _LOGGER.warning( "O365 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: _build_entity_id( self.hass, ENTITY_ID_FORMAT_SENSOR, 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_update_data(self): _LOGGER.debug( "Doing %s sensor update(s) for: %s", len(self._keys), self._account_name ) for key in self._keys: entity_type = key[CONF_ENTITY_TYPE] _LOGGER.debug("%s for: %s", entity_type, self._account_name) if entity_type == TODO_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_teams_status_update(self, key): """Update state.""" entity_key = key[CONF_ENTITY_KEY] email_account = key.get(CONF_EMAIL_ACCOUNT) if not email_account: if data := await self.hass.async_add_executor_job( self._account.teams().get_my_presence ): self._data[entity_key] = {ATTR_STATE: data.activity} return if data := await self.hass.async_add_executor_job( self._account.teams().get_user_presence, email_account ): 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 = [] for member in members: if member.display_name: memberlist.append(member.display_name) elif member.email: memberlist.append(member.email) else: memberlist.append("Name Unknown") 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_TODOS: {}, ATTR_STATE: 0} error = False data, error = await self._async_todos_update_query(key, error) if not error: self._data[entity_key][ATTR_DATA] = await self.hass.async_add_executor_job( list, data ) self._data[entity_key][ATTR_ERROR] = error async def _async_todos_update_query(self, key, error): data = None o365_task = key[CONF_O365_TASK_FOLDER] full_query = await async_build_todo_query(self._builder, key) name = key[CONF_NAME] try: data = await self.hass.async_add_executor_job( # pylint: disable=no-member ft.partial(o365_task.get_tasks, batch=100, query=full_query) ) if error: _LOGGER.info("O365 Task list reconnected for: %s", name) error = False except HTTPError: if not error: _LOGGER.error( "O365 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, } class O365EmailCordinator(DataUpdateCoordinator): """O365 email data update coordinator.""" def __init__(self, hass, config): """Initialize my coordinator.""" super().__init__( hass, _LOGGER, # Name of the data. For logging purposes. name="O365 Email", # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(seconds=30), ) self._hass = hass 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_util.get_default_time_zone() ) self._chat_members = {} self._ent_reg = entity_registry.async_get(hass) self._builder = QueryBuilder(protocol=self._account.protocol) 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() self._keys = email_keys + query_keys return self._keys async def _async_email_sensors(self): email_sensors = self._config.get(CONF_EMAIL_SENSORS, []) keys = [] for sensor_conf in email_sensors: name = sensor_conf[CONF_NAME] if mail_folder := await self._async_get_mail_folder( sensor_conf, CONF_EMAIL_SENSORS ): new_key = { CONF_ENTITY_KEY: _build_entity_id( self.hass, ENTITY_ID_FORMAT_SENSOR, name ), CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}_{name}", CONF_SENSOR_CONF: sensor_conf, CONF_O365_MAIL_FOLDER: mail_folder, CONF_NAME: name, CONF_ENTITY_TYPE: SENSOR_EMAIL, CONF_QUERY: await async_build_inbox_query( sensor_conf, self._builder ), } # Renames unique id to ensure uniqueness - To be deleted in early 2025 entity = self._ent_reg.async_get(new_key[CONF_ENTITY_KEY]) if ( entity and entity.unique_id == f"{mail_folder.folder_id}_{self._account_name}" ): self._ent_reg.async_update_entity( new_key[CONF_ENTITY_KEY], new_unique_id=new_key[CONF_UNIQUE_ID] ) 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: _build_entity_id( self.hass, ENTITY_ID_FORMAT_SENSOR, name ), CONF_UNIQUE_ID: f"{mail_folder.folder_id}_{self._account_name}_{name}", CONF_SENSOR_CONF: sensor_conf, CONF_O365_MAIL_FOLDER: mail_folder, CONF_NAME: name, CONF_ENTITY_TYPE: SENSOR_EMAIL, CONF_QUERY: await async_build_query_query( sensor_conf, self._builder ), } # Renames unique id to ensure uniqueness - To be deleted in early 2025 entity = self._ent_reg.async_get(new_key[CONF_ENTITY_KEY]) if ( entity and entity.unique_id == f"{mail_folder.folder_id}_{self._account_name}" ): self._ent_reg.async_update_entity( new_key[CONF_ENTITY_KEY], new_unique_id=new_key[CONF_UNIQUE_ID] ) keys.append(new_key) return keys async def _async_get_mail_folder(self, sensor_conf, sensor_type): """Get the configured folder.""" mailbox = await self.hass.async_add_executor_job(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 await self.hass.async_add_executor_job(mailbox.inbox_folder) async def _async_get_configured_mail_folder( self, mail_folder_conf, mailbox, sensor_type ): mail_folder = mailbox _LOGGER.debug("Get folder %s - start", mail_folder_conf) 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, ) ) _LOGGER.debug("Get folder %s - process - %s", mail_folder_conf, mail_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 _LOGGER.debug("Get folder %s - finish ", mail_folder_conf) return mail_folder async def _async_update_data(self): _LOGGER.debug( "Doing %s email update(s) for: %s", len(self._keys), self._account_name ) for key in self._keys: await self._async_email_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: await self.hass.async_add_executor_job(list, data) } def _build_entity_id(hass, entity_id_format, name): """Build and entity ID.""" return async_generate_entity_id( entity_id_format, name, hass=hass, ) async def _async_delete_redundant_sensors(ent_reg, unique_id): if entity_id := ent_reg.async_get_entity_id("sensor", DOMAIN, unique_id): ent_reg.async_remove(entity_id) ================================================ FILE: custom_components/o365/helpers/migration.py ================================================ """Migration services.""" import logging from enum import StrEnum from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EMAIL, CONF_ENABLED, CONF_NAME from homeassistant.core import HomeAssistant from ..const import ( CONF_ACCOUNT_NAME, CONF_ACCOUNTS, CONF_ALT_AUTH_METHOD, CONF_AUTO_REPLY_SENSORS, CONF_BASIC_CALENDAR, CONF_BODY_CONTAINS, CONF_CAL_ID, CONF_CHAT_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOWNLOAD_ATTACHMENTS, CONF_EMAIL_SENSORS, CONF_ENABLE_CALENDAR, CONF_ENABLE_UPDATE, CONF_GROUPS, CONF_HAS_ATTACHMENT, CONF_HTML_BODY, CONF_IMPORTANCE, CONF_IS_UNREAD, CONF_MAIL_FROM, CONF_MAX_ITEMS, CONF_QUERY_SENSORS, CONF_SHARED_MAILBOX, CONF_SHOW_BODY, CONF_STATUS_SENSORS, CONF_SUBJECT_CONTAINS, CONF_SUBJECT_IS, CONF_TODO_SENSORS, CONF_TRACK_NEW, CONF_TRACK_NEW_CALENDAR, CONF_YAML_TASK_LIST_ID, CONST_CONFIG_TYPE_LIST, CONST_PRIMARY, YAML_CALENDARS_FILENAME, YAML_TASK_LISTS_FILENAME, ) from ..schema import YAML_CALENDAR_DEVICE_SCHEMA, YAML_TASK_LIST_SCHEMA from ..utils.filemgmt import build_config_file_path, build_yaml_filename, load_yaml_file from ..utils.utils import build_account_config CONF_ALTERNATE_EMAIL = "alternate_email" CONF_CHAT_ENABLE = "chat_enable" CONF_ENABLE_AUTOREPLY = "enable_autoreply" CONF_ENTITY_NAME = "entity_name" CONF_FOLDER = "folder" CONF_STATUS_ENABLE = "status_enable" CONF_YAML_TODO_LIST_ID = "todo_list_id" _LOGGER = logging.getLogger(__name__) class Unread(StrEnum): """Mail Unread.""" TRUE = "Unread Only" FALSE = "Read Only" NONE = "All" class Attachment(StrEnum): """Mail Attachment.""" TRUE = "Has attachment" FALSE = "Does not have attachment" NONE = "All" class ImportanceLevel(StrEnum): """Mail Importance Level.""" NORMAL = "Normal" LOW = "Low" HIGH = "High" NONE = "All" class EnableOptions(StrEnum): """Teams sensors enablement.""" DISABLED = "Disabled" READ = "Read" UPDATE = "Update" class MigrationServices: """Migration Services.""" def __init__(self, hass: HomeAssistant, config): """Initialise the migration services.""" self._hass = hass self._config = config self._auto_reply_sensors = [] async def async_migrate_config(self, call): # pylint: disable=unused-argument """Service to migrate config entries.""" for account in self._config[CONF_ACCOUNTS]: account_config = build_account_config( account, None, None, CONST_CONFIG_TYPE_LIST, None ) account_config[CONF_ALT_AUTH_METHOD] = account[CONF_ALT_AUTH_METHOD] account_config[CONF_CLIENT_SECRET] = account[CONF_CLIENT_SECRET] account_config[CONF_BASIC_CALENDAR] = account[CONF_BASIC_CALENDAR] account_config[CONF_GROUPS] = account[CONF_GROUPS] await self._async_migrate_account(account_config) async def _async_migrate_account(self, config): base_config_entry = self._setup_base(config) await self._async_migrate_calendar(config, base_config_entry) await self._async_migrate_mail(config, base_config_entry) await self._async_migrate_teams(config, base_config_entry) await self._async_migrate_todos(config, base_config_entry) async def _async_migrate_calendar(self, config, base_config_entry): if not config.get(CONF_ENABLE_CALENDAR, True): return migrate_domain = "ms365_calendar" if not self._integration_installed(migrate_domain): return entry = {} entry[CONF_ENTITY_NAME] = config.get(CONF_ACCOUNT_NAME, CONST_PRIMARY) self._add_attribute(config, entry, CONF_ENABLE_UPDATE) self._add_attribute(config, entry, CONF_BASIC_CALENDAR) self._add_attribute(config, entry, CONF_GROUPS) self._add_attribute(config, entry, CONF_SHARED_MAILBOX) full_entry = base_config_entry | entry options = {} self._add_attribute(config, options, CONF_TRACK_NEW_CALENDAR) yaml_filename = build_yaml_filename(config, YAML_CALENDARS_FILENAME) yaml_filepath = build_config_file_path(self._hass, yaml_filename) calendars = await self._hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_CAL_ID, YAML_CALENDAR_DEVICE_SCHEMA ) await self._async_create_entry( migrate_domain, full_entry, options, calendars=calendars ) async def _async_migrate_mail(self, config, base_config_entry): email_sensors = config.get(CONF_EMAIL_SENSORS, []) query_sensors = config.get(CONF_QUERY_SENSORS, []) self._auto_reply_sensors = config.get(CONF_AUTO_REPLY_SENSORS, []) if not email_sensors and not query_sensors and not self._auto_reply_sensors: return migrate_domain = "ms365_mail" if not self._integration_installed(migrate_domain): return await self._async_mail_sensors( config, migrate_domain, base_config_entry, email_sensors ) await self._async_mail_sensors( config, migrate_domain, base_config_entry, query_sensors ) for sensor in self._auto_reply_sensors: entry = {} entry[CONF_ENTITY_NAME] = sensor[CONF_NAME] full_entry = base_config_entry | entry options = {} await self._async_create_entry(migrate_domain, full_entry, options) async def _async_migrate_teams(self, config, base_config_entry): chat_sensors = config.get(CONF_CHAT_SENSORS, []) status_sensors = config.get(CONF_STATUS_SENSORS, []) if not chat_sensors and not status_sensors: return migrate_domain = "ms365_teams" if not self._integration_installed(migrate_domain): return entry = {} entry[CONF_ENTITY_NAME] = config.get(CONF_ACCOUNT_NAME, CONST_PRIMARY) entry[CONF_CHAT_ENABLE] = EnableOptions.DISABLED entry[CONF_STATUS_ENABLE] = EnableOptions.DISABLED for chat_sensor in chat_sensors: enable_update = chat_sensor.get(CONF_ENABLE_UPDATE, False) if enable_update: entry[CONF_CHAT_ENABLE] = EnableOptions.UPDATE else: entry[CONF_CHAT_ENABLE] = EnableOptions.READ for status_sensor in status_sensors: email = status_sensor.get(CONF_EMAIL, None) if email: await self._async_create_alternate_email_status( migrate_domain, base_config_entry, status_sensor ) continue enable_update = status_sensor.get(CONF_ENABLE_UPDATE, False) if enable_update: entry[CONF_STATUS_ENABLE] = EnableOptions.UPDATE else: entry[CONF_STATUS_ENABLE] = EnableOptions.READ if ( entry[CONF_CHAT_ENABLE] == EnableOptions.DISABLED and entry[CONF_STATUS_ENABLE] == EnableOptions.DISABLED ): return full_entry = base_config_entry | entry options = {} await self._async_create_entry(migrate_domain, full_entry, options) async def _async_migrate_todos(self, config, base_config_entry): todo_sensors = config.get(CONF_TODO_SENSORS, {}) if not todo_sensors or not todo_sensors.get(CONF_ENABLED, False): return migrate_domain = "ms365_todo" if not self._integration_installed(migrate_domain): return entry = {} entry[CONF_ENTITY_NAME] = config.get(CONF_ACCOUNT_NAME, CONST_PRIMARY) self._add_attribute(todo_sensors, entry, CONF_ENABLE_UPDATE) full_entry = base_config_entry | entry options = {} self._add_attribute(todo_sensors, options, CONF_TRACK_NEW) yaml_filename = build_yaml_filename(config, YAML_TASK_LISTS_FILENAME) yaml_filepath = build_config_file_path(self._hass, yaml_filename) todos = await self._hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_YAML_TASK_LIST_ID, YAML_TASK_LIST_SCHEMA ) for value in todos.values(): value["todo_list_id"] = value[CONF_YAML_TASK_LIST_ID] del value[CONF_YAML_TASK_LIST_ID] await self._async_create_entry(migrate_domain, full_entry, options, todos=todos) async def _async_create_alternate_email_status( self, migrate_domain, base_config_entry, status_sensor ): entry = {} entry[CONF_ENTITY_NAME] = status_sensor[CONF_NAME] entry[CONF_ALTERNATE_EMAIL] = status_sensor.get(CONF_EMAIL, None) enable_update = status_sensor.get(CONF_ENABLE_UPDATE, False) if enable_update: entry[CONF_STATUS_ENABLE] = EnableOptions.UPDATE else: entry[CONF_STATUS_ENABLE] = EnableOptions.READ entry[CONF_CHAT_ENABLE] = EnableOptions.DISABLED full_entry = base_config_entry | entry options = {} await self._async_create_entry(migrate_domain, full_entry, options) async def _async_mail_sensors( self, config, migrate_domain, base_config_entry, mail_sensors, ): for sensor in mail_sensors: entry = {} entry[CONF_ENTITY_NAME] = sensor[CONF_NAME] self._add_attribute(config, entry, CONF_ENABLE_UPDATE) self._add_attribute(config, entry, CONF_SHARED_MAILBOX) if self._auto_reply_sensors: entry[CONF_ENABLE_AUTOREPLY] = True self._auto_reply_sensors = [] else: entry[CONF_ENABLE_AUTOREPLY] = False full_entry = base_config_entry | entry options = {} self._add_attribute(sensor, options, CONF_FOLDER) self._add_attribute(sensor, options, CONF_MAIL_FROM) self._add_attribute(sensor, options, CONF_MAX_ITEMS) attachments = sensor.get(CONF_HAS_ATTACHMENT, None) if attachments is True: options[CONF_HAS_ATTACHMENT] = Attachment.TRUE elif attachments is False: options[CONF_HAS_ATTACHMENT] = Attachment.FALSE importance = sensor.get(CONF_IMPORTANCE, None) if importance == "low": options[CONF_IMPORTANCE] = ImportanceLevel.LOW elif importance == "normal": options[CONF_IMPORTANCE] = ImportanceLevel.NORMAL elif importance == "high": options[CONF_IMPORTANCE] = ImportanceLevel.HIGH is_unread = sensor.get(CONF_HAS_ATTACHMENT, None) if is_unread is True: options[CONF_IS_UNREAD] = Unread.TRUE elif is_unread is False: options[CONF_IS_UNREAD] = Unread.FALSE self._add_attribute(sensor, options, CONF_BODY_CONTAINS) self._add_attribute(sensor, options, CONF_SUBJECT_CONTAINS) self._add_attribute(sensor, options, CONF_SUBJECT_IS) self._add_attribute(sensor, options, CONF_DOWNLOAD_ATTACHMENTS) self._add_attribute(sensor, options, CONF_HTML_BODY) self._add_attribute(sensor, options, CONF_SHOW_BODY) await self._async_create_entry(migrate_domain, full_entry, options) async def _async_create_entry( self, migrate_domain, data, options, calendars=None, todos=None ): entry = {"data": data, "options": options} if calendars: entry["calendars"] = calendars if todos: entry["todos"] = todos await self._hass.config_entries.flow.async_init( migrate_domain, context={"source": SOURCE_IMPORT}, data=entry, ) def _integration_installed(self, migrate_domain): installed = migrate_domain in self._hass.data["custom_components"] if not installed: _LOGGER.warning( "%s not installed. Install via HACS and try again.", migrate_domain ) return installed def _setup_base(self, config): entry = { CONF_CLIENT_ID: config.get(CONF_CLIENT_ID), CONF_CLIENT_SECRET: config.get(CONF_CLIENT_SECRET), } self._add_attribute(config, entry, CONF_ALT_AUTH_METHOD) return entry def _add_attribute(self, config, entry, attribute_name): attribute = config.get(attribute_name) if attribute is not None: entry[attribute_name] = attribute ================================================ FILE: custom_components/o365/helpers/setup.py ================================================ """Do configuration setup.""" import asyncio import logging from homeassistant.const import CONF_ENABLED from homeassistant.helpers import discovery from ..const import ( CONF_ACCOUNT_NAME, CONF_CHAT_SENSORS, CONF_COORDINATOR_EMAIL, CONF_COORDINATOR_SENSORS, CONF_EMAIL_SENSORS, CONF_ENABLE_CALENDAR, CONF_ENABLE_UPDATE, CONF_KEYS_EMAIL, CONF_KEYS_SENSORS, CONF_QUERY_SENSORS, CONF_STATUS_SENSORS, CONF_TODO_SENSORS, DOMAIN, ) from ..utils.utils import build_account_config from .coordinator import O365EmailCordinator, O365SensorCordinator _LOGGER = logging.getLogger(__name__) async def do_setup( hass, config, account, is_authenticated, account_name, conf_type, perms ): """Run the setup after we have everything configured.""" account_config = build_account_config( config, account, is_authenticated, conf_type, perms ) if DOMAIN not in hass.data: hass.data[DOMAIN] = {} hass.data[DOMAIN][account_name] = account_config async with asyncio.TaskGroup() as group: sensors_result = group.create_task(_async_sensor_setup(hass, account_config)) email_result = group.create_task(_async_email_setup(hass, account_config)) sensor_keys = sensors_result.result()["keys"] sensor_coordinator = sensors_result.result()["coordinator"] email_keys = email_result.result()["keys"] email_coordinator = email_result.result()["coordinator"] hass.data[DOMAIN][account_name][CONF_KEYS_SENSORS] = sensor_keys hass.data[DOMAIN][account_name][CONF_COORDINATOR_SENSORS] = sensor_coordinator hass.data[DOMAIN][account_name][CONF_KEYS_EMAIL] = email_keys hass.data[DOMAIN][account_name][CONF_COORDINATOR_EMAIL] = email_coordinator _load_platforms(hass, account_name, config, account_config) async def _async_sensor_setup(hass, account_config): _LOGGER.debug("Sensor setup - start") sensor_coordinator = O365SensorCordinator(hass, account_config) sensor_keys = await sensor_coordinator.async_setup_entries() if sensor_keys: await sensor_coordinator.async_config_entry_first_refresh() _LOGGER.debug("Sensor setup - finish") return {"coordinator": sensor_coordinator, "keys": sensor_keys} async def _async_email_setup(hass, account_config): _LOGGER.debug("Email setup - start") email_coordinator = O365EmailCordinator(hass, account_config) email_keys = await email_coordinator.async_setup_entries() if email_keys: await email_coordinator.async_config_entry_first_refresh() _LOGGER.debug("Email setup - finish") return {"coordinator": email_coordinator, "keys": email_keys} def _load_platforms(hass, account_name, config, account_config): if account_config[CONF_ENABLE_CALENDAR]: hass.async_create_task( discovery.async_load_platform( hass, "calendar", DOMAIN, {CONF_ACCOUNT_NAME: account_name}, config ) ) if account_config[CONF_ENABLE_UPDATE]: hass.async_create_task( discovery.async_load_platform( hass, "notify", DOMAIN, {CONF_ACCOUNT_NAME: account_name}, config ) ) if ( 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( hass, "sensor", DOMAIN, {CONF_ACCOUNT_NAME: account_name}, config ) ) if len(account_config[CONF_TODO_SENSORS]) > 0 and account_config[ CONF_TODO_SENSORS ].get(CONF_ENABLED, False): hass.async_create_task( discovery.async_load_platform( hass, "todo", DOMAIN, {CONF_ACCOUNT_NAME: account_name}, config ) ) ================================================ FILE: custom_components/o365/icons.json ================================================ { "services": { "scan_for_calendars": "mdi:calendar-sync", "scan_for_todo_lists": "mdi:clipboard-list", "respond_calendar_event": "mdi:calendar-arrow-left", "create_calendar_event": "mdi:calendar-plus", "modify_calendar_event": "mdi:calendar-edit", "remove_calendar_event": "mdi:calendar-remove", "new_todo": "mdi:clipboard-list", "update_todo": "mdi:clipboard-list", "delete_todo": "mdi:clipboard-list", "complete_todo": "mdi:clipboard-list", "auto_reply_enable": "mdi:microsoft-outlook", "auto_reply_disable": "mdi:microsoft-outlook", "send_chat_message": "mdi:microsoft-teams", "update_user_status": "mdi:microsoft-teams", "update_user_preferred_status": "mdi:microsoft-teams" }, "entity": { "sensor": { "auto_reply": { "default": "mdi:reply-all", "state": { "disabled": "mdi:reply-outline", "scheduled": "mdi:reply" } }, "mail": { "default": "mdi:microsoft-outlook" }, "teams": { "default": "mdi:microsoft-teams" } } } } ================================================ FILE: custom_components/o365/manifest.json ================================================ { "domain": "o365", "name": "Office 365", "codeowners": [ "@RogerSelwyn" ], "dependencies": [ "configurator", "http" ], "documentation": "https://github.com/RogerSelwyn/O365-HomeAssistant", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/RogerSelwyn/O365-HomeAssistant/issues", "loggers": [ "custom_components.o365", "O365" ], "requirements": [ "O365>=2.1.4", "BeautifulSoup4>=4.10.0", "oauthlib" ], "version": "v5.3.5" } ================================================ FILE: custom_components/o365/notify.py ================================================ """Notification processing.""" import logging import os import zipfile from pathlib import Path from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, ATTR_TITLE, BaseNotificationService, ) from .const import ( ATTR_ATTACHMENTS, ATTR_IMPORTANCE, ATTR_MESSAGE_IS_HTML, ATTR_PHOTOS, ATTR_SENDER, ATTR_ZIP_ATTACHMENTS, ATTR_ZIP_NAME, CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_IS_AUTHENTICATED, CONF_PERMISSIONS, DOMAIN, LEGACY_ACCOUNT_NAME, PERM_MAIL_SEND, ) from .schema import NOTIFY_SERVICE_BASE_SCHEMA _LOGGER = logging.getLogger(__name__) async def async_get_service(hass, config, discovery_info=None): # pylint: disable=unused-argument """Get the service.""" if discovery_info is None: return account_name = discovery_info[CONF_ACCOUNT_NAME] conf = hass.data[DOMAIN][account_name] account = conf[CONF_ACCOUNT] is_authenticated = conf[CONF_IS_AUTHENTICATED] if is_authenticated and conf[CONF_PERMISSIONS].validate_authorization( PERM_MAIL_SEND ): _LOGGER.warning( "The O365 Notify service is now deprecated - please migrate to MS365 Mail " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) return O365EmailService(account, hass, conf) return class O365EmailService(BaseNotificationService): """Implement the notification service for O365.""" def __init__(self, account, hass, config): """Initialize the service.""" self.account = account self._config = config self._cleanup_files = [] self._hass = hass self._account_name = config.get(CONF_ACCOUNT_NAME, None) if self._account_name: if self._account_name == LEGACY_ACCOUNT_NAME: self._account_name = "" else: self._account_name = f"_{self._account_name}" @property def targets(self): """Targets property.""" return {f"_email{self._account_name}": ""} def send_message(self, message="", **kwargs): """Send a message to a user.""" _LOGGER.warning("Non async send_message unsupported") async def async_send_message(self, message="", **kwargs): """Send an async message to a user.""" if not self._config[CONF_PERMISSIONS].validate_authorization(PERM_MAIL_SEND): _LOGGER.error( "Not authorised to send mail - requires permission: %s", PERM_MAIL_SEND ) return self._cleanup_files = [] data = kwargs.get(ATTR_DATA) if data is None: kwargs.pop(ATTR_DATA) NOTIFY_SERVICE_BASE_SCHEMA(kwargs) title = kwargs.get(ATTR_TITLE, "Notification from Home Assistant") if data and data.get(ATTR_TARGET, None): target = data.get(ATTR_TARGET) else: resp = await self.hass.async_add_executor_job(self.account.get_current_user) target = resp.mail new_message = await self.hass.async_add_executor_job(self.account.new_message) message = self._build_message(data, message, new_message.attachments) self._build_attachments(data, new_message.attachments) new_message.to.add(target) if data: if data.get(ATTR_SENDER, None): new_message.sender = data.get(ATTR_SENDER) if data.get(ATTR_IMPORTANCE, None): new_message.importance = data.get(ATTR_IMPORTANCE) new_message.subject = title new_message.body = message await self.hass.async_add_executor_job(new_message.send) self._cleanup() def _build_message(self, data, message, new_message_attachments): is_html = False photos = [] if data: is_html = data.get(ATTR_MESSAGE_IS_HTML, False) photos = data.get(ATTR_PHOTOS, []) if is_html or photos: message = f""" {message}""" message += self._build_photo_content(photos, new_message_attachments) message += "" return message def _build_photo_content(self, photos, new_message_attachments): if isinstance(photos, str): photos = [photos] photos_content = "" for i, photo in enumerate(photos, start=1): if photo.startswith("http"): photos_content += f'
' else: photo = self._get_ha_filepath(photo) new_message_attachments.add(photo) att = new_message_attachments[-1] att.is_inline = True att.content_id = str(i) photos_content += f'
' return photos_content def _build_attachments(self, data, new_message_attachments): attachments = [] zip_attachments = False zip_name = None if data: attachments = data.get(ATTR_ATTACHMENTS, []) zip_attachments = data.get(ATTR_ZIP_ATTACHMENTS, False) zip_name = data.get(ATTR_ZIP_NAME, None) attachments = [self._get_ha_filepath(x) for x in attachments] if attachments and zip_attachments: z_file = zip_files(attachments, zip_name) new_message_attachments.add(z_file) self._cleanup_files.append(z_file) else: for attachment in attachments: new_message_attachments.add(attachment) def _cleanup(self): for filename in self._cleanup_files: os.remove(filename) def _get_ha_filepath(self, filepath): """Get the file path.""" _filepath = Path(filepath) if _filepath.parts[0] == "/" and _filepath.parts[1] == "config": _filepath = os.path.join(self._hass.config.config_dir, *_filepath.parts[2:]) if not os.path.isfile(_filepath): if not os.path.isfile(filepath): raise ValueError(f"Could not access file {filepath} at {_filepath}") return filepath return _filepath def zip_files(filespaths, zip_name): """Zip the files.""" if not zip_name: zip_name = "archive.zip" if Path(zip_name).suffix != ".zip": zip_name += ".zip" with zipfile.ZipFile(zip_name, mode="w") as zip_file: for file_path in filespaths: zip_file.write(file_path, os.path.basename(file_path)) return zip_name ================================================ FILE: custom_components/o365/repairs.py ================================================ """Repair flows.""" from __future__ import annotations import functools as ft import logging import voluptuous as vol from aiohttp import web_response from homeassistant import data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.repairs import RepairsFlow # ConfirmRepairFlow, from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.network import get_url from .classes.permissions import Permissions from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH_ALT, AUTH_CALLBACK_PATH_DEFAULT, CONF_ACCOUNT, CONF_ACCOUNT_CONF, CONF_ACCOUNT_NAME, CONF_ALT_AUTH_METHOD, CONF_AUTH_URL, CONF_CONFIG_TYPE, CONF_FAILED_PERMISSIONS, CONF_URL, TOKEN_FILE_CORRUPTED, TOKEN_FILE_MISSING, ) from .helpers.setup import do_setup from .schema import REQUEST_AUTHORIZATION_DEFAULT_SCHEMA _LOGGER = logging.getLogger(__name__) class AuthorizationRepairFlow(RepairsFlow): """Handler for an issue fixing flow.""" def __init__( self, hass, data, ): """Initialise the repair flow.""" self._data = data self._conf = data.get(CONF_ACCOUNT_CONF) self._account = data.get(CONF_ACCOUNT) self._failed_permissions = data.get(CONF_FAILED_PERMISSIONS) self._conf_type = data.get(CONF_CONFIG_TYPE) self._alt_config = self._conf.get(CONF_ALT_AUTH_METHOD) self._account_name = self._conf.get(CONF_ACCOUNT_NAME) self._callback_url = get_callback_url(hass, self._alt_config) self._permissions = Permissions(hass, self._conf, self._conf_type) self._scope = self._permissions.requested_permissions self._flow = None self._url = None self._callback_view = None async def async_step_init( self, user_input: dict[str, str] | None = None, # pylint: disable=unused-argument ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" self._url, self._flow = await self.hass.async_add_executor_job( ft.partial( self._account.con.get_authorization_url, requested_scopes=self._scope, redirect_uri=self._callback_url, ) ) if self._alt_config: return await self.async_step_request_alt() return await self.async_step_request_default() async def async_step_request_default( self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" errors = {} _LOGGER.debug("Token file: %s", self._account.con.token_backend) if user_input is not None: errors = await self._async_validate_response(user_input) if not errors: return self.async_create_entry(title="", data={}) failed_permissions = None if self._failed_permissions: failed_permissions = f"\n\n {', '.join(self._failed_permissions)}" return self.async_show_form( step_id="request_default", data_schema=vol.Schema(REQUEST_AUTHORIZATION_DEFAULT_SCHEMA), description_placeholders={ CONF_AUTH_URL: self._url, CONF_ACCOUNT_NAME: self._account_name, CONF_FAILED_PERMISSIONS: failed_permissions, }, errors=errors, ) async def async_step_request_alt( self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" errors = {} if user_input is not None: errors = await self._async_validate_response(user_input) if not errors: return self.async_create_entry(title="", data={}) if not self._callback_view: self._callback_view = O365AuthCallbackView() self.hass.http.register_view(self._callback_view) failed_permissions = None if self._failed_permissions: failed_permissions = f"\n\nMissing - {', '.join(self._failed_permissions)}" return self.async_show_form( step_id="request_alt", description_placeholders={ CONF_AUTH_URL: self._url, CONF_ACCOUNT_NAME: self._account_name, CONF_FAILED_PERMISSIONS: failed_permissions, }, errors=errors, ) async def _async_validate_response(self, user_input): errors = {} url = ( self._callback_view.token_url if self._alt_config else user_input[CONF_URL] ) if url[:5].lower() == "http:": url = f"https:{url[5:]}" if "code" not in url: errors[CONF_URL] = "invalid_url" return errors result = await self.hass.async_add_executor_job( ft.partial( self._account.con.request_token, url, flow=self._flow, redirect_uri=self._callback_url, ) ) if result is not True: _LOGGER.error("Token file error - check log for errors from O365") errors[CONF_URL] = "token_file_error" return errors ( permissions, self._failed_permissions, ) = await self._permissions.async_check_authorizations() if permissions == TOKEN_FILE_MISSING: errors[CONF_URL] = "missing_token_file" return errors if permissions == TOKEN_FILE_CORRUPTED: errors[CONF_URL] = "corrupted_token_file" return errors if not permissions: errors[CONF_URL] = "minimum_permissions" await do_setup( self.hass, self._conf, self._account, True, self._account_name, self._conf_type, self._permissions, ) return errors async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" if issue_id == "authorization": return AuthorizationRepairFlow(hass, data) class O365AuthCallbackView(HomeAssistantView): """O365 Authorization Callback View.""" requires_auth = False url = AUTH_CALLBACK_PATH_ALT name = AUTH_CALLBACK_NAME def __init__(self): """Initialize.""" self.token_url = None @callback async def get(self, request): """Receive authorization token.""" self.token_url = str(request.url) return web_response.Response( headers={"content-type": "text/html"}, text="This window can be closed", ) def get_callback_url(hass, alt_config): """Get the callback URL.""" if alt_config: return f"{get_url(hass, prefer_external=True)}{AUTH_CALLBACK_PATH_ALT}" return AUTH_CALLBACK_PATH_DEFAULT ================================================ FILE: custom_components/o365/schema.py ================================================ """Schema for O365 Integration.""" import datetime from collections.abc import Callable from itertools import groupby from typing import Any import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ) from homeassistant.const import CONF_EMAIL, CONF_ENABLED, CONF_NAME from homeassistant.util import dt as dt_util from O365.calendar import ( # pylint: disable=no-name-in-module AttendeeType, EventSensitivity, EventShowAs, ) from O365.mailbox import ( # pylint: disable=no-name-in-module, import-error ExternalAudience, ) from O365.teams import ( # pylint: disable=import-error, no-name-in-module Activity, Availability, PreferredAvailability, ) from O365.utils import ImportanceLevel # pylint: disable=no-name-in-module from .const import ( ATTR_ACTIVITY, ATTR_ATTACHMENTS, ATTR_ATTENDEES, ATTR_AVAILABILITY, ATTR_BODY, ATTR_CATEGORIES, ATTR_CHAT_ID, ATTR_COMPLETED, ATTR_CONTENT_TYPE, ATTR_DESCRIPTION, ATTR_DUE, ATTR_EMAIL, ATTR_END, ATTR_EVENT_ID, ATTR_EXPIRATIONDURATION, ATTR_EXTERNAL_AUDIENCE, ATTR_EXTERNALREPLY, ATTR_IMPORTANCE, ATTR_INTERNALREPLY, ATTR_IS_ALL_DAY, ATTR_LOCATION, ATTR_MESSAGE_IS_HTML, ATTR_PHOTOS, ATTR_REMINDER, ATTR_RESPONSE, ATTR_SEND_RESPONSE, ATTR_SENDER, ATTR_SENSITIVITY, ATTR_SHOW_AS, ATTR_START, ATTR_SUBJECT, ATTR_TODO_ID, ATTR_TYPE, ATTR_ZIP_ATTACHMENTS, ATTR_ZIP_NAME, CONF_ACCOUNT_NAME, CONF_ACCOUNTS, CONF_ALT_AUTH_METHOD, CONF_AUTO_REPLY_SENSORS, CONF_BASIC_CALENDAR, CONF_BODY_CONTAINS, CONF_CAL_ID, CONF_CHAT_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DEVICE_ID, CONF_DOWNLOAD_ATTACHMENTS, CONF_DUE_HOURS_BACKWARD_TO_GET, CONF_DUE_HOURS_FORWARD_TO_GET, CONF_EMAIL_SENSORS, CONF_ENABLE_CALENDAR, CONF_ENABLE_UPDATE, CONF_ENTITIES, CONF_EXCLUDE, CONF_GROUPS, CONF_HAS_ATTACHMENT, CONF_HOURS_BACKWARD_TO_GET, CONF_HOURS_FORWARD_TO_GET, CONF_HTML_BODY, CONF_IMPORTANCE, CONF_IS_UNREAD, CONF_MAIL_FOLDER, CONF_MAIL_FROM, CONF_MAX_ITEMS, CONF_MAX_RESULTS, CONF_QUERY_SENSORS, CONF_SEARCH, CONF_SHARED_MAILBOX, CONF_SHOW_BODY, CONF_SHOW_COMPLETED, CONF_STATUS_SENSORS, CONF_SUBJECT_CONTAINS, CONF_SUBJECT_IS, CONF_TODO_SENSORS, CONF_TRACK, CONF_TRACK_NEW, CONF_TRACK_NEW_CALENDAR, CONF_URL, CONF_YAML_TASK_LIST_ID, CONTENT_TYPES, EventResponse, ) def _has_consistent_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: """Verify that all datetime values have a consistent timezone.""" def validate(obj: dict[str, Any]) -> dict[str, Any]: """Test that all keys that are datetime values have the same timezone.""" tzinfos = [] for key in keys: if not (value := obj.get(key)) or not isinstance(value, datetime.datetime): return obj tzinfos.append(value.tzinfo) uniq_values = groupby(tzinfos) if len(list(uniq_values)) > 1: raise vol.Invalid("Expected all values to have the same timezone") return obj return validate def _as_local_timezone(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: """Convert all datetime values to the local timezone.""" def validate(obj: dict[str, Any]) -> dict[str, Any]: """Convert all keys that are datetime values to local timezone.""" for k in keys: if (value := obj.get(k)) and isinstance(value, datetime.datetime): obj[k] = dt_util.as_local(value) return obj return validate EMAIL_SENSOR = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_MAIL_FOLDER): cv.string, vol.Optional(CONF_MAX_ITEMS, default=5): int, vol.Optional(CONF_IS_UNREAD): bool, vol.Optional(CONF_DOWNLOAD_ATTACHMENTS, default=True): bool, vol.Optional(CONF_HTML_BODY, default=False): bool, vol.Optional(CONF_SHOW_BODY, default=True): bool, } ) STATUS_SENSOR = vol.Schema( vol.All( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ENABLE_UPDATE, None): bool, vol.Optional(CONF_EMAIL, None): cv.string, }, cv.has_at_most_one_key(CONF_ENABLE_UPDATE, CONF_EMAIL), ) ) CHAT_SENSOR = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ENABLE_UPDATE, default=False): bool, } ) AUTO_REPLY_SENSOR = vol.Schema( { vol.Required(CONF_NAME): cv.string, } ) QUERY_SENSOR = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_MAIL_FOLDER): cv.string, vol.Optional(CONF_MAIL_FROM): cv.string, vol.Optional(CONF_MAX_ITEMS, default=5): int, vol.Optional(CONF_HAS_ATTACHMENT): bool, vol.Optional(CONF_IMPORTANCE): cv.string, vol.Optional(CONF_IS_UNREAD): bool, vol.Exclusive(CONF_BODY_CONTAINS, "body_*"): cv.string, vol.Exclusive(CONF_SUBJECT_CONTAINS, "subject_*"): cv.string, vol.Exclusive(CONF_SUBJECT_IS, "subject_*"): cv.string, vol.Optional(CONF_DOWNLOAD_ATTACHMENTS, default=True): bool, vol.Optional(CONF_HTML_BODY, default=False): bool, vol.Optional(CONF_SHOW_BODY, default=True): bool, } ) TODO_SENSOR = vol.Schema( { vol.Required(CONF_ENABLED, default=False): bool, vol.Optional(CONF_TRACK_NEW, default=True): bool, vol.Optional(CONF_ENABLE_UPDATE, default=False): bool, } ) MULTI_ACCOUNT_SCHEMA = vol.Schema( { CONF_ACCOUNTS: vol.Schema( [ { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_TRACK_NEW_CALENDAR, default=True): bool, vol.Optional(CONF_ENABLE_CALENDAR, default=True): bool, vol.Optional(CONF_ENABLE_UPDATE, default=False): bool, vol.Optional(CONF_GROUPS, default=False): bool, vol.Required(CONF_ACCOUNT_NAME, ""): cv.string, vol.Optional(CONF_ALT_AUTH_METHOD, default=False): bool, vol.Optional(CONF_BASIC_CALENDAR, default=False): bool, 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], vol.Optional(CONF_TODO_SENSORS): TODO_SENSOR, vol.Optional(CONF_AUTO_REPLY_SENSORS): [AUTO_REPLY_SENSOR], vol.Optional(CONF_SHARED_MAILBOX, None): cv.string, } ] ) } ) NOTIFY_SERVICE_DATA_SCHEMA = vol.Schema( { vol.Optional(ATTR_MESSAGE_IS_HTML, default=False): bool, vol.Optional(ATTR_TARGET): cv.string, vol.Optional(ATTR_SENDER): cv.string, vol.Optional(ATTR_ZIP_ATTACHMENTS, default=False): bool, vol.Optional(ATTR_ZIP_NAME): cv.string, vol.Optional(ATTR_PHOTOS, default=[]): [cv.string], vol.Optional(ATTR_ATTACHMENTS, default=[]): [cv.string], vol.Optional(ATTR_IMPORTANCE): vol.Coerce(ImportanceLevel), } ) NOTIFY_SERVICE_BASE_SCHEMA = vol.Schema( { vol.Optional(ATTR_TARGET, default=[]): [cv.string], vol.Optional(ATTR_TITLE, default=""): cv.string, vol.Optional(ATTR_DATA): NOTIFY_SERVICE_DATA_SCHEMA, } ) CALENDAR_SERVICE_RESPOND_SCHEMA = { vol.Required(ATTR_EVENT_ID): cv.string, vol.Required(ATTR_RESPONSE, None): cv.enum(EventResponse), vol.Optional(ATTR_SEND_RESPONSE, True): bool, vol.Optional(ATTR_MESSAGE, None): cv.string, } CALENDAR_SERVICE_ATTENDEE_SCHEMA = vol.Schema( { vol.Required(ATTR_EMAIL): cv.string, vol.Required(ATTR_TYPE): cv.enum(AttendeeType), } ) CALENDAR_SERVICE_CREATE_SCHEMA = vol.All( cv.make_entity_service_schema( { vol.Required(ATTR_SUBJECT): cv.string, vol.Required(ATTR_START): cv.datetime, vol.Required(ATTR_END): cv.datetime, vol.Optional(ATTR_BODY): cv.string, vol.Optional(ATTR_LOCATION): cv.string, vol.Optional(ATTR_CATEGORIES): [cv.string], vol.Optional(ATTR_SENSITIVITY): vol.Coerce(EventSensitivity), vol.Optional(ATTR_SHOW_AS): vol.Coerce(EventShowAs), vol.Optional(ATTR_IS_ALL_DAY): bool, vol.Optional(ATTR_ATTENDEES): [CALENDAR_SERVICE_ATTENDEE_SCHEMA], } ), _has_consistent_timezone(ATTR_START, ATTR_END), _as_local_timezone(ATTR_START, ATTR_END), ) CALENDAR_SERVICE_MODIFY_SCHEMA = vol.All( cv.make_entity_service_schema( { vol.Required(ATTR_EVENT_ID): cv.string, vol.Optional(ATTR_START): cv.datetime, vol.Optional(ATTR_END): cv.datetime, vol.Optional(ATTR_SUBJECT): cv.string, vol.Optional(ATTR_BODY): cv.string, vol.Optional(ATTR_LOCATION): cv.string, vol.Optional(ATTR_CATEGORIES): [cv.string], vol.Optional(ATTR_SENSITIVITY): vol.Coerce(EventSensitivity), vol.Optional(ATTR_SHOW_AS): vol.Coerce(EventShowAs), vol.Optional(ATTR_IS_ALL_DAY): bool, vol.Optional(ATTR_ATTENDEES): [CALENDAR_SERVICE_ATTENDEE_SCHEMA], } ), _has_consistent_timezone(ATTR_START, ATTR_END), _as_local_timezone(ATTR_START, ATTR_END), ) CALENDAR_SERVICE_REMOVE_SCHEMA = { vol.Required(ATTR_EVENT_ID): cv.string, } STATUS_SERVICE_UPDATE_USER_STATUS_SCHEMA = { vol.Required(ATTR_AVAILABILITY): vol.Coerce(Availability), vol.Required(ATTR_ACTIVITY): vol.Coerce(Activity), vol.Optional(ATTR_EXPIRATIONDURATION): cv.string, } STATUS_SERVICE_UPDATE_USER_PERERRED_STATUS_SCHEMA = { vol.Required(ATTR_AVAILABILITY): vol.Coerce(PreferredAvailability), vol.Optional(ATTR_EXPIRATIONDURATION): cv.string, } TODO_SERVICE_NEW_SCHEMA = { vol.Required(ATTR_SUBJECT): cv.string, vol.Optional(ATTR_DESCRIPTION): cv.string, vol.Optional(ATTR_DUE): cv.date, vol.Optional(ATTR_REMINDER): vol.Any(cv.date, cv.datetime), } TODO_SERVICE_UPDATE_SCHEMA = { vol.Required(ATTR_TODO_ID): cv.string, vol.Optional(ATTR_SUBJECT): cv.string, vol.Optional(ATTR_DESCRIPTION): cv.string, vol.Optional(ATTR_DUE): cv.date, vol.Optional(ATTR_REMINDER): vol.Any(cv.date, cv.datetime), } TODO_SERVICE_DELETE_SCHEMA = { vol.Required(ATTR_TODO_ID): cv.string, } TODO_SERVICE_COMPLETE_SCHEMA = { vol.Required(ATTR_TODO_ID): cv.string, vol.Required(ATTR_COMPLETED): bool, } AUTO_REPLY_SERVICE_ENABLE_SCHEMA = { vol.Required(ATTR_EXTERNALREPLY): cv.string, vol.Required(ATTR_INTERNALREPLY): cv.string, vol.Optional(ATTR_START): cv.datetime, vol.Optional(ATTR_END): cv.datetime, vol.Optional(ATTR_EXTERNAL_AUDIENCE): vol.Coerce(ExternalAudience), } AUTO_REPLY_SERVICE_DISABLE_SCHEMA = {} YAML_CALENDAR_ENTITY_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_HOURS_FORWARD_TO_GET, default=24): int, vol.Optional(CONF_HOURS_BACKWARD_TO_GET, default=0): int, vol.Optional(CONF_SEARCH): cv.string, vol.Optional(CONF_EXCLUDE): [cv.string], vol.Optional(CONF_TRACK): cv.boolean, vol.Optional(CONF_MAX_RESULTS): cv.positive_int, } ) YAML_CALENDAR_DEVICE_SCHEMA = vol.Schema( { vol.Required(CONF_CAL_ID): cv.string, vol.Required(CONF_ENTITIES, None): vol.All( cv.ensure_list, [YAML_CALENDAR_ENTITY_SCHEMA] ), }, extra=vol.ALLOW_EXTRA, ) YAML_TASK_LIST_SCHEMA = vol.Schema( { vol.Required(CONF_YAML_TASK_LIST_ID): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_TRACK, default=True): cv.boolean, vol.Optional(CONF_SHOW_COMPLETED, default=False): cv.boolean, vol.Optional(CONF_DUE_HOURS_FORWARD_TO_GET): int, vol.Optional(CONF_DUE_HOURS_BACKWARD_TO_GET): int, } ) REQUEST_AUTHORIZATION_DEFAULT_SCHEMA = {vol.Required(CONF_URL): cv.string} CHAT_SERVICE_SEND_MESSAGE_SCHEMA = { vol.Required(ATTR_CHAT_ID): cv.string, vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_CONTENT_TYPE, default="text"): vol.In(CONTENT_TYPES), } ================================================ FILE: custom_components/o365/sensor.py ================================================ """Sensor processing.""" import logging from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_UNIQUE_ID from homeassistant.helpers import entity_platform from .classes.mailsensor import O365AutoReplySensor, O365MailSensor from .classes.teamssensor import O365TeamsChatSensor, O365TeamsStatusSensor from .const import ( CONF_ACCOUNT_NAME, CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, CONF_COORDINATOR_EMAIL, CONF_COORDINATOR_SENSORS, CONF_ENABLE_UPDATE, CONF_ENTITY_KEY, CONF_ENTITY_TYPE, CONF_IS_AUTHENTICATED, CONF_KEYS_EMAIL, CONF_KEYS_SENSORS, CONF_PERMISSIONS, CONF_SENSOR_CONF, CONF_STATUS_SENSORS, DOMAIN, PERM_CHAT_READWRITE, PERM_MAILBOX_SETTINGS, PERM_PRESENCE_READWRITE, SENSOR_AUTO_REPLY, SENSOR_TEAMS_CHAT, SENSOR_TEAMS_STATUS, ) from .schema import ( AUTO_REPLY_SERVICE_DISABLE_SCHEMA, AUTO_REPLY_SERVICE_ENABLE_SCHEMA, CHAT_SERVICE_SEND_MESSAGE_SCHEMA, STATUS_SERVICE_UPDATE_USER_PERERRED_STATUS_SCHEMA, STATUS_SERVICE_UPDATE_USER_STATUS_SCHEMA, ) _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): # pylint: disable=unused-argument """O365 platform definition.""" if discovery_info is None: return None account_name = discovery_info[CONF_ACCOUNT_NAME] conf = hass.data[DOMAIN][account_name] is_authenticated = conf[CONF_IS_AUTHENTICATED] if not is_authenticated: return False sensor_entities = await _async_sensor_entities(conf, hass) email_entities = _email_entities(conf) entities = sensor_entities + email_entities async_add_entities(entities, False) await _async_setup_register_services(conf) return True async def _async_sensor_entities(conf, hass): sensor_coordinator = conf[CONF_COORDINATOR_SENSORS] sensorentities = [] for key in conf[CONF_KEYS_SENSORS]: if key[CONF_ENTITY_TYPE] == SENSOR_TEAMS_CHAT: sensorentities.append( O365TeamsChatSensor( sensor_coordinator, key[CONF_NAME], key[CONF_ENTITY_KEY], conf, key[CONF_UNIQUE_ID], ) ) _LOGGER.warning( "The O365 Teams Chat sensors are now deprecated - please migrate to MS365 Teams " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) elif key[CONF_ENTITY_TYPE] == SENSOR_TEAMS_STATUS: sensorentities.append( O365TeamsStatusSensor( sensor_coordinator, key[CONF_NAME], key[CONF_ENTITY_KEY], conf, key[CONF_UNIQUE_ID], key[CONF_EMAIL], ) ) _LOGGER.warning( "The O365 Teams Status sensors are now deprecated - please migrate to MS365 Teams " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) elif key[CONF_ENTITY_TYPE] == SENSOR_AUTO_REPLY: entity = O365AutoReplySensor( sensor_coordinator, key[CONF_NAME], key[CONF_ENTITY_KEY], conf, key[CONF_UNIQUE_ID], ) await entity.async_init(hass) sensorentities.append(entity) _LOGGER.warning( "The O365 Auto Reply sensors are now deprecated - please migrate to MS365 Mail " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) return sensorentities def _email_entities(conf): email_coordinator = conf[CONF_COORDINATOR_EMAIL] _LOGGER.warning( "The O365 Email sensors are now deprecated - please migrate to MS365 Mail " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) return [ O365MailSensor( email_coordinator, conf, key[CONF_SENSOR_CONF], key[CONF_NAME], key[CONF_ENTITY_KEY], key[CONF_UNIQUE_ID], ) for key in conf[CONF_KEYS_EMAIL] ] async def _async_setup_register_services(config): perms = config[CONF_PERMISSIONS] await _async_setup_status_services(config, perms) await _async_setup_chat_services(config, perms) await _async_setup_mailbox_services(config, perms) async def _async_setup_status_services(config, perms): status_sensors = config.get(CONF_STATUS_SENSORS) if not status_sensors: return if not any( status_sensor.get(CONF_ENABLE_UPDATE) for status_sensor in status_sensors ): return platform = entity_platform.async_get_current_platform() if perms.validate_authorization(PERM_PRESENCE_READWRITE): platform.async_register_entity_service( "update_user_status", STATUS_SERVICE_UPDATE_USER_STATUS_SCHEMA, "async_update_user_status", ) platform.async_register_entity_service( "update_user_preferred_status", STATUS_SERVICE_UPDATE_USER_PERERRED_STATUS_SCHEMA, "async_update_user_preferred_status", ) async def _async_setup_chat_services(config, perms): chat_sensors = config.get(CONF_CHAT_SENSORS) if not chat_sensors: return chat_sensor = chat_sensors[0] if not chat_sensor or not chat_sensor.get(CONF_ENABLE_UPDATE): return platform = entity_platform.async_get_current_platform() if perms.validate_authorization(PERM_CHAT_READWRITE): platform.async_register_entity_service( "send_chat_message", CHAT_SERVICE_SEND_MESSAGE_SCHEMA, "async_send_chat_message", ) async def _async_setup_mailbox_services(config, perms): if not config.get(CONF_ENABLE_UPDATE): return if not config.get(CONF_AUTO_REPLY_SENSORS): return platform = entity_platform.async_get_current_platform() if perms.validate_authorization(PERM_MAILBOX_SETTINGS): platform.async_register_entity_service( "auto_reply_enable", AUTO_REPLY_SERVICE_ENABLE_SCHEMA, "async_auto_reply_enable", ) platform.async_register_entity_service( "auto_reply_disable", AUTO_REPLY_SERVICE_DISABLE_SCHEMA, "async_auto_reply_disable", ) ================================================ FILE: custom_components/o365/services.yaml ================================================ scan_for_calendars: name: Scan for new calendars description: "Scan for newly available calendars" scan_for_todo_lists: name: Scan for new todo lists description: "Scan for newly available todo lists" respond_calendar_event: name: Respond to an event description: "Respond to calendar event/invite" target: entity: integration: o365 domain: calendar fields: event_id: name: Event ID description: ID for event, can be found as an attribute on your calendar entity's events example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: response: name: Response description: "The response to the invite [Accept, Tentative, Decline]" example: Decline required: true selector: text: message: name: Message description: "The response message to the invite (Optional)" example: "I cannot attend this meeting" selector: text: send_response: name: Send Response description: "Send the response to the organizer (Optional)" example: True selector: boolean: create_calendar_event: name: Create a new event description: Create new calendar event target: entity: integration: o365 domain: calendar fields: subject: name: Subject description: The subject of the created event example: Clean up the garage required: true selector: text: start: name: Start description: The start time of the event example: "2025-03-22 20:00:00" required: true selector: datetime: end: name: End description: The end time of the event example: "2025-03-22 20:30:00" required: true selector: datetime: body: name: Body description: The body text for the event (optional) example: Remember to also clean out the gutters selector: text: location: name: Location description: The location for the event (optional) example: "1600 Pennsylvania Ave Nw, Washington, DC 20500" selector: text: categories: name: Categories description: list of categories for the event (optional) selector: text: sensitivity: name: Sensitivity description: "The sensitivity for the event (optional) [Normal, Personal, Private, Confidential]" example: normal selector: select: mode: dropdown options: - label: "Normal" value: "normal" - label: "Personal" value: "personal" - label: "Private" value: "private" - label: "Confidential" value: "confidential" show_as: name: Show As description: "Show event as (optional) [Free, Tentative, Busy, Oof, WorkingElsewhere, Unknown]" example: busy selector: select: mode: dropdown options: - label: "Free" value: "free" - label: "Tentative" value: "tentative" - label: "Busy" value: "busy" - label: "Out of Office" value: "oof" - label: "Working Elsewhere" value: "workingElsewhere" - label: "Unknown" value: "unknown" is_all_day: name: All Day description: Set whether event is all day (optional) example: False selector: boolean: attendees: name: Attendees description: "list of attendees formatted as email: example@example.com type: Required, Optional, or Resource (optional)" selector: object: modify_calendar_event: name: Modify an event description: Modify existing calendar event, all properties except event_id are optional. target: entity: integration: o365 domain: calendar fields: event_id: name: Event ID description: ID for the event, can be found as an attribute on you calendar entity's events example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: subject: name: Subject description: The subject of the created event example: Clean up the garage selector: text: start: name: Start description: The start time of the event example: "2025-03-22 20:00:00" selector: datetime: end: name: End description: The end time of the event example: "2025-03-22 20:30:00" selector: datetime: body: name: Body description: The body text for the event example: Remember to also clean out the gutters selector: text: location: name: Location description: The location for the event example: "1600 Pennsylvania Ave Nw, Washington, DC 20500" selector: text: categories: name: Categories description: list of categories for the event selector: text: sensitivity: name: Sensitivity description: "The sensitivity for the event[Normal, Personal, Private, Confidential]" example: normal selector: select: mode: dropdown options: - label: "Normal" value: "normal" - label: "Personal" value: "personal" - label: "Private" value: "private" - label: "Confidential" value: "confidential" show_as: name: Show As description: "Show event as [Free, Tentative, Busy, Oof, WorkingElsewhere, Unknown]" example: busy selector: select: mode: dropdown options: - label: "Free" value: "free" - label: "Tentative" value: "tentative" - label: "Busy" value: "busy" - label: "Out of Office" value: "oof" - label: "Working Elsewhere" value: "workingElsewhere" - label: "Unknown" value: "unknown" is_all_day: name: All Day description: Set whether event is all day example: False selector: boolean: attendees: name: Attendees description: "list of attendees formatted as email: example@example.com type: Required, Optional, or Resource" selector: object: remove_calendar_event: name: Delete an event description: Delete calendar event target: entity: integration: o365 domain: calendar fields: event_id: name: Event ID description: ID for the event, can be found as an attribute on you calendar entity's events example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: new_todo: name: Create a ToDo description: Create a new ToDo target: entity: integration: o365 domain: todo fields: subject: name: Subject description: The subject of the todo example: Pick up the mail required: true selector: text: description: name: Description description: Description of the todo example: Walk to the post box and collect the mail selector: text: due: name: Due date description: When the todo is due by example: '"2025-01-01"' selector: date: reminder: name: Reminder date & time description: When a reminder is needed example: "2025-01-01T12:00:00+0000" selector: datetime: update_todo: name: Update a ToDo description: Update a ToDo target: entity: integration: o365 domain: todo fields: todo_id: name: ToDo ID description: ID for the todo, can be found as an attribute on your todo example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: subject: name: Subject description: The subject of the todo example: Pick up the mail selector: text: description: name: Description description: Description of the todo example: Walk to the post box and collect the mail selector: text: due: name: Due date description: When the todo is due by example: '"2025-01-01"' selector: date: reminder: name: reminder date & time description: When a reminder is needed example: "2025-01-01T12:00:00+0000" selector: datetime: delete_todo: name: Delete a ToDo description: Delete a ToDo target: entity: integration: o365 domain: todo fields: todo_id: name: ToDo ID description: ID for the todo, can be found as an attribute on your todo example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: complete_todo: name: Complete a ToDo description: Complete a ToDo target: entity: integration: o365 domain: todo fields: todo_id: name: Todo ID description: ID for the todo, can be found as an attribute on your todo example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: completed: name: Completed description: Set whether todo is completed or not example: True required: true selector: boolean: auto_reply_enable: name: Auto reply enable description: Schedules auto reply target: entity: integration: o365 domain: sensor fields: external_reply: name: External reply description: The message to be send to external emails (or to all emails, if you don't have an organisation email) example: I'm currently on holliday, please email Bob for answers required: true selector: text: internal_reply: name: Internal Reply description: The message to be send to internal emails example: I'm currently on holliday required: true selector: text: start: name: Start date & time description: The start time of the schedule example: "2025-01-01T12:00:00+0000" selector: text: end: name: End date & time description: The end time of the schedule example: "2025-01-02T12:30:00+0000" selector: text: external_audience: name: External Audience description: "The set of audience external to the signed-in user's organization who will receive the ExternalReplyMessage. The possible values are: none, contactsOnly, all." example: all selector: select: mode: dropdown options: - label: "All" value: "all" - label: "Contacts Only" value: "contactsOnly" - label: "None" value: "none" auto_reply_disable: name: Auto reply disable description: Disables auto reply target: entity: integration: o365 domain: sensor send_chat_message: name: Send chat message description: "Send message to a specified chat" target: entity: integration: o365 domain: sensor fields: chat_id: name: Chat ID description: ID for chat, can be found as an attribute on your chat entity example: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx required: true selector: text: message: name: Message description: "Message to send to the chat" example: Hello team required: true selector: text: content_type: name: Content Type description: The type of content to send, html if you are sending a HTML message or text for plain text example: text required: false selector: select: mode: dropdown options: - label: HTML value: html - label: Text value: text update_user_status: name: Update user Teams status description: "Update the user's Teams status" target: entity: integration: o365 domain: sensor fields: availability: name: Availability description: "The base presence information" example: Busy required: true selector: select: mode: dropdown options: - label: "Available" value: "Available" - label: "Busy" value: "Busy" - label: "Away" value: "Away" - label: "Do Not Disturb" value: "DoNotDisturb" activity: name: Activity description: "The supplemental information to availability" example: InACall required: true selector: select: mode: dropdown options: - label: "Available" value: "Available" - label: "In a Call" value: "InACall" - label: "In a Conference Call" value: "InAConferenceCall" - label: "Away" value: "Away" - label: "Presenting" value: "Presenting" expiration_duration: name: Expiration Duration description: "The expiration of the app presence session. The value is represented in ISO 8601 format for durations" example: PT1H required: false selector: text: update_user_preferred_status: name: Update user preferred Teams status description: "Update the user's preferred Teams status" target: entity: integration: o365 domain: sensor fields: availability: name: Availability description: "The base presence information" example: Busy required: true selector: select: mode: dropdown options: - label: "Available" value: "Available" - label: "Busy" value: "Busy" - label: "Do Not Disturb" value: "DoNotDisturb" - label: "Be Right Back" value: "BeRightBack" - label: "Away" value: "Away" - label: "Offline" value: "Offline" expiration_duration: name: Expiration Duration description: "The expiration of the app presence session. The value is represented in ISO 8601 format for durations" example: PT1H required: false selector: text: migrate_config: name: Migrate existing O365 config to MS365 description: "Create disabled MS365 config entries based on existing O365 config" ================================================ FILE: custom_components/o365/strings.json ================================================ { "issues": { "deprecated_legacy_configuration": { "title": "Deprecated Secondary/Legacy configuration method", "description": "Secondary/Legacy configuration method is now deprecated and will be removed in a future release. Please migrate to the Primary configuration method. Click 'Learn More' for details on configuration. A potential configuration has been placed in the o365_storage folder." }, "authorization": { "title": "Authorization Required - {account_name}", "fix_flow": { "error": { "invalid_url": "Error, the originating url does not seem to be a valid microsoft redirect", "minimum_permissions": "Minimum permissions not granted for account: {account_name}", "missing_token_file": "Token file is missing after successful authentication, check log for file system errors", "token_file_error": "Token file retrieval error, check log for errors from O365" }, "step": { "request_default": { "title": "Authorization Required - {account_name}", "description": "Complete the configuration by clicking on the link and copying the returned url into this field afterwards and submit\n\n[Link O365 account]({auth_url}){failed_permissions}", "data": { "url": "Returned URL" } }, "request_alt": { "title": "Authorization Required - {account_name}", "description": "To link your O365 account, click the link, login, and authorize:\n\n[Link O365 account]({auth_url}){failed_permissions}", "submit": "I authorized successfully" } } } } } } ================================================ FILE: custom_components/o365/todo.py ================================================ """Todo processing.""" import logging from datetime import datetime, timedelta from homeassistant.components.todo import TodoItem, TodoListEntity from homeassistant.components.todo.const import TodoItemStatus, TodoListEntityFeature from homeassistant.const import CONF_ENABLED, CONF_NAME, CONF_UNIQUE_ID from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_platform from homeassistant.util import dt as dt_util from O365.utils.query import ( # pylint: disable=no-name-in-module, import-error QueryBuilder, ) from .classes.entity import O365Entity from .const import ( ATTR_ALL_TODOS, ATTR_COMPLETED, ATTR_CREATED, ATTR_DATA, ATTR_DESCRIPTION, ATTR_DUE, ATTR_OVERDUE_TODOS, ATTR_REMINDER, ATTR_STATUS, ATTR_SUBJECT, ATTR_TODO_ID, CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_COORDINATOR_SENSORS, CONF_DUE_HOURS_BACKWARD_TO_GET, CONF_DUE_HOURS_FORWARD_TO_GET, CONF_ENABLE_UPDATE, CONF_ENTITY_KEY, CONF_ENTITY_TYPE, CONF_IS_AUTHENTICATED, CONF_KEYS_SENSORS, CONF_O365_TASK_FOLDER, CONF_PERMISSIONS, CONF_SHOW_COMPLETED, CONF_TODO_SENSORS, CONF_TRACK_NEW, CONF_YAML_TASK_LIST, DATETIME_FORMAT, DOMAIN, EVENT_COMPLETED_TODO, EVENT_DELETE_TODO, EVENT_HA_EVENT, EVENT_NEW_TODO, EVENT_UNCOMPLETED_TODO, EVENT_UPDATE_TODO, PERM_TASKS_READWRITE, TODO_TODO, ) from .schema import ( TODO_SERVICE_COMPLETE_SCHEMA, TODO_SERVICE_DELETE_SCHEMA, TODO_SERVICE_NEW_SCHEMA, TODO_SERVICE_UPDATE_SCHEMA, ) from .utils.filemgmt import async_update_task_list_file _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): # pylint: disable=unused-argument """O365 platform definition.""" if discovery_info is None: return None account_name = discovery_info[CONF_ACCOUNT_NAME] conf = hass.data[DOMAIN][account_name] is_authenticated = conf[CONF_IS_AUTHENTICATED] if not is_authenticated: return False coordinator = conf[CONF_COORDINATOR_SENSORS] todoentities = [ O365TodoList( hass, coordinator, key[CONF_O365_TASK_FOLDER], key[CONF_NAME], key[CONF_YAML_TASK_LIST], conf, key[CONF_ENTITY_KEY], key[CONF_UNIQUE_ID], ) for key in conf[CONF_KEYS_SENSORS] if key[CONF_ENTITY_TYPE] == TODO_TODO ] async_add_entities(todoentities, False) await _async_setup_register_services(hass, conf) _LOGGER.warning( "The O365 Todo sensors are now deprecated - please migrate to MS365 To Do " + "- for more details on how to do this see " + "https://rogerselwyn.github.io/O365-HomeAssistant/migration.html" ) return True async def _async_setup_register_services(hass, config): perms = config[CONF_PERMISSIONS] await _async_setup_task_services(hass, config, perms) async def _async_setup_task_services(hass, config, perms): todo_sensors = config.get(CONF_TODO_SENSORS) if ( not todo_sensors or not todo_sensors.get(CONF_ENABLED) or not todo_sensors.get(CONF_ENABLE_UPDATE) ): return sensor_services = O365TodoEntityServices(hass) hass.services.async_register( DOMAIN, "scan_for_todo_lists", sensor_services.async_scan_for_todo_lists ) platform = entity_platform.async_get_current_platform() if perms.validate_authorization(PERM_TASKS_READWRITE): platform.async_register_entity_service( "new_todo", TODO_SERVICE_NEW_SCHEMA, "async_new_todo", ) platform.async_register_entity_service( "update_todo", TODO_SERVICE_UPDATE_SCHEMA, "async_update_todo", ) platform.async_register_entity_service( "delete_todo", TODO_SERVICE_DELETE_SCHEMA, "async_delete_todo", ) platform.async_register_entity_service( "complete_todo", TODO_SERVICE_COMPLETE_SCHEMA, "async_complete_todo", ) class O365TodoList(O365Entity, TodoListEntity): # pylint: disable=abstract-method """O365 ToDo processing.""" def __init__( self, hass, coordinator, o365_task_folder, name, yaml_task_list, config, entity_id, unique_id, ): """Initialise the ToDo List.""" super().__init__(coordinator, config, name, entity_id, TODO_TODO, unique_id) self.todolist = o365_task_folder self._show_completed = yaml_task_list.get(CONF_SHOW_COMPLETED) self.todo_last_created = dt_util.utcnow() - timedelta(minutes=5) self.todo_last_completed = dt_util.utcnow() - timedelta(minutes=5) self._zero_date = datetime( 1, 1, 1, 0, 0, 0, tzinfo=dt_util.get_default_time_zone() ) self._state = None self._todo_items = None self._extra_attributes = None self._update_status(hass) if config.get(CONF_TODO_SENSORS).get(CONF_ENABLE_UPDATE): self._attr_supported_features = ( TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM | TodoListEntityFeature.DELETE_TODO_ITEM | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM ) @property def state(self): """Todo state.""" return self._state @property def todo_items(self): """List of Todos.""" return self._todo_items @property def extra_state_attributes(self): """Device state attributes.""" return self._extra_attributes def _handle_coordinator_update(self) -> None: self._update_status(self.hass) self.async_write_ha_state() def _update_status(self, hass): todos = self.coordinator.data[self.entity_key][ATTR_DATA] self._state = sum(not task.completed for task in todos) self._todo_items = [] for todo in todos: completed = ( TodoItemStatus.COMPLETED if todo.completed else TodoItemStatus.NEEDS_ACTION ) self._todo_items.append( TodoItem( uid=todo.task_id, summary=todo.subject, status=completed, description=todo.body, due=todo.due.date() if todo.due else None, ) ) self._extra_attributes = self._update_extra_state_attributes(todos) todo_last_completed = self._zero_date todo_last_created = self._zero_date for todo in todos: if todo.completed and todo.completed > self.todo_last_completed: _raise_event_external( hass, EVENT_COMPLETED_TODO, todo.task_id, ATTR_COMPLETED, todo.completed, ) if todo.completed > todo_last_completed: todo_last_completed = todo.completed if todo.created and todo.created > self.todo_last_created: _raise_event_external( hass, EVENT_NEW_TODO, todo.task_id, ATTR_CREATED, todo.created ) if todo.created > todo_last_created: todo_last_created = todo.created if todo_last_completed > self._zero_date: self.todo_last_completed = todo_last_completed if todo_last_created > self._zero_date: self.todo_last_created = todo_last_created def _update_extra_state_attributes(self, todos): """Extra state attributes.""" all_todos = [] overdue_todos = [] for item in todos: todo = { ATTR_SUBJECT: item.subject, ATTR_TODO_ID: item.task_id, ATTR_STATUS: item.status, } if item.body: todo[ATTR_DESCRIPTION] = item.body if self._show_completed: todo[ATTR_COMPLETED] = ( item.completed.strftime(DATETIME_FORMAT) if item.completed else False ) if item.due: due = item.due.date() todo[ATTR_DUE] = due if due < dt_util.utcnow().date(): overdue_todo = { ATTR_SUBJECT: item.subject, ATTR_TODO_ID: item.task_id, ATTR_DUE: due, } if item.is_reminder_on: overdue_todo[ATTR_REMINDER] = item.reminder overdue_todos.append(overdue_todo) if item.is_reminder_on: todo[ATTR_REMINDER] = item.reminder all_todos.append(todo) extra_attributes = {ATTR_ALL_TODOS: all_todos} if overdue_todos: extra_attributes[ATTR_OVERDUE_TODOS] = overdue_todos return extra_attributes async def async_create_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" await self.async_new_todo( subject=item.summary, description=item.description, due=item.due ) async def async_new_todo(self, subject, description=None, due=None, reminder=None): """Create a new task for this task list.""" if not self._validate_task_permissions(): return False new_o365_task = await self.hass.async_add_executor_job(self.todolist.new_task) await self._async_save_task(new_o365_task, subject, description, due, reminder) self._raise_event(EVENT_NEW_TODO, new_o365_task.task_id) self.todo_last_created = new_o365_task.created await self.coordinator.async_refresh() return True async def async_update_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" o365_task = await self.hass.async_add_executor_job( self.todolist.get_task, item.uid ) if item.status: completed = None if item.status == TodoItemStatus.COMPLETED and not o365_task.completed: completed = True elif item.status == TodoItemStatus.NEEDS_ACTION and o365_task.completed: completed = False if completed is not None: await self.async_complete_todo(item.uid, completed, o365_task=o365_task) return if ( item.summary != o365_task.subject or item.description != o365_task.body or (item.due and item.due != o365_task.due) ): await self.async_update_todo( todo_id=item.uid, subject=item.summary, description=item.description, due=item.due, o365_task=o365_task, hatodo=True, ) async def async_update_todo( self, todo_id, subject=None, description=None, due=None, reminder=None, o365_task=None, hatodo=False, ): """Update a task for this task list.""" if not self._validate_task_permissions(): return False if not o365_task: o365_task = await self.hass.async_add_executor_job( self.todolist.get_task, todo_id ) await self._async_save_task( o365_task, subject, description, due, reminder, hatodo ) self._raise_event(EVENT_UPDATE_TODO, todo_id) await self.coordinator.async_refresh() return True async def async_delete_todo_items(self, uids: list[str]) -> None: """Delete items from the To-do list.""" for todo_id in uids: await self.async_delete_todo(todo_id) async def async_delete_todo(self, todo_id): """Delete task for this task list.""" if not self._validate_task_permissions(): return False o365_task = await self.hass.async_add_executor_job( self.todolist.get_task, todo_id ) await self.hass.async_add_executor_job(o365_task.delete) self._raise_event(EVENT_DELETE_TODO, todo_id) await self.coordinator.async_refresh() return True async def async_complete_todo(self, todo_id, completed, o365_task=None): """Complete task for this task list.""" if not self._validate_task_permissions(): return False if not o365_task: o365_task = await self.hass.async_add_executor_job( self.todolist.get_task, todo_id ) if completed: await self._async_complete_task(o365_task, todo_id) else: await self._async_uncomplete_task(o365_task, todo_id) await self.coordinator.async_refresh() return True async def _async_complete_task(self, o365_task, todo_id): if o365_task.completed: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="todo_completed", ) await self.hass.async_add_executor_job(o365_task.mark_completed) await self.hass.async_add_executor_job(o365_task.save) self._raise_event(EVENT_COMPLETED_TODO, todo_id) self.todo_last_completed = dt_util.utcnow() async def _async_uncomplete_task(self, o365_task, todo_id): if not o365_task.completed: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="todo_not_completed", ) await self.hass.async_add_executor_job(o365_task.mark_uncompleted) await self.hass.async_add_executor_job(o365_task.save) self._raise_event(EVENT_UNCOMPLETED_TODO, todo_id) async def _async_save_task( self, o365_task, subject, description, due, reminder, hatodo=False ): # sourcery skip: raise-from-previous-error if subject or hatodo: o365_task.subject = subject if description or hatodo: o365_task.body = description if due: if isinstance(due, str): try: if len(due) > 10: o365_task.due = dt_util.parse_datetime(due).date() else: o365_task.due = dt_util.parse_date(due) except ValueError: raise ServiceValidationError( # pylint: disable=raise-missing-from translation_domain=DOMAIN, translation_key="due_date_invalid", translation_placeholders={ "due": due, }, ) else: o365_task.due = due if reminder: o365_task.reminder = reminder await self.hass.async_add_executor_job(o365_task.save) def _raise_event(self, event_type, todo_id): self.hass.bus.fire( f"{DOMAIN}_{event_type}", {ATTR_TODO_ID: todo_id, EVENT_HA_EVENT: True}, ) _LOGGER.debug("%s - %s", event_type, todo_id) def _validate_task_permissions(self): return self._validate_permissions( PERM_TASKS_READWRITE, f"Not authorised to create new ToDo - requires permission: {PERM_TASKS_READWRITE}", ) def _raise_event_external(hass, event_type, todo_id, time_type, task_datetime): hass.bus.fire( f"{DOMAIN}_{event_type}", {ATTR_TODO_ID: todo_id, time_type: task_datetime, EVENT_HA_EVENT: False}, ) _LOGGER.debug("%s - %s - %s", event_type, todo_id, task_datetime) async def async_build_todo_query(builder: QueryBuilder, key): """Build query for ToDo.""" o365_task = key[CONF_YAML_TASK_LIST] show_completed = o365_task[CONF_SHOW_COMPLETED] query = builder.select() if not show_completed: query = query & builder.unequal("status", "completed") start_offset = o365_task.get(CONF_DUE_HOURS_BACKWARD_TO_GET) end_offset = o365_task.get(CONF_DUE_HOURS_FORWARD_TO_GET) if start_offset: start = dt_util.utcnow() + timedelta(hours=start_offset) query = query & builder.greater_equal( "due", start.strftime("%Y-%m-%dT%H:%M:%S") ) if end_offset: end = dt_util.utcnow() + timedelta(hours=end_offset) query = query & builder.less_equal("due", end.strftime("%Y-%m-%dT%H:%M:%S")) return query class O365TodoEntityServices: """Sensor Services.""" def __init__(self, hass): """Initialise the sensor services.""" self._hass = hass async def async_scan_for_todo_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 = await self._hass.async_add_executor_job( 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: await async_update_task_list_file( config, todo, self._hass, track, ) ================================================ FILE: custom_components/o365/translations/en.json ================================================ { "issues": { "deprecated_legacy_configuration": { "title": "Deprecated Secondary/Legacy configuration method", "description": "Secondary/Legacy configuration method is now deprecated and will be removed in a future release. Please migrate to the Primary configuration method. Click 'Learn More' for details on configuration. A potential configuration has been placed in the o365_storage folder." }, "authorization": { "title": "Authorization Required - {account_name}", "fix_flow": { "error": { "invalid_url": "Error, the originating url does not seem to be a valid microsoft redirect", "minimum_permissions": "Minimum permissions not granted for account: {account_name}", "corrupted_token_file": "Token file is corrupted after successful authentication, check log for file system errors", "missing_token_file": "Token file is missing after successful authentication, check log for file system errors", "token_file_error": "Token file retrieval error, check log for errors from O365" }, "step": { "request_default": { "title": "Authorization Required - {account_name}", "description": "Complete the configuration by clicking on the link and copying the returned url into this field afterwards and submit\n\n[Link O365 account]({auth_url}){failed_permissions}", "data": { "url": "Returned URL" } }, "request_alt": { "title": "Authorization Required - {account_name}", "description": "To link your O365 account, click the link, login, and authorize:\n\n[Link O365 account]({auth_url}){failed_permissions}", "submit": "I authorized successfully" } } } } }, "exceptions": { "o365_group_calendar_error": { "message": "O365 Python does not have capability to update/respond to group calendar events: {entity_id}" }, "not_authorised_to_event": { "message": "Not authorised to {calendar} calendar event - requires permission: {error_message}" }, "not_authorised": { "message": "Not authorised requires permission: {required_permission}" }, "not_possible": { "message": "Not possible to update another user's status: {email}" }, "todo_completed": { "message": "ToDo is already completed" }, "todo_not_completed": { "message": "ToDo has not been completed previously" }, "due_date_invalid": { "message": "Due date {due} is not in valid format YYYY-MM-DD" } } } ================================================ FILE: custom_components/o365/translations/sk.json ================================================ { "issues": { "deprecated_legacy_configuration": { "title": "Zastaraná sekundárna/stará metóda konfigurácie", "description": "Sekundárna/stará metóda konfigurácie je teraz zastaraná a bude odstránená v budúcom vydaní. Prejdite na primárnu metódu konfigurácie. Kliknutím na „Viac informácií“ získate podrobnosti o konfigurácii. Potenciálna konfigurácia bola umiestnená do priečinka o365_storage." }, "authorization": { "title": "Je potrebná autorizácia - {account_name}", "fix_flow": { "error": { "invalid_url": "Chyba, pôvodná adresa URL sa nezdá byť platným presmerovaním spoločnosti Microsoft", "minimum_permissions": "Minimálne povolenia nie sú udelené pre účet: {account_name}", "corrupted_token_file": "Súbor tokenu je po úspešnej autentifikácii poškodený, skontrolujte denník, či neobsahuje chyby systému súborovs", "missing_token_file": "Po úspešnom overení chýba súbor tokenu, skontrolujte denník kvôli chybám systému súborov", "token_file_error": "Chyba pri načítaní súboru tokenu, skontrolujte protokol, či neobsahuje chyby od O365" }, "step": { "request_default": { "title": "Je potrebná autorizácia - {account_name}", "description": "Dokončite konfiguráciu kliknutím na odkaz, skopírovaním vrátenej adresy URL do tohto poľa a odoslaním\n\n[Link O365 account]({auth_url}){failed_permissions}", "data": { "url": "Vrátená adresa URL" } }, "request_alt": { "title": "Je potrebná autorizácia - {account_name}", "description": "Ak chcete prepojiť svoj účet O365, kliknite na odkaz, prihláste sa a autorizujte:\n\n[Link O365 account]({auth_url}){failed_permissions}", "submit": "Úspešne som autorizoval" } } } } }, "exceptions": { "o365_group_calendar_error": { "message": "O365 Python nemá schopnosť aktualizovať/odpovedať na udalosti skupinového kalendára: {entity_id}" }, "not_authorised_to_event": { "message": "Nie je na to oprávnený {calendar} udalosť v kalendári – vyžaduje povolenie: {error_message}" }, "not_authorised": { "message": "Neoprávnené požadované povolenie: {required_permission}" }, "not_possible": { "message": "Nie je možné aktualizovať stav iného používateľa: {email}" }, "todo_completed": { "message": "Úloha je už dokončená" }, "todo_not_completed": { "message": "ToDo ešte nebolo dokončené" }, "due_date_invalid": { "message": "Do dátumu {due} nie je v platnom formáte RRRR-MM-DD" } } } ================================================ FILE: custom_components/o365/utils/__init__.py ================================================ """Initialise.""" ================================================ FILE: custom_components/o365/utils/calendar_utils.py ================================================ """Calendar utilities processes.""" import logging from datetime import datetime, timedelta from O365.calendar import Attendee # pylint: disable=no-name-in-module) from ..const import ( ATTR_ATTENDEES, ATTR_BODY, ATTR_CATEGORIES, ATTR_IS_ALL_DAY, ATTR_LOCATION, ATTR_RRULE, ATTR_SENSITIVITY, ATTR_SHOW_AS, DAYS, INDEXES, ) from .utils import clean_html _LOGGER = logging.getLogger(__name__) def format_event_data(event): """Format the event data.""" return { "summary": event.subject, "start": get_hass_date(event.start, event.is_all_day), "end": get_hass_date(get_end_date(event), event.is_all_day), "all_day": event.is_all_day, "description": clean_html(event.body), "location": event.location["displayName"], "categories": event.categories, "sensitivity": event.sensitivity.name, "show_as": event.show_as.name, "attendees": [ {"email": x.address, "type": x.attendee_type.value} for x in event.attendees._Attendees__attendees # pylint: disable=protected-access ], "uid": event.object_id, } def get_hass_date(obj, is_all_day): """Get the date.""" return obj if isinstance(obj, datetime) and not is_all_day else obj.date() def get_end_date(obj): """Get the end date.""" if hasattr(obj, "end"): return obj.end if hasattr(obj, "duration"): return obj.start + obj.duration.value return obj.start + timedelta(days=1) def get_start_date(obj): """Get the start date.""" return obj.start def add_call_data_to_event(event, subject, start, end, **kwargs): """Add the call data.""" event.subject = _add_attribute(subject, event.subject) event.body = _add_attribute(kwargs.get(ATTR_BODY, None), event.body) event.location = _add_attribute(kwargs.get(ATTR_LOCATION, None), event.location) event.categories = _add_attribute(kwargs.get(ATTR_CATEGORIES, []), event.categories) event.show_as = _add_attribute(kwargs.get(ATTR_SHOW_AS, None), event.show_as) event.start = _add_attribute(start, event.start) event.end = _add_attribute(end, event.end) event.sensitivity = _add_attribute( kwargs.get(ATTR_SENSITIVITY, None), event.sensitivity ) _add_attendees(kwargs.get(ATTR_ATTENDEES, []), event) _add_all_day(kwargs.get(ATTR_IS_ALL_DAY, False), event) if kwargs.get(ATTR_RRULE, None): _rrule_processing(event, kwargs[ATTR_RRULE]) return event def _add_attribute(attribute, event_attribute): return attribute or event_attribute def _add_attendees(attendees, event): if attendees: event.attendees.clear() event.attendees.add( [ Attendee(x["email"], attendee_type=x["type"], event=event) for x in attendees ] ) def _add_all_day(is_all_day, event): if is_all_day is not None: event.is_all_day = is_all_day if event.is_all_day: event.start = datetime( event.start.year, event.start.month, event.start.day, 0, 0, 0 ) event.end = datetime( event.end.year, event.end.month, event.end.day, 0, 0, 0 ) def _rrule_processing(event, rrule): rules = {} for item in rrule.split(";"): keys = item.split("=") rules[keys[0]] = keys[1] kwargs = {} if "COUNT" in rules: kwargs["occurrences"] = int(rules["COUNT"]) if "UNTIL" in rules: end = datetime.strptime(rules["UNTIL"], "%Y%m%dT%H%M%S") end.replace(tzinfo=event.start.tzinfo) kwargs["end"] = end interval = int(rules["INTERVAL"]) if "INTERVAL" in rules else 1 if "BYDAY" in rules: days, index = _process_byday(rules["BYDAY"]) kwargs["days_of_week"] = days if index: kwargs["index"] = index if rules["FREQ"] == "YEARLY": kwargs["day_of_month"] = event.start.day event.recurrence.set_yearly(interval, event.start.month, **kwargs) if rules["FREQ"] == "MONTHLY": if "BYDAY" not in rules: kwargs["day_of_month"] = event.start.day event.recurrence.set_monthly(interval, **kwargs) if rules["FREQ"] == "WEEKLY": kwargs["first_day_of_week"] = "sunday" event.recurrence.set_weekly(interval, **kwargs) if rules["FREQ"] == "DAILY": event.recurrence.set_daily(interval, **kwargs) def _process_byday(byday): days = [] for item in byday.split(","): if len(item) > 2: days.append(DAYS[item[2:4]]) index = INDEXES[item[:2]] else: days.append(DAYS[item[:2]]) index = None return days, index ================================================ FILE: custom_components/o365/utils/filemgmt.py ================================================ """File management processes.""" import logging import os import yaml from homeassistant.const import CONF_NAME from voluptuous.error import Error as VoluptuousError from ..const import ( CONF_ACCOUNT_NAME, CONF_CAL_ID, CONF_CONFIG_TYPE, CONF_DEVICE_ID, CONF_ENTITIES, CONF_TRACK, CONF_YAML_TASK_LIST_ID, CONST_CONFIG_TYPE_LIST, DOMAIN, O365_STORAGE, YAML_CALENDARS_FILENAME, YAML_TASK_LISTS_FILENAME, ) from ..schema import YAML_CALENDAR_DEVICE_SCHEMA, YAML_TASK_LIST_SCHEMA _LOGGER = logging.getLogger(__name__) def load_yaml_file(path, item_id, item_schema): """Load the o365 yaml file.""" items = {} try: with open(path, encoding="utf8") as file: data = yaml.safe_load(file) if data is None: return {} for item in data: try: items[item[item_id]] = item_schema(item) except VoluptuousError as exception: # keep going _LOGGER.warning("Invalid Data: %s", exception) except FileNotFoundError: # When YAML file could not be loaded/did not contain a dict return {} return items def _write_yaml_file(yaml_filepath, yaml_list): with open(yaml_filepath, "a", encoding="UTF8") as out: out.write("\n") yaml.dump([yaml_list], out, default_flow_style=False, encoding="UTF8") out.close() def _get_calendar_info(calendar, track_new_devices): """Convert data from O365 into DEVICE_SCHEMA.""" return YAML_CALENDAR_DEVICE_SCHEMA( { CONF_CAL_ID: calendar.calendar_id, CONF_ENTITIES: [ { CONF_TRACK: track_new_devices, CONF_NAME: calendar.name, CONF_DEVICE_ID: calendar.name, } ], } ) async def async_update_calendar_file(config, calendar, hass, track_new_devices): """Update the calendar file.""" path = build_yaml_filename(config, YAML_CALENDARS_FILENAME) yaml_filepath = build_config_file_path(hass, path) existing_calendars = await hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_CAL_ID, YAML_CALENDAR_DEVICE_SCHEMA ) cal = _get_calendar_info(calendar, track_new_devices) if cal[CONF_CAL_ID] in existing_calendars: return await hass.async_add_executor_job(_write_yaml_file, yaml_filepath, cal) def _get_task_list_info(yaml_task_list, track_new_devices): """Convert data from O365 into DEVICE_SCHEMA.""" return YAML_TASK_LIST_SCHEMA( { CONF_YAML_TASK_LIST_ID: yaml_task_list.folder_id, CONF_NAME: yaml_task_list.name, CONF_TRACK: track_new_devices, } ) async def async_update_task_list_file(config, yaml_task_list, hass, track_new_devices): """Update the calendar file.""" path = build_yaml_filename(config, YAML_TASK_LISTS_FILENAME) yaml_filepath = build_config_file_path(hass, path) existing_task_lists = await hass.async_add_executor_job( load_yaml_file, yaml_filepath, CONF_YAML_TASK_LIST_ID, YAML_TASK_LIST_SCHEMA ) yaml_task_list = _get_task_list_info(yaml_task_list, track_new_devices) if yaml_task_list[CONF_YAML_TASK_LIST_ID] in existing_task_lists: return await hass.async_add_executor_job(_write_yaml_file, yaml_filepath, yaml_task_list) def build_config_file_path(hass, filepath): """Create config path.""" root = hass.config.config_dir return os.path.join(root, O365_STORAGE, filepath) def build_yaml_filename(conf, filename, conf_type=None): """Create the token file name.""" if conf_type: config_file = f"_{conf.get(CONF_ACCOUNT_NAME)}" else: config_file = ( f"_{conf.get(CONF_ACCOUNT_NAME)}" if conf.get(CONF_CONFIG_TYPE) == CONST_CONFIG_TYPE_LIST else "" ) return filename.format(DOMAIN, config_file) ================================================ FILE: custom_components/o365/utils/utils.py ================================================ """Utilities processes.""" import logging from bs4 import BeautifulSoup from ..const import ( CONF_ACCOUNT, CONF_ACCOUNT_NAME, CONF_AUTO_REPLY_SENSORS, CONF_CHAT_SENSORS, CONF_CLIENT_ID, CONF_CONFIG_TYPE, CONF_EMAIL_SENSORS, CONF_ENABLE_CALENDAR, CONF_ENABLE_UPDATE, CONF_IS_AUTHENTICATED, CONF_PERMISSIONS, CONF_QUERY_SENSORS, CONF_STATUS_SENSORS, CONF_TODO_SENSORS, CONF_TRACK_NEW_CALENDAR, DATETIME_FORMAT, ) _LOGGER = logging.getLogger(__name__) def clean_html(html): """Clean the HTML.""" soup = BeautifulSoup(html, features="html.parser") if body := soup.find("body"): # get text text = body.get_text() # break into lines and remove leading and trailing space on each lines = (line.strip() for line in text.splitlines()) # break multi-headlines into a line each chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) # drop blank lines text = "\n".join(chunk for chunk in chunks if chunk) return text.replace("\xa0", " ") return html def _safe_html(html): """Make the HTML safe.""" soup = BeautifulSoup(html, features="html.parser") if soup.find("body"): blacklist = ["script", "style"] for tag in soup.findAll(): if tag.name.lower() in blacklist: # blacklisted tags are removed in their entirety tag.extract() return str(soup.find("body")) return html def get_email_attributes(mail, download_attachments, html_body, show_body): """Get the email attributes.""" data = { "subject": mail.subject, "received": mail.received.strftime(DATETIME_FORMAT), "to": [x.address for x in mail.to], "cc": [x.address for x in mail.cc], "sender": mail.sender.address, "has_attachments": mail.has_attachments, "importance": mail.importance.value, "is_read": mail.is_read, "flag": { "is_flagged": mail.flag.is_flagged, "is_completed": mail.flag.is_completed, "due_date": mail.flag.due_date, "completion_date": mail.flag.completition_date, }, } if show_body or html_body: data["body"] = _safe_html(mail.body) if html_body else clean_html(mail.body) if download_attachments: data["attachments"] = [x.name for x in mail.attachments] return data def build_account_config(config, account, is_authenticated, conf_type, perms): """Build the account 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, []) todo_sensors = config.get(CONF_TODO_SENSORS, []) auto_reply_sensors = config.get(CONF_AUTO_REPLY_SENSORS, []) enable_update = config.get(CONF_ENABLE_UPDATE, False) enable_calendar = config.get(CONF_ENABLE_CALENDAR, True) return { CONF_CLIENT_ID: config.get(CONF_CLIENT_ID), CONF_ACCOUNT: account, CONF_IS_AUTHENTICATED: is_authenticated, CONF_EMAIL_SENSORS: email_sensors, CONF_QUERY_SENSORS: query_sensors, CONF_STATUS_SENSORS: status_sensors, CONF_CHAT_SENSORS: chat_sensors, CONF_TODO_SENSORS: todo_sensors, CONF_AUTO_REPLY_SENSORS: auto_reply_sensors, CONF_ENABLE_UPDATE: enable_update, CONF_ENABLE_CALENDAR: enable_calendar, CONF_TRACK_NEW_CALENDAR: config.get(CONF_TRACK_NEW_CALENDAR, True), CONF_ACCOUNT_NAME: config.get(CONF_ACCOUNT_NAME, ""), CONF_CONFIG_TYPE: conf_type, CONF_PERMISSIONS: perms, } ================================================ FILE: docs/_config.yml ================================================ title: O365 Home Assistant remote_theme: just-the-docs/just-the-docs search_enabled: true search: heading_level: 3 # Back to top link back_to_top: true back_to_top_text: "Back to top" gh_edit_link: true # show or hide edit this page link gh_edit_link_text: "Edit this page on GitHub" gh_edit_repository: "https://github.com/RogerSelwyn/O365-HomeAssistant" # the github URL for your repo gh_edit_branch: "master" # the branch that your docs is served from gh_edit_source: docs # the source that your files originate from gh_edit_view_mode: "tree" # "tree" or "edit" if you want the user to jump into the editor immediately footer_content: "To learn about migration to the new MS365 integrations click here - Migration" ================================================ FILE: docs/authentication.md ================================================ --- title: Authentication nav_order: 5 --- # Authentication The Primary method of authentication is the simplest to configure and requires no access from the internet to your HA instance, therefore is the most secure method. It has slightly more steps to follow when authenticating. The alternate method is more complex to set up, since the Azure App that is created in the prerequisites' section must be configured to enable authentication from your HA instance whether located in your home network or utilising a cloud service such as Nabu Casa. The actual authentication is slightly simpler with fewer steps. During setup, the difference in configuration between each method is the value of the redirect URI on your Azure App. The authentication steps for each method are shown below. ## Primary (default) authentication method This requires *alt_auth_method* to be set to *False* or be not present and the redirect URI in your Azure app set to `https://login.microsoftonline.com/common/oauth2/nativeclient`. After setting up configuration.yaml and restarting Home Assistant, a repair will be created. 1. Click on this repair. 2. Click the `Link O365 account` link. 3. Login on the Microsoft page; when prompted, authorize the app you created 4. Copy the URL from the browser URL bar. 5. Insert into the "Returned URL" field 6. Click `Submit`. ## Alternate authentication method This requires the *alt_auth_method* to be set to *True* and the redirect URI in your Azure app set to `https:///api/o365` (Nabu Casa users should use `https:///api/o365` instead). After setting up configuration.yaml with the key set to _True_ and restarting Home Assistant a repair will be created. 1. Click on this repair. 2. Click the `Link O365 account` link. 3. Login on the Microsoft page; when prompted, authorize the app you created 4. If required, close the window when the message "This window can be closed" appears. 5. Click `I authorized successfully` ## Multi-Factor Authentication (MFA) If you are using Multi-factor Authentication (MFA), you may find you also need to add `https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize` to your redirect URIs. ## Re-authentication If you need to re-authenticate for any reason, for instance if you have changed features (such as enabling update) and haven't had the repair notification, you should delete the token as described on the [token page](./token.md). ================================================ FILE: docs/calendar_configuration.md ================================================ --- title: Calendar Configuration nav_order: 6 --- # Calendar configuration The integration uses an external `o365_calendars_.yaml` file which is stored in the `o365_storage` directory. ## Example Calendar yaml: ```yaml - cal_id: xxxx entities: - device_id: work_calendar end_offset: 24 name: My Work Calendar start_offset: 0 track: true - cal_id: xxxx entities: - device_id: birthdays end_offset: 24 name: Birthdays start_offset: 0 track: true ``` ### Calendar yaml configuration variables Key | Type | Required | Description -- | -- | -- | -- `cal_id` | `string` | `True` | O365 generated unique ID, DO NOT CHANGE `entities` | `list` | `True` | List of entities (see below) to generate from this calendar ### Entity configuration Key | Type | Required | Description -- | -- | -- | -- `device_id` | `string` | `True` | The entity_id will be "calendar.{device_id}" `name` | `string` | `True` | The name of your sensor that you’ll see in the frontend. `track` | `boolean` | `True` | **True**=Create calendar entity. False=Don't create entity `search` | `string` | `False` | Only get events if subject contains this string `exclude` | `list[string/regex]` | `False` | Exclude events where the subject contains any one of items in the list of strings `start_offset` | `integer` | `False` | Number of hours to offset the start time to search for events for (negative numbers to offset into the past). `end_offset` | `integer` | `False` | Number of hours to offset the end time to search for events for (negative numbers to offset into the past). ## Group calendars The integration supports Group calendars in a fairly simple form. The below are the constraints. * This gets the default calendar for the group. * There is no discovery. You will need to find them in the MS Graph api. Using the MS Graph API you can call https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group to get the groups. You will need the relevant group's `id` for configuration purposes, see below * You can create events using the standard service, but you cannot modify/delete/respond to them. To configure a Group Calendar, add an extra section to `o365_calendars_.yaml`. Set `cal_id` to `group:xxxxxxxxxxxxxxx` using the ID you found via the api above. Make sure to set the `device_id` to something unique. ```yaml - cal_id: group:xxxx entities: - device_id: group_calendar end_offset: 24 name: Group Calendar start_offset: 0 track: true ``` ## Exclude To exclude calendar items from being displayed, e.g. cancelled events, the exclude attribute can be used. This takes straight strings or can be configured with a regex for more complex exclusions. ```yaml exclude: - "Cancelled" - "^In.*Junk$" ``` ================================================ FILE: docs/calendar_panel.md ================================================ --- title: Calendar Panel nav_order: 17 --- Creation and deletion of events is possible via the Calendar Panel introduced in HA 2023.1. This UI allows you to create recurring events, which is not possible via the HA services methods. If you choose to delete 'Delete All Future Events', it will delete the whole series not just future events. This is due to the differences between O365 and the iCal specification that the core calendar is built on. ================================================ FILE: docs/errors.md ================================================ --- title: Errors nav_order: 99 --- # Errors * **The reply URL specified in the request does not match the reply URLs configured for the application.** or * **The provided value for the input parameter 'redirect_uri' is not valid.** * Please ensure that you have configured the internet URL on your Home Assistant network settings config and that you have added the correct redirect URI to your Azure app as described in the [Prerequisites](./prerequisites.md) * **Client is public so neither 'client_assertion' nor 'client_secret' should be presented.** * Please ensure that you have set "Allow public client flows" to Yes in your Azure app under Authentication ![image](https://user-images.githubusercontent.com/36969394/198787952-9f818372-7684-42e1-ac30-a8ab05a5f478.png) * **Application {x} is not configured as a multi-tenant application.** * In your azure app go to Manifest, find the key "signInAudience", change its value to "AzureADandPersonalMicrosoftAccount" * **The logged in user is not authorized to fetch tokens for extension 'Microsoft_AAD_RegisteredApps' because the account is not a member of tenant 'xxxx'.** * Please make sure you have set the Supported accounts correctly as described in the [Prerequisites](./prerequisites.md) * **No token, or token doesn't have all required permissions; requesting authorization for account: Account1 Minimum required permissions not granted: ['Presence.Read', []]** * Where this error mentions `Presence.Read` or `Chat.Read` it probably means you have tried to configure a Teams Presence or Chat sensor when your account is a Personal Account (such as @outlook.com) * For other items, it means you have changed your configuration to require new permissions. You will likely need to delete your token and reauthenticate. Please check the [token page](./token.md) for more details. * **The user could not be authenticated as the grant is expired. The user must sign in again.** * Create a new secret and update your O365 configuration. Then delete your token and reauthenticate. Please check the [token page](./token.md) for more details. * **Client secret expired for account: xxxxxxxx. Create new client id in Azure App.** * The Client Secret on your Azure App has expired. Create a new secret and update your O365 configuration. * **Unable to fetch auth token. Error: (invalid_client) AADSTS7000215: Invalid client secret provided.** * Ensure the configured secret is the client secret __value__, not the client secret ID * **Token corrupt for account - please delete and re-authenticate.** * You will need to delete your token and reauthenticate. Please check the [token page](./token.md) for more details. * **O365 config requests permission: 'xxxxxx.xxxxxxx'. Not available in token 'o365_xxxxxxxx.token' for account 'xxxxxxxx'** Validate your Azure permissions match those required as detailed on the [permissions page](./permissions.md). If they are correct, you will need to delete your token and reauthenticate as described on the [token page](./token.md). **_Please note that any changes made to your Azure app settings takes a few minutes to propagate. Please wait around 5 minutes between changes to your settings and any auth attempts from Home Assistant._** # Installation issues * If your installation does not complete authentication, or the sensors are not created, please go back and ensure you have accurately followed the steps detailed, also look in the logs to see if there are any errors. ================================================ FILE: docs/events.md ================================================ --- title: Events nav_order: 16 --- # Events The attribute `ha_event` shows whether the event is triggered by an HA initiated action ## Calendar Events Events will be raised for the following items. - o365_create_calendar_event - Creation of a new event via the O365 integration - o365_modify_calendar_event - Update of an event via the O365 integration - o365_modify_calendar_recurrences - Update of a recurring event via the O365 integration - o365_remove_calendar_event - Removal of an event via the O365 integration - o365_remove_calendar_recurrences - Removal of a recurring event series via the O365 integration - o365_respond_calendar_event - Response to an event via the O365 integration The events have the following general structure: ```yaml event_type: o365_create_calendar_event data: event_id: >- AAMkAGQwYzQ5ZjZjLTQyYmItNDJmNy04NDNjLTJjYWY3NzMyMDBmYwBGAAAAAAC9VxHxYFrdCHSJkXtJ-BwCoiRErLbiNRJDCFyMjq4khAAY9v0_vAACoiRErLbiNRJDCFyMjq4khAAcZSY4SAAA= ha_event: true origin: LOCAL time_fired: "2023-02-19T15:29:01.962020+00:00" context: id: 01GSN4NWGABVFQQWPP2D8G3CN8 parent_id: null user_id: null ``` ## To-Do Events Events will be raised for the following items. - o365_new_todo - New to-do created either by the O365 integration or via some external app - o365_update_todo - Update of a to-do via the O365 integration - o365_delete_todo - Deletion of a to-do via the O365 integration - o365_completed_todo - To-do marked complete either by the O365 integration or via some external app (`show_completed` must be enabled for to-do list in `o365_tasks_xxxx.yaml`) - o365_uncompleted_todo - To-do marked incomplete via the O365 integration It should be noted that actions occurring external to HA are identified via a 30-second poll, so will very likely be delayed by up to that time. Any new or completed to-do occurring within 5 minutes before HA restart will very likely have a new event sent after the restart. The events have the following general structure. A `created` or `completed` attribute will be included where the action happened outside HA: ```yaml event_type: o365_new_todo data: todo_id: >- AAMkAGQwYzQ5ZjZjLTQyYmItNDJmNy04NDNjLTJjYWY3NzMyMDBGAAAAAAC9VxHxYFTdSrdCHSJkXtJ-BwCoiRErLbiNRJDCFyMjq4khAAbWN3xqAACoiRErLbiNRJDCFyMjq4khAAcZSXKvAAA= created: "2023-02-19T15:36:05.436266+00:00" ha_event: false origin: LOCAL time_fired: "2023-02-19T15:36:14.679300+00:00" context: id: 01GSN5332Q90ZKVEX0CZQNND73 parent_id: null user_id: null ``` ## Teams Status Events Events will be raised for the following items. - o365_update_user_status - User teams presence updated The events have the following general structure: ```yaml event_type: o365_update_user_status data: name: Joe Teams Status status: availability: Available activity: Available ha_event: true origin: LOCAL time_fired: "2024-02-12T18:22:36.694771+00:00" context: id: 01HPF8X14PYZ1QRZ8V199JSQTQ parent_id: null user_id: null ``` ## Teams Chat Events Events will be raised for the following items. - o365_send_chat_message - Message sent to specified chat via the O365 integration The events have the following general structure: ```yaml event_type: o365_send_chat_message data: chat_id: >- 19:5f6d6952-ace3-9999-9999-14af19704e05_99999999-a5c7-46da-8107-b25090a1ed66@unq.gbl.spaces ha_event: true origin: LOCAL time_fired: "2023-06-07T17:43:39.509758+00:00" context: id: 01H2BFA0QNCGEN2ZYRWGBFFHRF parent_id: null user_id: null ``` ================================================ FILE: docs/index.md ================================================ --- title: Home nav_order: 1 --- # Office 365 Integration for Home Assistant ## Deprecation Notice: The Calendar, Mail Notification and To Do entities and services are now deprecated in the O365 integration. Teams will also be deprecated in the near future at which point all development on O365 will stop unless it is required to support migration to the MS365 integrations. Details on how to migration to the new MS365 integrations can be found here - [Migration](https://rogerselwyn.github.io/O365-HomeAssistant/migration.html) This integration enables: 1. Getting and creating calendar events 2. Getting emails from your inbox using one of two available sensor types (e-mail and query) 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) 6. Getting and creating To-Dos 7. Setting Auto Reply/Out of Office response This project would not be possible without the wonderful [python-o365 project](https://github.com/O365/python-o365). Details on how to migration to the new `MS365` integrations can be found here - [Migration](migration.md) ================================================ FILE: docs/installation_and_configuration.md ================================================ --- title: Installation and Configuration nav_order: 4 --- # Installation and Configuration ## Installation 1. Ensure you have followed the [prerequisites instructions](./prerequisites.md) * Ensure you have a copy of the Client ID and the Client Secret **Value** (not the ID) 1. Optionally you can set up the [permissions](./permissions.md), alternatively you will be requested to approve permissions when you authenticate to MS 365. 1. Install this integration: * Recommended - [Home Assistant Community Store (HACS)](https://hacs.xyz/) or * Manually - Copy [these files](https://github.com/RogerSelwyn/O365-HomeAssistant/tree/master/custom_components/o365) to custom_components/o365/. 1. Restart your Home Assistant instance to enable the integration 1. Add o365 configuration to configuration.yaml using the [Configuration example](#configuration-examples) below. 1. Restart your Home Assistant instance again to enable your configuration. 1. A notification will be shown in the Repairs dialogue of your HA instance. Follow the instructions on this notification (or see [Authentication](./authentication.md)) to establish the link between this integration and the Azure app * A persistent token will be created in the hidden directory config/o365_storage/.O365-token-cache * The `o365_calendars_.yaml` will be created under the config directory in the `o365_storage` directory. * If todo_sensors is enabled then `o365_tasks_.yaml` will be created under the config directory in the `o365_storage` directory. 1. [Configure Calendars](./calendar_configuration.md) 1. [Configure To-Dos](./todos_configuration.md) (if required) 1. Restart your Home Assistant instance. **Note** If your installation does not complete authentication, or the sensors are not created, please go back and ensure you have accurately followed the steps detailed, also look in the logs to see if there are any errors. You can also look at the [errors page](./errors.md) for some other possibilities. ## Configuration examples ### Configuration format ```yaml o365: accounts: - account_name: Account1 # Do not use email address or spaces client_id: "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx" client_secret: "xx.xxxxxxxxxxxxxxxxxxxxxxxxxxxx" alt_auth_method: False enable_update: False email_sensor: - name: inbox max_items: 2 is_unread: True download_attachments: False query_sensors: - name: "Example" folder: "Inbox/Test_Inbox" #Default is Inbox from: "mail@example.com" subject_contains: "Example subject" has_attachment: True max_items: 2 is_unread: True status_sensors: # Cannot be used for personal accounts - name: "User Teams Status" chat_sensors: # Cannot be used for personal accounts - name: "User Chat" todo_sensors: enabled: False enable_update: False # set this to true if you want to be able to create new todos - account_name: Account2 client_secret: "xx.xxxxxxxxxxxxxxxxxxxxxxxxxxxx" client_id: "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx" ``` ### Configuration variables Key | Type | Required | Description -- | -- | -- | -- `account_name` | `string` | `True` | Uniquely identifying name for the account. Calendars entity names will be suffixed with this. `calendar.calendar_account1`. Do not use email address or spaces. `client_id` | `string` | `True` | Client ID from your O365 application. `client_secret` | `string` | `True` | Client Secret from your O365 application. `alt_auth_method` | `boolean` | `False` | If False (default), authentication is not dependent on internet access to your HA instance. [See Authentication](./authentication.md) `enable_calendar` | `boolean` | `False` | If True (**default is True**), this will enable calendars within the integration `enable_update` | `boolean` | `False` | If True (**default is False**), this will enable the various services that allow the sending of emails and updates to calendars `basic_calendar` | `boolean` | `False` | If True (**default is False**), the permission requested will be `calendar.ReadBasic`. `enable_update: true` = true cannot be used if `basic_calendar: true` `groups` | `boolean` | `False` | If True (**default is False**), will enable support for group calendars. No discovery is performed. You will need to know how to get the group ID from the MS Graph API. *Not for use on shared mailboxes* `track_new_calendar` | `boolean` | `False` | If True (default), will automatically generate a calendar_entity when a new calendar is detected. The system scans for new calendars only on startup. `email_sensors` | `list` | `False` | List of email_sensor config entries `query_sensors` | `list` | `False` | List of query_sensor config entries `status_sensors` | `list` | `False` | List of status_sensor config entries. *Not for use on personal accounts or shared mailboxes* `chat_sensors` | `list` | `False` | List of chat_sensor config entries. *Not for use on personal accounts or shared mailboxes* `todo_sensors` | `object` | `False` | To-Do List options *Not for use on shared mailboxes* `auto_reply_sensors` | `object` | `False` | Auto-reply sensor options *Not for use on shared mailboxes* `shared_mailbox` | `string` | `False` | Email address or ID of shared mailbox *Only available for calendar and email sensors* #### email_sensors Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. `folder` | `string` | `False` | Mail folder to monitor, for nested calendars separate with '/' ex. "Inbox/SubFolder/FinalFolder" Default is Inbox `max_items` | `integer` | `False` | Max number of items to retrieve (default 5) `is_unread` | `boolean` | `False` | True=Only get unread, False=Only get read, Not set=Get all `download_attachments` | `boolean` | `False` | **True**=Download attachments, False=Don't download attachments `show_body` | `boolean` | `False` | **True**=Show body on entity, False=Don't show body on entity `html_body` | `boolean` | `False` | True=Output HTML body, **False**=Output plain text body #### query_sensors Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. `folder` | `string` | `False` | Mail folder to monitor, for nested calendars separate with '/' ex. "Inbox/SubFolder/FinalFolder" Default is Inbox `max_items` | `integer` | `False` | Max number of items to retrieve (default 5) `is_unread` | `boolean` | `False` | True=Only get unread, False=Only get read, Not set=Get all `from` | `string` | `False` | Only retrieve emails from this email address `has_attachment` | `boolean` | `False` | True=Only get emails with attachments, False=Only get emails without attachments, Not set=Get all `importance` | `string` | `False` | Only get items with 'low'/'normal'/'high' importance `subject_contains` | `string` | `False` | Only get emails where the subject contains this string (Mutually exclusive with `subject_is`) `subject_is` | `string` | `False` | Only get emails where the subject equals exactly this string (Mutually exclusive with `subject_contains`) `download_attachments` | `boolean` | `False` | **True**=Download attachments, False=Don't download attachments `html_body` | `boolean` | `False` | True=Output HTML body, **False**=Output plain text body `show_body` | `boolean` | `False` | **True**=Show body on entity, False=Don't show body on entity `body_contains` | `string` | `False` | Only get emails where the body contains this string #### status_sensors (not for personal accounts) Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. `enable_update` | `boolean` | `False` | If True (**default is False**), this will enable the services to update user status. `email address` key must not be present. `email` | `string` | `False` | Enter email address to monitor status for. `enable_update` key must not be present. #### chat_sensors (not for personal accounts) Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. `enable_update` | `boolean` | `False` | If True (**default is False**), this will enable the services to send messages to a chat #### todo_sensors Key | Type | Required | Description -- | -- | -- | -- `enabled` | `boolean` | `True` | True=Enables To-Do Lists, **False**=Disables To-Do Lists. `enable_update` | `boolean` | `False` | If True (**default is False**), this will enable the services to create/update/delete to-dos `track_new` | `boolean` | `False` | If True (default), will automatically generate a todo_entity when a new to-do list is detected. The system scans for new to-do lists only on startup. #### auto_reply_sensors Key | Type | Required | Description -- | -- | -- | -- `name` | `string` | `True` | The name of the sensor. ================================================ FILE: docs/migration.md ================================================ --- title: Migration nav_order: 19 --- # Migration **Please upgrade to the latest version of this O365 integration, prior to migrating to the MS365 integrations** ## Migration Service The `o365.migrate_config` service enables the migration from the Office 365 Integration for Home Assistant to the new Microsoft 365 suite of integrations. The new suite is made up of: * [MS365 Calendar](https://github.com/RogerSelwyn/MS365-Calendar) * [MS365 Mail](https://github.com/RogerSelwyn/MS365-Mail) * [MS365-Teams](https://github.com/RogerSelwyn/MS365-Teams) * [MS365-ToDo](https://github.com/RogerSelwyn/MS365-ToDo) For the service to function fully it requires the relevant MS365 integrations to have been installed on the Home Assistant server. ### Note You will need to be on consitent versions of the integrations in order to minimise errors: * O365 < 5.0.0 - Needs MS365 < 1.3.0 * O365 >= 5.0.0 - Needs MS365 >= 1.3.0 ### o365.migrate_config The service will attempt to create config entries for any sensors that would have been created via the O365 YAML based configuration. You will see repair requests for them to be re-configured to authenticate to Microsoft. To do this: * Select the config item in `Devices & Services` * Click on the 3 vertical dots * Select `Reconfigure` You should see that all your configuration has been retained. You should only need to click Next/Submit to reach the dialogue where you can request Microsoft authentication. When successfully authenticated, your entities will be created and enabled. *Note* your entities will have new names, but can be renamed to replace existing O365 entities if needed, so that automations/etc don't need to be modified. * Calendars - One entry for each O365 account * Mail * One entry for each Inbox/Query O365 sensor. They are now treated equally. * AutoReply - This will be associated with one of your mail entries * Teams * A combined entry for O365 chat/status which do not an alternate email to monitor for status. * A separate entry for each O365 status sensor that has an alternate email to monitor * To Dos - One entry for each O365 account The calendars.yaml, tasks.yaml (now named todos.yaml) and token files will be in the new `ms365_storage` folder in your configuration. ### Post migration When you have migrated over the following can be removed: * O365 can be removed from configuration.yaml * O365 can be uninstalled from HACS * The o365_storage folder can be deleted. If you did not use the migration service and created a new Entra App, you can delete the O365 application from Azure by clicking on `App registrations` or `Applications from personal account` [here](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade). ================================================ FILE: docs/permissions.md ================================================ --- title: Permissions nav_order: 3 --- # Permissions Under "API Permissions" click Add a permission, then Microsoft Graph, then Delegated permission, and add the permissions as detailed in the list and table below: * Calendar - For calendars *Note the requirement for `.Shared` permissions for shared mailbox calendars* * Email - For an email_sensor or a query_sensor *Note the requirement for `.Shared` permissions for shared mailboxes* * Status - For a status_sensor * Chat - For a chat_sensor * ToDo - For a todo_sensor * Group Calendar - For a manually added Group calendar * AutoReply - For Auto reply/Out of Office message configuration If you intend to send emails use calendar update functionality, then set [enable_update](./installation_and_configuration.md#configuration_variables) at the top level to `true`. For other sensors set [enable_update](installation_and_configuration.md) to true for each sensor supporting it. Then for any sensor type, add the relevant `ReadWrite` permission as denoted by a `Y` in the update column. | Feature | Permissions | Update | O365 Description | Notes | |----------|-----------------------|:------:|---------------------------------------|-------| | Calendar | offline_access | | *Maintain access to data you have given it access to* | | | Calendar | Calendars.ReadBasic | | *Read basic details of user calendars* | Used when `basic_calendar` is set to `true` | | Calendar | Calendars.Read | | *Read user calendars* | | | Calendar | Calendars.ReadWrite | Y | *Read and write user calendars* | | | Calendar | Calendars.Read.Shared | | *Read user and shared calendars* | For shared mailboxes | | Calendar | Calendars.ReadWrite.Shared | Y | *Read and write user and shared calendars* | For shared mailboxes | | Calendar | User.Read | | *Sign in and read user profile* | | | Email | Mail.Read | | *Read access to user mail* | | | Email | Mail.Send | Y | *Send mail as a user* | | | Email | Mail.Read.Shared | | *Read user and shared mail* | For shared mailboxes | | Email | Mail.Send.Shared | Y | *Send mail on behalf of others* | For shared mailboxes | | Status | Presence.Read | | *Read user's presence information* | Not for personal accounts/shared mailboxes | | Status | Presence.ReadWrite | Y | *Read and write a user's presence information* | Not for personal accounts/shared mailboxes | | Status | Presence.Read.All | | *Read presence information of all users in your organization* | Used if you want to monitor another user's status. Not for personal accounts/shared mailboxes | | Status | User.ReadBasic.All | | *Read all users' basic profiles* Used if you want to monitor another user's status. Not for personal accounts/shared mailboxes | | Chat | Chat.Read | | *Read user chat messages* | Not for personal accounts/shared mailboxes | | Chat | Chat.ReadWrite | Y | *Read and write user chat messages* | Not for personal accounts/shared mailboxes | | ToDo | Tasks.Read | | *Read user's tasks and task lists* | Not for shared mailboxes | | ToDo | Tasks.ReadWrite | Y | *Create, read, update, and delete user’s tasks and task lists* | Not for shared mailboxes | | Group Calendar | Group.Read.All | | *Read all groups* | Not supported in shared mailboxes | | Group Calendar | Group.ReadWrite.All | Y | *Read and write all groups* | Not supported in shared mailboxes | | AutoReply | MailboxSettings.ReadWrite | | *Read and write user mailbox settings* | Not for shared mailboxes | **Note** It should be noted that these are the permissions that are requested at authentication time (as appropriate for each sensor configured). When `enable_update` is configured to `true` all the associated `ReadWrite` permissions are requested as well, however you do not need to add `ReadWrite` for any sensor type where you do not what update permissions, it will still act as a Read Only sensor. This excludes the AutoReply option which is only `ReadWrite`. For example, permissions as below (and with `enable_update` set to `true`) will create calendar sensors, create chat sensors, and create auto reply enable/disable services but will not enable create/modify/remove/respond services: ```json "scope": [ "Calendars.Read", "Chat.Read", "MailboxSettings.ReadWrite", "User.Read", ] ``` ## Changing Features and Permissions If you decide to enable new features in the integration, or decide to change from read only to read/write, you will very likely get a warning message similar to the following in your logs. `Minimum required permissions not granted: ['Tasks.Read', ['Tasks.ReadWrite']]` You will need to delete as detailed on the [token page](./token.md) ================================================ FILE: docs/prerequisites.md ================================================ --- title: Prerequisites nav_order: 2 --- # Prerequisites ## Note - Personal accounts Since the middle of 2024, Microsoft has mandated that any new app registrations must be created within an Entra ID directory. You will need to sign up for [Azure](https://azure.microsoft.com/en-gb/free) on a pay as you go basis, where Microsoft Entra ID is indicated as always being free. ## Getting the client ID and client secret To allow authentication, you first need to register your application at Azure App Registrations: 1. Login at [Azure Portal (App Registrations)](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade). Personal accounts may receive an authentication notification that can be ignored. 2. Create a new App Registration. Give it a name. In Supported account types, choose one of the following as needed by your setup: * `Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)`. * `Accounts in any organizational directory (Any Azure AD directory - Multitenant)` **Do not use the following:** * `Accounts in this organizational directory only (xxxxx only - Single tenant)` * `Personal Microsoft accounts only` 3. Click Add a Redirect URI. Click Add a platform. Select Web. Set redirect URI to `https://login.microsoftonline.com/common/oauth2/nativeclient`. Leave the other fields blank and click Configure. An alternate method of authentication is available which requires internet access to your HA instance if preferred. The alternate method is simpler to use when authenticating, but is more complex to set up correctly. See [Authentication](./authentication.md) section for more details. 4. From the Overview page, copy the Application (client) ID. 5. Under "Certificates & secrets", generate a new client secret. Set the expiration as desired. This appears to be limited to 2 years. Copy the **Value** of the client secret now (not the ID), it will be hidden later on. If you lose track of the secret, return here to generate a new one. 6. Under "API Permissions" click Add a permission, then Microsoft Graph, then Delegated permission, and add the permissions as detailed on the [permissions page](./permissions.md). ================================================ FILE: docs/sensor.md ================================================ --- title: Sensors nav_order: 8 --- # Sensors ## Calendar Sensor The status of the calendar sensor indicates (on/off) whether there is an event on at the current time. The `message`, `all_day`, `start_time`, `end_time`, `location`, `description` and `offset_reached` attributes provide details of the current of next event. A non-all-day event is favoured over all_day events. The `data` attribute provides an array of events for the period defined by the `start_offset` and `end_offset` in `o365_calendars_.yaml`. Individual array elements can be accessed using the template notation `states.calendar.calendar_.attributes.data[0...n]`. ## Teams Status Sensor The Teams Status sensor shows the user's current status on Teams: * `available` * `away` * `beRightBack` * `busy` * `doNotDisturb` * `inACall` * `inAConferenceCall` * `inactive` * `inAMeeting` * `offline` * `offWork` * `outOfOffice` * `presenceUnknown` * `presenting` * `urgentInterruptionsOnly` ## Teams Chat Sensor Shows the latest chat found on MS Teams. Shows the date and time as the status of the sensor, plus content, ID and importance of the chat item. The `data` attribute provides an array of chats (max 20), including chat_id and supporting information. Individual array elements can be accessed using the template notation `states.sensor..attributes.data[0...n]`. ## Auto Reply Sensor Shows the current auto reply settings for your account. Supports the enabling and disabling of auto reply. Note that all attributes are displayed even if auto reply is disabled for reference purposes. ================================================ FILE: docs/services.md ================================================ --- title: Services nav_order: 15 --- # Services ## Notify Services ### notify.o365_email_xxxxxxxx #### Service data Key | Type | Required | Description -- | -- | -- | -- `message` | `string` | `True` | The email body `title` | `string` | `False` | The email subject `data` | `dict` | `False` | Additional attributes - see table below #### Extended data Key | Type | Required | Description -- | -- | -- | -- `target` | `string` | `False` | Recipient of the email, if not set will use the configured account's email address `sender` | `string` | `False` | Sender of the email, if not set will use the configured account's email address - where the authenticated user has been delegated access to the mailbox `message_is_html` | `boolean` | `False` | Is the message formatted as HTML `importance` | `string` | `False` | Set importance to `low`, `medium` or `high` `photos` | `list` | `False` | File paths or URLs of pictures to embed into the email body `attachments` | `list` | `False` | File paths to attach to email `zip_attachments` | `boolean` | `False` | Zip files from attachments into a zip file before sending `zip_name` | `string` | `False` | Name of the generated zip file #### Example notify service call ```yaml service: notify.o365_email_xxxxxxxx data: message: The garage door has been open for 10 minutes. title: Your Garage Door Friend data: target: joebloggs@hotmail.com sender: mgmt@noname.org.uk message_is_html: true attachments: - "/config/documents/sendfile.txt" zip_attachments: true zip_name: "zipfile.zip" photos: - "/config/documents/image.jpg" ``` ## Calendar Services o365.create_calendar_event Create an event in the specified calendar - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.modify_calendar_event Modify an event in the specified calendar - All parameters are shown in the available parameter list on the Developer Tools/Services tab. Not possible for group calendars. ### o365.remove_calendar_event Remove an event in the specified calendar - All parameters are shown in the available parameter list on the Developer Tools/Services tab. Not possible for group calendars. ### o365.respond_calendar_event Respond to an event in the specified calendar - All parameters are shown in the available parameter list on the Developer Tools/Services tab. Not possible for group calendars. ### o365.scan_for_calendars Scan for new calendars and add to o365_calendars.yaml - No parameters. Does not scan for group calendars. #### Example create event service call ```yaml service: o365.create_calendar_event target: entity_id: - calendar.user_primary data: subject: Clean up the garage start: 2023-01-01T12:00:00+0000 end: 2023-01-01T12:30:00+0000 body: Remember to also clean out the gutters location: 1600 Pennsylvania Ave Nw, Washington, DC 20500 sensitivity: Normal show_as: Busy attendees: - email: test@example.com type: Required ``` ## To-Do Services These O365 services must be targeted at a `todo` sensor. Alternatively the core To-Do services (e.g.`todo.add_item`) can be used. The core services do not support reminder date/time setting. The intention is to phase out the O365 services once the core services provide full functionality. ### o365.new_todo Create a new To-Do - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.update_todo Update a To-Do - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.delete_todo Delete a To-Do - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.complete_todo (Un)complete a To-Do - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.scan_for_todo_lists Scan for new for to-do lists and add to o365_tasks.yaml - No parameters. #### Example create to-do service call ```yaml service: o365.new_todo target: entity_id: todo.hass_primary data: subject: Pick up the mail description: Walk to the post box and collect the mail due: "2023-01-01" # Note that due only takes a date, not a datetime reminder: 2023-01-01T12:00:00+0000 ``` ## Auto reply Services These services must be targeted at `auto_reply` sensors. ### o365.set_auto_reply Schedule the auto reply - All parameters are shown in the available parameter list on the Developer Tools/Services tab. ### o365.disable_auto_reply Disable the auto reply - All parameters are shown in the available parameter list on the Developer Tools/Services tab. #### Example enable auto reply service call ```yaml service: o365.auto_reply_enable target: entity_id: sensor.inbox data: external_reply: I'm currently on holliday, please email Bob for answers internal_reply: I'm currently on holliday start: 2023-01-01T12:00:00+0000 end: 2023-01-02T12:30:00+0000 external_audience: all ``` ## Chat Services These services must be targeted at a `chat` sensor. ### o365.send_chat_message Send message to specified chat - All parameters are shown in the available parameter list on the Developer Tools/Services tab. #### Example send chat message service call ```yaml service: o365.send_chat_message target: entity_id: sensor.roger_chats data: chat_id: xxxxxxxxxxxxxxxxxxxxxxxxx message: Hello world content_type: text ``` ## Status Services These services must be targeted at a `status` sensor. They can only target the logged-in user's status. ### o365.update_user_status Update Teams status for the logged in client. This will not override a status that is set via the MS Teams client. Allowable pairings of availability and activity are show in the [MS Graph Documentation](https://learn.microsoft.com/en-us/graph/api/presence-setpresence?view=graph-rest-1.0&tabs=http#request-body). The expiration/duration field is also documented on the same page. It defaults to 5 minutes. #### Example update user status service call ```yaml service: o365.update_user_status data: availability: Busy activity: InACall expiration_duration: PT1H target: entity_id: sensor.roger_teams_status ``` ### o365.update_user_preferred_status Update Teams preferred status for the logged-in user. This is equivalent to setting status within the Teams client. Allowable pairings of availability and activity are show in the [MS Graph Documentation](https://learn.microsoft.com/en-us/graph/api/presence-setuserpreferredpresence?view=graph-rest-1.0&tabs=http#request-body). The expiration/duration field is also documented on the same page. If not provided, a default expiration will be applied: DoNotDisturb or Busy - Expiration in 1 day; All others - Expiration in 7 days #### Example update user preferred status service call ```yaml service: o365.update_user_preferred_status data: availability: Offline expiration_duration: PT1H target: entity_id: sensor.roger_teams_status ``` ================================================ FILE: docs/todo.md ================================================ --- title: To-Do Lists nav_order: 9 --- # To-Do Lists One To-Do List entity is created for each to-do list on the user account. Each sensor shows the number of incomplete to-do items as the status of the sensor. The `all_todos` attribute is an array of incomplete to-dos. The `overdue_todos` attribute shows any to-dos which have a due date and are overdue as an array. ### Display In order to show the to-dos in the front end either use the HA inbuilt To-Do panel or a markdown card can be used. The following is an example that allows you to display a bulleted list subject from the `all_todos` array of todos. ```yaml type: markdown title: Todos content: |- {% raw %}{% for todo in state_attr('todo.todos_sc_personal', 'all_todos') -%} - {{ todo['subject'] }} {% endfor %}{% endraw %} ``` ================================================ FILE: docs/todos_configuration.md ================================================ --- title: To-Do Configuration nav_order: 7 --- # ToDo configuration The integration uses an external `o365_tasks_.yaml` file which is stored in the `o365_storage` directory. ## Example Tasks yaml: ```yaml - name: Tasks task_list_id: xxxx track: false - name: HASS task_list_id: xxxx track: true ``` ### Tasks yaml configuration variables Key | Type | Required | Description -- | -- | -- | -- `task_list_id` | `string` | `True` | O365 generated unique ID, DO NOT CHANGE `name` | `string` | `True` | The name of your sensor that you’ll see in the frontend. `track` | `boolean` | `False` | **True**=Create sensor entity. False=Don't create entity. `show_completed` | `boolean` | `False` | True=Show completed items. **False**=Don't show completed items. `due_start_offset` | `integer` | `False` | Number of hours to offset the due time for to-do to retrieve (negative numbers to offset into the past). `due_end_offset` | `integer` | `False` | Number of hours to offset the due time for to-do to retrieve (negative numbers to offset into the past). ================================================ FILE: docs/token.md ================================================ --- title: Token nav_order: 18 --- # Token At some point you are very likely need to delete your token so that you re-authenticate to Office 365. This may be because you have added new features to your configuration or your secret has expired. To do this delete the relevant token file (according to the account name in your config) from the `/o365_storage/.O365-token-cache` directory. When you restart HA, you will then be prompted to re-authenticate with O365 which will store a new token. ================================================ FILE: hacs.json ================================================ { "name": "Office 365", "zip_release": true, "filename": "o365.zip", "homeassistant": "2024.6.0", "content_in_root": false, "render_readme": true } ================================================ FILE: requirements.txt ================================================ O365>=2.1.4 BeautifulSoup4>=4.10.0 oauthlib ================================================ FILE: requirements_release.txt ================================================ PyGithub>=1.51 ruff==0.14.2