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