[
  {
    "path": ".coveragerc",
    "content": "[run]\nsource = homeassistant_cli\n\n[report]\n# Regexes for lines to exclude from consideration\nexclude_lines =\n    # Have to re-enable the standard pragma\n    pragma: no cover\n\n    # Don't complain about missing debug-only code:\n    def __repr__\n\n    # Don't complain if tests don't hit defensive assertion code:\n    raise AssertionError\n    raise NotImplementedError\n"
  },
  {
    "path": ".dockerignore",
    "content": "# General files\n.git\n.github\nconfig\n\n# Test related files\n.tox\n\n# Other virtualization methods\nvenv\n.vagrant\n\n# Temporary files\n**/__pycache__\n.#*\n"
  },
  {
    "path": ".github/release-drafter.yml",
    "content": "template: |\n  ## What's Changed\n\n  $CHANGES\n"
  },
  {
    "path": ".github/workflows/publish-to-pypi.yml",
    "content": "name: Publish to PyPI\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n\njobs:\n  deploy:\n\n    runs-on: ubuntu-latest\n\n    environment: release\n    permissions:\n      id-token: write\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.13'\n        cache: 'pip'\n\n    - name: Install Poetry\n      uses: snok/install-poetry@v1\n      with:\n        virtualenvs-create: true\n        virtualenvs-in-project: true\n        installer-parallel: true\n\n    - name: Load cached venv\n      id: cached-poetry-dependencies\n      uses: actions/cache@v5\n      with:\n        path: .venv\n        key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}\n\n    - name: Install dependencies\n      if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'\n      run: poetry install --no-interaction --no-root\n\n    - name: Install library\n      run: poetry install --no-interaction\n\n    - name: Run tests\n      run: |\n        source .venv/bin/activate\n        pytest tests/\n\n    - name: Build package\n      run: poetry build\n\n    - name: Publish package distributions to PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/testing.yml",
    "content": "name: Testing package\n\non:\n  push:\n    branches: [ master, dev ]\n  pull_request:\n    branches: [ master, dev ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [ \"3.13\", \"3.14\" ]\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v6\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Install Poetry\n      uses: snok/install-poetry@v1\n      with:\n        virtualenvs-create: true\n        virtualenvs-in-project: true\n        installer-parallel: true\n\n    - name: Load cached venv\n      id: cached-poetry-dependencies\n      uses: actions/cache@v5\n      with:\n        path: .venv\n        key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}\n\n    - name: Install dependencies\n      if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'\n      run: poetry install --no-interaction --no-root\n\n    - name: Install library\n      run: poetry install --no-interaction\n\n    - name: Run tests\n      run: |\n        source .venv/bin/activate\n        pytest tests/\n\n    - name: Lint with ruff\n      run: |\n        pip install ruff\n        ruff check .\n"
  },
  {
    "path": ".gitignore",
    "content": "# Hide sublime text stuff\n*.sublime-project\n*.sublime-workspace\n\n# Hide vscode\n.vscode\n*.code-workspace\n\n# Hide some OS X stuff\n.DS_Store\n.AppleDouble\n.LSOverride\nIcon\n\n# Thumbnails\n._*\n\n.idea\n\n# pytest\n.cache\nhtmlcov\n\n# GitHub Proposed Python stuff:\n*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\n.eggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\nlib\nlib64\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\nhtmlcov\nnosetests.xml\n\n# Translations\n*.mo\n\n# Mr Developer\n.mr.developer.cfg\n.project\n.pydevproject\n\n.python-version\n\n# emacs auto backups\n*~\n*#\n*.orig\n\n# venv stuff\npyvenv.cfg\npip-selfcheck.json\nvenv\n.venv\n.Python\ninclude\n\n# vimmy stuff\n*.swp\n*.swo\n\nctags.tmp\n\n# vagrant stuff\nvirtualization/vagrant/setup_done\nvirtualization/vagrant/.vagrant\nvirtualization/vagrant/config\n\n# pytest\n.pytest_cache\n\n# share/man ignore\nshare\n\n# ignored to make check_dirty not fail\ntravis_wait*\n.mypy_cache\n\nREADME.html\npip-wheel-metadata\n.xprocess\nresults.xml\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n  - repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.10\n    hooks:\n      - id: ruff-format\n        files: ^((homeassistant_cli|script|tests)/.+)?[^/]+\\.py$\n      - id: ruff\n        args: [--fix]\n        files: ^((homeassistant_cli|script|tests)/.+)?[^/]+\\.py$\n  - repo: https://github.com/codespell-project/codespell\n    rev: v2.4.2\n    hooks:\n      - id: codespell\n        args:\n          - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing\n          - --skip=\"./.*,*.json\"\n          - --quiet-level=2\n        exclude_types: [json]\n  - repo: https://github.com/PyCQA/bandit\n    rev: 1.9.4\n    hooks:\n      - id: bandit\n        args:\n          - --quiet\n          - --format=custom\n          - --configfile=tests/bandit.yaml\n        files: ^(homeassistant_cli|tests)/.+\\.py$\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-executables-have-shebangs\n        stages: [manual]\n      - id: check-json\n      - id: no-commit-to-branch\n        args:\n          #- --branch=dev\n          - --branch=master\n          - --branch=rc\n      - id: trailing-whitespace\n      - id: end-of-file-fixer\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.13-alpine\nLABEL maintainer=\"Fabian Affolter <fabian@affolter-engineering.ch>\"\n\nWORKDIR /usr/src/app\n\nCOPY . .\n\nRUN apk add --no-cache --virtual build-dependencies gcc musl-dev\\\n    &&  rm -rf /var/cache/apk/*\n\nRUN pip3 install --upgrade pip; pip3 install --no-cache-dir -e .\n\nENTRYPOINT [\"hass-cli\"]\n"
  },
  {
    "path": "Dockerfile.armhf",
    "content": "# Python 3.11 with Alpine\nFROM balenalib/armv7hf-alpine-python:3.11-3.15\nLABEL maintainer=\"Fabian Affolter <fabian@affolter-engineering.ch>\"\n\nRUN [ \"cross-build-start\" ]\n\nRUN apk add --no-cache --virtual build-dependencies gcc musl-dev\\\n    &&  rm -rf /var/cache/apk/*\n\nWORKDIR /usr/src/app\nCOPY . .\nRUN pip3 install --upgrade pip; pip3 install --no-cache-dir -e .\n\nRUN [ \"cross-build-end\" ]\n\nENTRYPOINT [\"hass-cli\"]\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Apache License\n==============\n\n_Version 2.0, January 2004_\n_&lt;<http://www.apache.org/licenses/>&gt;_\n\n### Terms and Conditions for use, reproduction, and distribution\n\n#### 1. Definitions\n\n“License” shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n“Licensor” shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n“Legal Entity” shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, “control” means **(i)** the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the\noutstanding shares, or **(iii)** beneficial ownership of such entity.\n\n“You” (or “Your”) shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n“Source” form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n“Object” form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n“Work” shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n“Derivative Works” shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n“Contribution” shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n“submitted” means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as “Not a Contribution.”\n\n“Contributor” shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n#### 2. Grant of Copyright License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n#### 3. Grant of Patent License\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n#### 4. Redistribution\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\n* **(a)** You must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\n* **(b)** You must cause any modified files to carry prominent notices stating that You\nchanged the files; and\n* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\n* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\n\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n#### 5. Submission of Contributions\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n#### 6. Trademarks\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n#### 7. Disclaimer of Warranty\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an “AS IS” BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n#### 8. Limitation of Liability\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n#### 9. Accepting Warranty or Additional Liability\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\n_END OF TERMS AND CONDITIONS_\n\n### APPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets `[]` replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same “printed page” as the copyright notice for easier identification within\nthird-party archives.\n\n    Copyright [yyyy] [name of copyright owner]\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.rst\ninclude LICENSE.md\ngraft homeassistant_cli\ngraft tests\nrecursive-exclude * *.py[co]\n"
  },
  {
    "path": "README.rst",
    "content": "Home Assistant Command-line Interface (``hass-cli``)\n====================================================\n\n|Coverage| |License| |PyPI|\n\nThe Home Assistant Command-line interface (``hass-cli``) allows one to\nwork with a local or a remote `Home Assistant <https://home-assistant.io>`_\ninstance directly from the command-line.\n\n.. image:: https://asciinema.org/a/216235.png\n      :alt: hass-cli screencast\n      :target: https://asciinema.org/a/216235?autoplay=1&speed=1\n\n\nInstallation\n============\n\nTo use latest release:\n\n.. code:: bash\n\n    $ pip install homeassistant-cli\n\nTo use latest pre-release from ``dev`` branch:\n\n.. code:: bash\n\n   $ pip install git+https://github.com/home-assistant-ecosystem/home-assistant-cli@dev\n\nThe developers of `hass-cli` usually provide up-to-date `packages <https://src.fedoraproject.org/rpms/home-assistant-cli>`_ for recent Fedora and EPEL releases. Use ``dnf`` for the installation:\n\n.. code:: bash\n\n   $ sudo  dnf -y install home-assistant-cli\n\nThe community is providing support for macOS through `homebew <https://formulae.brew.sh/formula/homeassistant-cli#default>`_.\n\n.. code:: bash\n\n   $ brew install homeassistant-cli\n\nKeep in mind that the available releases in the distribution could be out-dated.\n\n``home-assistant-cli`` is also available for NixOS.\n\nTo use the tool on NixOS. Keep in mind that the latest release could only\nbe available in the ``unstable`` channel.\n\n.. code:: bash\n\n   $ nix-env -iA nixos.home-assistant-cli\n\nDocker\n-------\n\nIf you do not have a Python setup you can try use ``hass-cli`` via a container\nusing Docker.\n\n.. code:: bash\n\n   $ docker run homeassistant/home-assistant-cli\n\n\nTo make auto-completion and access environment work like other scripts you'll\nneed to create a script file to execute.\n\n.. code:: bash\n\n   $ curl https://raw.githubusercontent.com/home-assistant/home-assistant-cli/master/docker-hass-cli > hass-cli\n   $ chmod +x hass-cli\n\n\nNow put the ``hass-cli`` script into your path and you can use it like if you\nhad installed it via command line as long as you don't need file system access\n(like for ``hass-cli template``).\n\nSetup\n======\n\nTo get started you'll need to have or generate a long lasting token format\non your Home Assistant profile page (e. g., http://homeassistant.local:8123/profile\nthen scroll down to \"Long-Lived Access Tokens\").\n\nThen you can use ``--server`` and ``--token`` parameter on each call or as is\nrecommended setup ``HASS_SERVER`` and ``HASS_TOKEN`` environment variables.\n\n.. code:: bash\n\n    $ export HASS_SERVER=http://homeassistant.local:8123\n    $ export HASS_TOKEN=<secret>\n\n\nRemote API access\n-----------------\n\nFor Home Assistant Operating System users, the `Remote API proxy <https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access>`\nadd-on is needed. Keep in mind that this is not a feature for regular users as it allows access to\nthe Supervisor API. This is relevant for the ``ha`` commands in ``hass-cli``.\n\nInstall the add-on and start it. Once started you can get the Supervisor API key for the add-on via\nthe logs:\n\n.. code:: text\n\n    s6-rc: info: service legacy-services: starting\n    s6-rc: info: service legacy-services successfully started\n    Your API key is: 89400091.....d0897\n\n\nUse ``--supervisor-token`` or the ``HASS_SUPERVISOR_TOKEN`` environment variable.\n\n.. code:: bash\n\n    $ export HASS_SUPERVISOR_TOKEN=<supervisor_secret>\n\n\nAutomatic completion\n--------------------\n\nOnce that is enabled, run one of the following commands to enable\nautocompletion for ``hass-cli`` commands.\n\n.. code:: bash\n\n  $ source <(_HASS_CLI_COMPLETE=bash_source hass-cli) # for bash\n  $ source <(_HASS_CLI_COMPLETE=zsh_source hass-cli)  # for zsh\n  $ eval (_HASS_CLI_COMPLETE=fish_source hass-cli)    # for fish\n\n\nUsage\n=====\n\nBasic info\n----------\n\nNote: Below is listed **some** of the features, make sure to use ``--help`` and\nautocompletion to learn more of the features as they become available.\n\nMost commands returns a table version of what the Home Assistant API returns.\nFor example to get basic info about your Home Assistant server you use ``system``:\n\n.. code:: bash\n\n   $ hass-cli config release\n   VERSION\n   2026.4.1\n\n\nIf you prefer yaml you can use ``--output=yaml``:\n\n.. code:: bash\n\n    $ hass-cli --output=yaml config release\n      -  2026.4.1\n\n\nBackup\n------\n\nBackup can be created with command:\n\n.. code:: bash\n\n    $ hass-cli service list | grep backup\n    $ hass-cli service call backup.create\n\nStates\n------\n\nTo get list of states you use `state list`:\n\n.. code:: bash\n\n    $ hass-cli state list\n    ENTITY                                                     DESCRIPTION                                     STATE\n    zone.school                                                School                                          zoning\n    zone.home                                                  Andersens                                       zoning\n    sun.sun                                                    Sun                                             below_horizon\n    camera.babymonitor                                         babymonitor                                     idle\n    timer.timer_office_lights                                                                                  idle\n    timer.timer_small_bathroom                                                                                 idle\n    [...]\n\n\nYou can use ``--no-headers`` to suppress the header.\n\n``--table-format`` let you select which table format you want. Default is\n``simple`` but you can use any of the formats supported by https://pypi.org/project/tabulate/:\n``plain``, ``simple``, ``github``, ``grid``, ``fancy_grid``, ``pipe``,\n``orgtbl``, ``rst``, ``mediawiki``, ``html``, ``latex``, ``latex_raw``,\n``latex_booktabs`` or ``tsv``\n\nFinally, you can also via ``--columns`` control which data you want shown.\nEach column has a name and a jsonpath. The default setup for entities are:\n\n``--columns=ENTITY=entity_id,DESCRIPTION=attributes.friendly_name,STATE=state,CHANGED=last_changed``\n\nIf you for example just wanted the name and all attributes you could do:\n\n.. code:: bash\n\n   $ hass-cli --columns=ENTITY=\"entity_id,ATTRIBUTES=attributes[*]\" state list zone\n   ENTITY             ATTRIBUTES\n   zone.school        {'friendly_name': 'School', 'hidden': True, 'icon': 'mdi:school', 'latitude': 7.011023, 'longitude': 16.858151, 'radius': 50.0}\n   zone.unnamed_zone  {'friendly_name': 'Unnamed zone', 'hidden': True, 'icon': 'mdi:home', 'latitude': 37.006476, 'longitude': 2.861699, 'radius': 50.0}\n   zone.home          {'friendly_name': 'Andersens', 'hidden': True, 'icon': 'mdi:home', 'latitude': 27.006476, 'longitude': 7.861699, 'radius': 100}\n\nYou can get more details about a state by using ``yaml`` or ``json`` output\nformat. In this example we use the shorthand of output: ``-o``:\n\n.. code:: bash\n\n    $ hass-cli -o yaml state get light.guestroom_light                                                                                                                                                                       ◼\n    attributes:\n      friendly_name: Guestroom Light\n      supported_features: 61\n    context:\n      id: 84d52fe306ec4895948b546b492702a4\n      user_id: null\n    entity_id: light.guestroom_light\n    last_changed: '2018-12-10T18:33:51.883238+00:00'\n    last_updated: '2018-12-10T18:33:51.883238+00:00'\n    state: 'off'\n\nYou can edit state via an editor:\n\n.. code:: bash\n\n    $ hass-cli state edit light.guestroom_light\n\nThis will open the current state in your favorite editor and any changes you\nsave will be used for an update.\n\nYou can also explicitly create/edit via the ``--json`` flag:\n\n.. code:: bash\n\n  $ hass-cli state edit sensor.test --json='{ \"state\":\"off\"}'\n\nList possible services with or without a regular expression filter:\n\nServices\n--------\n\n.. code:: bash\n\n    $ hass-cli service list 'home.*toggle'\n      DOMAIN         SERVICE    DESCRIPTION\n      homeassistant  toggle     Generic service to toggle devices on/off...\n\nFor more details the YAML format is useful:\n\n.. code:: bash\n\n    $ hass-cli -o yaml service list homeassistant.toggle\n    homeassistant:\n      services:\n        toggle:\n          description: Generic service to toggle devices on/off under any domain. Same\n            usage as the light.turn_on, switch.turn_on, etc. services.\n          fields:\n            entity_id:\n              description: The entity_id of the device to toggle on/off.\n              example: light.living_room\n\nYou can get history about one or more entities, here getting state changes for the last\n50 minutes:\n\n.. code:: bash\n\n   $ hass-cli state history --since 50m light.kitchen_light_1 binary_sensor.presence_kitchen\n     ENTITY                          DESCRIPTION      STATE    CHANGED\n     binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:19:55.322474+00:00\n     binary_sensor.presence_kitchen  Kitchen Motion   on       2019-01-27T23:21:44.015071+00:00\n     binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:22:02.330566+00:00\n     light.kitchen_light_1           Kitchen Light 1  on       2019-01-27T23:19:55.322474+00:00\n     light.kitchen_light_1           Kitchen Light 1  off      2019-01-27T23:36:45.254266+00:00\n\nThe data is sorted by default as Home Assistant returns it, thus for history it is useful\nto sort by a property:\n\n.. code:: bash\n\n   $ hass-cli --sort-by last_changed state history --since 50m  light.kitchen_light_1 binary_sensor.presence_kitchen\n   ENTITY                          DESCRIPTION      STATE    CHANGED\n   binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:18:00.717611+00:00\n   light.kitchen_light_1           Kitchen Light 1  on       2019-01-27T23:18:00.717611+00:00\n   binary_sensor.presence_kitchen  Kitchen Motion   on       2019-01-27T23:18:12.135015+00:00\n   binary_sensor.presence_kitchen  Kitchen Motion   off      2019-01-27T23:18:30.417064+00:00\n   light.kitchen_light_1           Kitchen Light 1  off      2019-01-27T23:36:45.254266+00:00\n\nNote: the `--sort-by` argument is referring to the attribute in the underlying\n``json``/``yaml`` NOT the column name. The advantage for this is that it can\nbe used for sorting on any property even if not included in the default output.\n\nAreas and Device Registry\n-------------------------\n\nSince v0.87 of Home Assistant there is a notion of Areas in the Device registry. ``hass-cli`` lets\nyou list devices and areas and assign areas to devices.\n\nListing devices and areas works similar to list Entities.\n\n.. code:: bash\n\n   $ hass-cli device list\n   ID                                NAME                           MODEL                            MANUFACTURER        AREA\n   a3852c3c3ebd47d3acac195478ca6f8b  Basement stairs motion         SML001                           Philips             c6c962b892064a218e968fcaee7950c8\n   880a944e74db4bb48ea3db6dd24af357  Basement Light 2               TRADFRI bulb GU10 WS 400lm       IKEA of Sweden      c6c962b892064a218e968fcaee7950c8\n   657c3cc908594479aab819ff80d0c710  Office                         Hue white lamp                   Philips             None\n   [...]\n\n   $ hass-cli area list\n   ID                                NAME\n   295afc88012341ecb897cd12d3fbc6b4  Bathroom\n   9e08d89203804d5db995c3d0d5dbd91b  Winter Garden\n   8816ee92b7b84f54bbb30a68b877e739  Office\n   [...]\n\n\nYou can create and delete areas:\n\n.. code:: bash\n\n   $ hass-cli area delete \"Old Shed\"\n   -  id: 1\n      type: result\n      success: true\n      result: success\n\n   $ hass-cli area create \"New Shed\"\n   -  id: 1\n      type: result\n      success: true\n      result:\n          area_id: cdd09a80f03a4cc59d2943053c0414c0\n          name: New Shed\n\nYou can assign area to a specific device. Here the Kitchen\narea gets assigned to device named \"Cupboard Light\".\n\n.. code:: bash\n\n   $ hass-cli device assign Kitchen \"Cupboard Light\"\n\nBesides assigning individual devices you can assign in bulk:\n\n.. code:: bash\n\n   $ hass-cli device assign Kitchen --match \"Kitchen Light\"\n\nThe above line will assign Kitchen area to all devices with substring \"Kitchen Light\".\n\nYou can also combine individual and matched devices in one line:\n\n.. code:: bash\n\n   $ hass-cli device assign Kitchen --match \"Kitchen Light\" eab9930f8652408882cc8cb604651c60 Cupboard\n\nAbove will assign area named \"Kitchen\" to all devices having substring \"Kitchen Light\" and to\nspecific area with id \"eab9930...\" or named \"Cupboard\".\n\nEvents\n------\n\nYou can subscribe and watch all or a specific event type using ``event watch``.\n\n.. code:: bash\n\n   $ hass-cli event watch\n\nThis will watch for all event types, you can limit to a specific event type\nby specifying it as an argument:\n\n.. code:: bash\n\n   $ hass-cli event watch deconz_event\n\n\nHome Assistant Operating System\n-------------------------------\n\nIf you are using Home Assistant Operating System there are commands available\nfor you to interact with Home Assistant services/systems. This includes the\nunderlying services like the supervisor.\n\nCheck the Supervisor release you are running:\n\n.. code:: bash\n\n   $ hass-cli ha supervisor info\n     result: ok\n     data:\n       version: 2026.03.3\n        version_latest: 2026.03.3\n       update_available: false\n       channel: stable\n    [...]\n\nCheck the Core release you are using at the moment:\n\n.. code:: bash\n\n   $ hass-cli ha core info\n   result: ok\n   data:\n       version: 2026.4.1\n       version_latest: 2026.4.1\n       update_available: false\n       machine: generic-x86-64\n       [...]\n\nUpdate Core to the latest available release:\n\n.. code:: bash\n\n   $ hass-cli ha core update\n\n\nOther\n-----\n\nYou can call services:\n\n.. code:: bash\n\n    $ hass-cli service call deconz.device_refresh\n\nWith arguments:\n\n.. code:: bash\n\n    $ hass-cli service call homeassistant.toggle --arguments entity_id=light.office_light\n\n\nOpen a map for your Home Assistant location:\n\n.. code:: bash\n\n    $ hass-cli map\n\nRender templates server side:\n\n.. code:: bash\n\n    $ hass-cli template motionlight.yaml.j2 motiondata.yaml\n\nRender templates client (local) side:\n\n.. code:: bash\n\n    $ hass-cli template --local lovelace-template.yaml\n\n\nAuto-completion\n###############\n\nAs described above you can use ``source <(hass-cli completion zsh)`` to\nquickly and easy enable auto completion. If you do it from your ``.bashrc``\nor ``.zshrc`` it's recommend to use the form below as that does not trigger\na run of ``hass-cli`` itself.\n\nFor zsh:\n\n.. code:: bash\n\n  eval \"$(_HASS_CLI_COMPLETE=source_zsh hass-cli)\"\n\n\nFor bash:\n\n.. code:: bash\n\n  eval \"$(_HASS_CLI_COMPLETE=source hass-cli)\"\n\n\nOnce enabled there is autocompletion for commands and for certain attributes like entities:\n\n.. code:: bash\n\n  $ hass-cli state get light.<TAB>                                                                                                                                                                    ⏎ ✱ ◼\n  light.kitchen_light_5          light.office_light             light.basement_light_4         light.basement_light_9         light.dinner_table_light_4     light.winter_garden_light_2    light.kitchen_light_2\n  light.kitchen_table_light_1    light.hallroom_light_2         light.basement_light_5         light.basement_light_10        light.dinner_table_wall_light  light.winter_garden_light_4    light.kitchen_table_light_2\n  light.kitchen_light_1          light.hallroom_light_1         light.basement_light_6         light.small_bathroom_light     light.dinner_table_light_5     light.winter_garden_light_3    light.kitchen_light_4\n  [...]\n\n\nNote: For this to work you'll need to have setup the following environment\nvariables if your Home Assistant installation is secured and not running on\nlocalhost:8123:\n\n.. code:: bash\n\n   export HASS_SERVER=http://homeassistant.local:8123\n   export HASS_TOKEN=eyJ0eXAiO-----------------------ed8mj0NP8\n\n\nHelp\n####\n\n.. code:: bash\n\n   $ hass-cli --help\n   Usage: hass-cli [OPTIONS] COMMAND [ARGS]...\n\n   Command line interface for Home Assistant.\n\n   Options:\n   -l, --loglevel LVL              Either CRITICAL, ERROR, WARNING, INFO or\n                                    DEBUG\n   --version                       Show the version and exit.\n   -s, --server TEXT               The server URL or `auto` for automatic\n                                    detection. Can also be set with the\n                                    environment variable HASS_SERVER.  [default:\n                                    auto]\n   --token TEXT                    The Bearer token for Home Assistant\n                                    instance. Can also be set with the\n                                    environment variable HASS_TOKEN.\n   --supervisor-token TEXT         The Bearer token for Home Assistant\n                                    supervisor. Can also be set with the\n                                    environment variable HASS_SUPERVISOR_TOKEN.\n   --password TEXT                 The API password for Home Assistant\n                                    instance. Can also be set with the\n                                    environment variable HASS_PASSWORD.\n   --timeout INTEGER               Timeout for network operations.  [default:\n                                    5]\n   -o, --output [json|yaml|table|auto|ndjson]\n                                    Output format.  [default: auto]\n   -v, --verbose                   Enables verbose mode.\n   -x                              Print backtraces when exception occurs.\n   --cert TEXT                     Path to client certificate file (.pem) to\n                                    use when connecting.\n   --insecure                      Ignore SSL Certificates. Allow to connect to\n                                    servers with self-signed certificates. Be\n                                    careful!\n   --debug                         Enables debug mode.\n   --columns TEXT                  Custom columns key=value list. Example:\n                                    ENTITY=entity_id,\n                                    NAME=attributes.friendly_name\n   --no-headers                    When printing tables don't use headers\n                                    (default: print headers)\n   --table-format TEXT             Which table format to use.\n   --sort-by TEXT                  Sort table by the jsonpath expression.\n                                    Example: last_changed\n   --help                          Show this message and exit.\n\n   Commands:\n   area         Get info and operate on areas from Home Assistant...\n   config       Get configuration from a Home Assistant instance.\n   device       Get info and operate on devices from Home Assistant.\n   discover     Discovery for the local network.\n   entity       Get info on entities from Home Assistant.\n   event        Interact with events.\n   ha           Home Assistant Operating System commands.\n   info         Show information about Home Assistant CLI.\n   integration  Get info and operate on integrations (config entries) from...\n   map          Show the location of the config or an entity on a map.\n   raw          Call the raw API (advanced).\n   service      Call and work with services.\n   state        Get info on entity state from Home Assistant.\n   system       System details and operations for Home Assistant.\n   template     Render templates on server or locally.\n\n\nClone the git repository and\n\n.. code:: bash\n\n    $ pip3 install --editable .\n\n\n\nDevelopment\n###########\n\nDeveloping is (re)using as much as possible from\n`Home Assistant development setup <https://developers.home-assistant.io/docs/en/development_environment.html>`_.\n\nRecommended way to develop is to use virtual environment to ensure isolation\nfrom rest of your system using the following steps:\n\nClone the git repository and do the following:\n\n.. code:: bash\n\n    $ python3 -m venv .\n    $ source bin/activate\n    $ script/setup\n\n\nafter this you should be able to edit the source code and running ``hass-cli``\ndirectly:\n\n.. code:: bash\n\n    $ hass-cli\n\n.. |License| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg\n   :target: https://github.com/home-assistant/home-assistant-cli/blob/master/LICENSE\n   :alt: License\n.. |PyPI| image:: https://img.shields.io/pypi/v/homeassistant_cli.svg\n   :target: https://pypi.org/project/homeassistant_cli/\n   :alt: PyPI release\n.. |Coverage| image:: https://coveralls.io/repos/github/home-assistant/home-assistant-cli/badge.svg?branch=dev\n    :target: https://coveralls.io/github/home-assistant/home-assistant-cli?branch=dev\n    :alt: Coveralls\n.. |Docker| image:: https://img.shields.io/docker/pulls/homeassistant/home-assistant-cli.svg?style=flat\n    :target: https://hub.docker.com/r/homeassistant/home-assistant-cli\n    :alt: Docker\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nIf would find a vulernability in the code base, please contact the author (mail@fabian-affolter.ch, Key: 0xE23CD2DD36A4397F, Fingerprint: 2F6C 930F D3C4 7E38 6AFA 4EB4 E23C D2DD 36A4 397F). \n"
  },
  {
    "path": "docker-hass-cli",
    "content": "\n## Use the tag that best fits you\n## dev contains last build from dev branch.\n#TAG=dev\n## latest is latest released build\nTAG=latest\n## You can also use a specific tag, see https://hub.docker.com/r/homeassistant/home-assistant-cli/tags\n## for available ones.\n##TAG=0.6.0\n\nIMAGE=homeassistant/home-assistant-cli:$TAG\n\n## The -e arguments passes in the environment variables needed for basic hass-cli setup (HASS_SERVER and HASS_TOKEN)\n## and what's needed for auto completion (_HASS_CLI_COMPLETE, COMP_WORDS, COMP_CWORD)\n## Be aware that while hass-cli runs via docker these variables values will be visible using `docker inspect`.\ndocker run -e HASS_TOKEN -e HASS_SERVER -e _HASS_CLI_COMPLETE -e COMP_WORDS -e COMP_CWORD -e _HASS_CLI_COMPLETE $IMAGE $*\n"
  },
  {
    "path": "homeassistant_cli/__init__.py",
    "content": "\"\"\"Init file for Home Assistant CLI (hass-cli).\"\"\"\n"
  },
  {
    "path": "homeassistant_cli/autocompletion.py",
    "content": "\"\"\"Details for the auto-completion.\"\"\"\n\nimport os\n\nfrom requests.exceptions import HTTPError\n\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli import const, hassconst\nfrom homeassistant_cli.config import Configuration, resolve_server\n\n\ndef _init_ctx(ctx: Configuration) -> None:\n    \"\"\"Initialize ctx.\"\"\"\n    # ctx is incomplete thus need to 'hack' around it\n    # see bug https://github.com/pallets/click/issues/942\n    if not hasattr(ctx, \"server\"):\n        ctx.server = os.environ.get(\"HASS_SERVER\", const.AUTO_SERVER)\n\n    if not hasattr(ctx, \"token\"):\n        ctx.token = os.environ.get(\"HASS_TOKEN\", os.environ.get(\"HASSIO_TOKEN\", None))\n\n    if not hasattr(ctx, \"password\"):\n        ctx.password = os.environ.get(\"HASS_PASSWORD\", None)\n\n    if not hasattr(ctx, \"timeout\"):\n        ctx.timeout = int(os.environ.get(\"HASS_TIMEOUT\", str(const.DEFAULT_TIMEOUT)))\n\n    if not hasattr(ctx, \"insecure\"):\n        ctx.insecure = False\n\n    if not hasattr(ctx, \"session\"):\n        ctx.session = None\n\n    if not hasattr(ctx, \"cert\"):\n        ctx.cert = None\n\n    if not hasattr(ctx, \"resolved_server\"):\n        ctx.resolved_server = resolve_server(ctx)\n\n\ndef services(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:\n    \"\"\"Services.\"\"\"\n    _init_ctx(ctx)\n    try:\n        response = api.get_services(ctx)\n    except HTTPError:\n        response = []\n\n    completions = []  # type: List[Tuple[str, str]]\n    if response:\n        for domain in response:\n            domain_name = domain[\"domain\"]\n            servicesdict = domain[\"services\"]\n\n            for service in servicesdict:\n                completions.append(\n                    (\n                        f\"{domain_name}.{service}\",\n                        servicesdict[service][\"description\"],\n                    )\n                )\n\n        completions.sort()\n\n        return [c for c in completions if incomplete in c[0]]\n\n    return completions\n\n\ndef entities(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:\n    \"\"\"Entities.\"\"\"\n    _init_ctx(ctx)\n    try:\n        response = api.get_states(ctx)\n    except HTTPError:\n        response = []\n\n    completions = []  # type List[Tuple[str, str]]\n\n    if response:\n        for entity in response:\n            friendly_name = entity[\"attributes\"].get(\"friendly_name\", \"\")\n            completions.append((entity[\"entity_id\"], friendly_name))\n\n        completions.sort()\n\n        return [c for c in completions if incomplete in c[0]]\n\n    return completions\n\n\ndef events(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:\n    \"\"\"Events.\"\"\"\n    _init_ctx(ctx)\n    try:\n        response = api.get_events(ctx)\n    except HTTPError:\n        response = {}\n\n    completions = []\n\n    if response:\n        for entity in response:\n            completions.append((entity[\"event\"], \"\"))  # type: ignore\n\n        completions.sort()\n\n        return [c for c in completions if incomplete in c[0]]\n\n    return completions\n\n\ndef table_formats(\n    ctx: Configuration, args: list, incomplete: str\n) -> list[tuple[str, str]]:\n    \"\"\"Table Formats.\"\"\"\n    _init_ctx(ctx)\n\n    completions = [\n        (\"plain\", \"Plain tables, no pseudo-graphics to draw lines\"),\n        (\"simple\", \"Simple table with --- as header/footer (default)\"),\n        (\"github\", \"Github flavored Markdown table\"),\n        (\"grid\", \"Formatted as Emacs 'table.el' package\"),\n        (\"fancy_grid\", \"Draws a fancy grid using box-drawing characters\"),\n        (\"pipe\", \"PHP Markdown Extra\"),\n        (\"orgtbl\", \"org-mode table\"),\n        (\"jira\", \"Atlassian Jira Markup\"),\n        (\"presto\", \"Formatted as PrestoDB CLI\"),\n        (\"psql\", \"Formatted as Postgres psql CLI\"),\n        (\"rst\", \"reStructuredText\"),\n        (\"mediawiki\", \"Media Wiki as used in Wikipedia\"),\n        (\"moinmoin\", \"MoinMoin Wiki\"),\n        (\"youtrack\", \"Youtrack format\"),\n        (\"html\", \"HTML Markup\"),\n        (\"latex\", \"LaTeX markup, replacing special characters\"),\n        (\"latex_raw\", \"LaTeX markup, no replacing of special characters\"),\n        (\n            \"latex_booktabs\",\n            \"LaTex markup using spacing and style from `booktabs\",\n        ),\n        (\"textile\", \"Textile\"),\n        (\"tsv\", \"Tab Separated Values\"),\n    ]\n\n    completions.sort()\n\n    return [c for c in completions if incomplete in c[0]]\n\n\ndef api_methods(\n    ctx: Configuration, args: list, incomplete: str\n) -> list[tuple[str, str]]:\n    \"\"\"Auto completion for methods.\"\"\"\n    _init_ctx(ctx)\n\n    from inspect import getmembers\n\n    completions = []\n    for name, value in getmembers(hassconst):\n        if name.startswith(\"URL_API_\"):\n            completions.append((value, name[len(\"URL_API_\") :]))\n\n    completions.sort()\n\n    return [c for c in completions if incomplete in c[0]]\n\n\ndef wsapi_methods(\n    ctx: Configuration, args: list, incomplete: str\n) -> list[tuple[str, str]]:\n    \"\"\"Auto completion for websocket methods.\"\"\"\n    _init_ctx(ctx)\n\n    from inspect import getmembers\n\n    completions = []\n    for name, value in getmembers(hassconst):\n        if name.startswith(\"WS_TYPE_\"):\n            completions.append((value, name[len(\"WS_TYPE_\") :]))\n\n    completions.sort()\n\n    return [c for c in completions if incomplete in c[0]]\n\n\ndef _quote_if_needed(value: str) -> str:\n    \"\"\"Add quotes if needed.\"\"\"\n    if value and \" \" in value:\n        return f'\"{value}\"'\n    return value\n\n\ndef areas(ctx: Configuration, args: list, incomplete: str) -> list[tuple[str, str]]:\n    \"\"\"Areas.\"\"\"\n    _init_ctx(ctx)\n    all_areas = api.get_areas(ctx)\n\n    completions = []  # type: List[Tuple[str, str]]\n\n    if all_areas:\n        for area in all_areas:\n            completions.append((_quote_if_needed(area[\"name\"]), area[\"area_id\"]))\n\n        completions.sort()\n\n        return [c for c in completions if incomplete in c[0]]\n\n    return completions\n"
  },
  {
    "path": "homeassistant_cli/cli.py",
    "content": "\"\"\"Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\nimport sys\nfrom typing import cast\n\nimport click\nimport click_log\nfrom click.core import Command, Context, Group\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.const as const\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import debug_requests_on, to_tuples\n\nclick_log.basic_config()\n\n_LOGGER = logging.getLogger(__name__)\n\nCONTEXT_SETTINGS = dict(auto_envvar_prefix=\"HOMEASSISTANT\")\n\npass_context = click.make_pass_decorator(  # pylint: disable=invalid-name\n    Configuration, ensure=True\n)\n\n\ndef run() -> None:\n    \"\"\"Run entry point.\n\n    Wraps click for full control over exception handling in Click.\n    \"\"\"\n    # A hack to see if exception details should be printed.\n    exceptionflags = [\"-x\"]\n    verbose = [c for c in exceptionflags if c in sys.argv]\n\n    try:\n        # Could use cli.invoke here to use the just created context\n        # but then shell completion will not work. Thus calling\n        # standalone mode to keep that working.\n        result = cli.main(standalone_mode=False)\n        if isinstance(result, int):\n            sys.exit(result)\n\n    # Exception handling below is done to use logger\n    # and mimic as close as possible what click would\n    # do normally in its main()\n    except click.ClickException as ex:\n        ex.show()  # let Click handle its own errors\n        sys.exit(ex.exit_code)\n    except click.Abort:\n        _LOGGER.critical(\"Aborted!\")\n        sys.exit(1)\n    except Exception as ex:  # pylint: disable=broad-except\n        if verbose:\n            _LOGGER.exception(ex)\n        else:\n            _LOGGER.error(\"%s: %s\", type(ex).__name__, ex)\n            _LOGGER.info(\n                \"Run with %s to see full exception information\",\n                \" or \".join(exceptionflags),\n            )\n        sys.exit(1)\n\n\nclass HomeAssistantCli(click.MultiCommand):\n    \"\"\"The Home Assistant Command-line.\"\"\"\n\n    def list_commands(self, ctx: Context) -> list[str]:\n        \"\"\"List all command available as plugin.\"\"\"\n        cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), \"plugins\"))\n\n        commands = []\n        for filename in os.listdir(cmd_folder):\n            if filename.endswith(\".py\") and not filename.startswith(\"__\"):\n                commands.append(filename[:-3])\n        commands.sort()\n\n        return commands\n\n    def get_command(self, ctx: Context, cmd_name: str) -> Group | Command | None:\n        \"\"\"Import the commands of the plugins.\"\"\"\n        try:\n            mod = __import__(\n                f\"{const.PACKAGE_NAME}.plugins.{cmd_name}\",\n                {},\n                {},\n                [\"cli\"],\n            )\n        except ImportError:\n            # todo: print out issue of loading plugins?\n            return None\n        return cast(Group | Command, mod.cli)  # type: ignore\n\n\ndef _default_token() -> str | None:\n    \"\"\"Handle the token provided as env variable.\"\"\"\n    return os.environ.get(\"HASS_TOKEN\", os.environ.get(\"HASSIO_TOKEN\", None))\n\n\n@click.command(cls=HomeAssistantCli, context_settings=CONTEXT_SETTINGS)\n@click_log.simple_verbosity_option(logging.getLogger(), \"--loglevel\", \"-l\")\n@click.version_option(const.__version__)\n@click.option(\n    \"--server\",\n    \"-s\",\n    help=(\n        \"The server URL or `auto` for automatic detection. Can also be set \"\n        \"with the environment variable HASS_SERVER.\"\n    ),\n    default=\"auto\",\n    show_default=True,\n    envvar=\"HASS_SERVER\",\n)\n@click.option(\n    \"--token\",\n    default=_default_token,\n    help=(\n        \"The Bearer token for Home Assistant instance. Can also be set with \"\n        \"the environment variable HASS_TOKEN.\"\n    ),\n    envvar=\"HASS_TOKEN\",\n)\n@click.option(\n    \"--supervisor-token\",\n    default=_default_token,\n    help=(\n        \"The Bearer token for Home Assistant supervisor. Can also be set with \"\n        \"the environment variable HASS_SUPERVISOR_TOKEN.\"\n    ),\n    envvar=\"HASS_SUPERVISOR_TOKEN\",\n)\n@click.option(\n    \"--password\",\n    default=None,\n    help=(\n        \"The API password for Home Assistant instance. Can also be set with \"\n        \"the environment variable HASS_PASSWORD.\"\n    ),\n    envvar=\"HASS_PASSWORD\",\n)\n@click.option(\n    \"--timeout\",\n    help=\"Timeout for network operations.\",\n    default=const.DEFAULT_TIMEOUT,\n    show_default=True,\n)\n@click.option(\n    \"--output\",\n    \"-o\",\n    help=\"Output format.\",\n    type=click.Choice([\"json\", \"yaml\", \"table\", \"auto\", \"ndjson\"]),\n    default=\"auto\",\n    show_default=True,\n)\n@click.option(\n    \"-v\",\n    \"--verbose\",\n    is_flag=True,\n    default=False,\n    help=\"Enables verbose mode.\",\n)\n@click.option(\n    \"-x\",\n    \"showexceptions\",\n    default=False,\n    is_flag=True,\n    help=\"Print backtraces when exception occurs.\",\n)\n@click.option(\n    \"--cert\",\n    default=None,\n    envvar=\"HASS_CERT\",\n    help=\"Path to client certificate file (.pem) to use when connecting.\",\n)\n@click.option(\n    \"--insecure\",\n    is_flag=True,\n    default=False,\n    help=(\n        \"Ignore SSL Certificates.\"\n        \" Allow to connect to servers with self-signed certificates.\"\n        \" Be careful!\"\n    ),\n)\n@click.option(\"--debug\", is_flag=True, default=False, help=\"Enables debug mode.\")\n@click.option(\n    \"--columns\",\n    default=None,\n    help=(\n        \"Custom columns key=value list.\"\n        \" Example: ENTITY=entity_id, NAME=attributes.friendly_name\"\n    ),\n)\n@click.option(\n    \"--no-headers\",\n    default=False,\n    is_flag=True,\n    help=\"When printing tables don't use headers (default: print headers)\",\n)\n@click.option(\n    \"--table-format\",\n    default=\"plain\",\n    help=\"Which table format to use.\",\n    shell_complete=autocompletion.table_formats,\n)\n@click.option(\n    \"--sort-by\",\n    default=None,\n    help=\"Sort table by the jsonpath expression. Example: last_changed\",\n)\n@pass_context\ndef cli(\n    ctx: Configuration,\n    verbose: bool,\n    server: str,\n    token: str | None,\n    supervisor_token: str | None,\n    password: str | None,\n    output: str,\n    timeout: int,\n    debug: bool,\n    insecure: bool,\n    showexceptions: bool,\n    cert: str,\n    columns: str,\n    no_headers: bool,\n    table_format: str,\n    sort_by: str | None,\n) -> None:\n    \"\"\"Command line interface for Home Assistant.\"\"\"\n    ctx.verbose = verbose\n    ctx.server = server\n    ctx.token = token\n    ctx.supervisor_token = supervisor_token\n    ctx.password = password\n    ctx.timeout = timeout\n    ctx.output = output\n    ctx.debug = debug\n    ctx.insecure = insecure\n    ctx.showexceptions = showexceptions\n    ctx.cert = cert\n    ctx.columns = to_tuples(columns)\n    ctx.no_headers = no_headers\n    ctx.table_format = table_format\n    ctx.sort_by = sort_by  # type: ignore\n\n    _LOGGER.debug(\"Using settings: %s\", ctx)\n\n    if debug:\n        debug_requests_on()\n"
  },
  {
    "path": "homeassistant_cli/config.py",
    "content": "\"\"\"Configuration for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\nimport sys\nfrom typing import Any, Dict, List, Optional, Tuple, cast\n\nimport click\nimport zeroconf\nfrom ruamel.yaml import YAML\n\nimport homeassistant_cli.const as const\nimport homeassistant_cli.yaml as yaml\n\n_LOGGING = logging.getLogger(__name__)\n\n\nclass _ZeroconfListener:\n    \"\"\"Representation of the Zeroconf listener.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the listener.\"\"\"\n        self.services = {}  # type: Dict[str, zeroconf.ServiceInfo]\n\n    def remove_service(\n        self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str\n    ) -> None:\n        \"\"\"Remove service.\"\"\"\n        self.services[name] = None\n\n    def add_service(self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str) -> None:\n        \"\"\"Add service.\"\"\"\n        self.services[name] = _zeroconf.get_service_info(_type, name)\n\n    def update_service(\n        self, _zeroconf: zeroconf.Zeroconf, _type: str, name: str\n    ) -> None:\n        \"\"\"Update service.\"\"\"\n        self.services[name] = _zeroconf.get_service_info(_type, name)\n\n\ndef _locate_ha() -> str | None:\n    \"\"\"Locate the Home Assistant instance.\"\"\"\n    _zeroconf = zeroconf.Zeroconf(interfaces=zeroconf.InterfaceChoice.Default)\n    listener = _ZeroconfListener()\n    zeroconf.ServiceBrowser(_zeroconf, \"_home-assistant._tcp.local.\", listener)\n    try:\n        import time\n\n        retries = 0\n        while not listener.services and retries < 5:\n            _LOGGING.info(\"Trying to locate Home Assistant on local network...\")\n            time.sleep(0.5)\n            retries = retries + 1\n    finally:\n        _zeroconf.close()\n\n    if listener.services:\n        if len(listener.services) > 1:\n            _LOGGING.warning(\n                f\"Found multiple Home Assistant instances at \"\n                f\"{', '.join(listener.services)}\"\n            )\n            _LOGGING.warning(\"Use --server to explicitly specify one.\")\n            return None\n\n        _, service = listener.services.popitem()\n        base_url = service.properties[b\"base_url\"].decode(\"utf-8\")\n        _LOGGING.info(f\"Found and using {base_url} as server\")\n        return cast(str, base_url)\n\n    _LOGGING.warning(\"Found no Home Assistant on local network. Using defaults\")\n    return None\n\n\ndef resolve_server(ctx: Any) -> str:\n    \"\"\"Resolve server if not already done.\n\n    if server is `auto` try and resolve it\n    \"\"\"\n    # Work-around for bug in click that hands out non-Configuration context objects\n    if not hasattr(ctx, \"resolved_server\"):\n        ctx.resolved_server = None\n\n    if not ctx.resolved_server:\n        if ctx.server == \"auto\":\n            if \"HASSIO_TOKEN\" in os.environ and \"HASS_TOKEN\" not in os.environ:\n                ctx.resolved_server = const.DEFAULT_SERVER_MDNS\n            else:\n                if not ctx.resolved_server and \"pytest\" in sys.modules:\n                    ctx.resolved_server = const.DEFAULT_SERVER\n                else:\n                    ctx.resolved_server = _locate_ha()\n                    if not ctx.resolved_server:\n                        sys.exit(3)\n        else:\n            ctx.resolved_server = ctx.server\n\n        if not ctx.resolved_server:\n            ctx.resolved_server = const.DEFAULT_SERVER\n\n    return cast(str, ctx.resolved_server)\n\n\ndef set_supervisor_server(ctx: Any) -> str:\n    \"\"\"Derive the supervisor server URL from the main server URL.\"\"\"\n    if not hasattr(ctx, \"supervisor_server\"):\n        ctx.supervisor_server = None\n\n    if not ctx.supervisor_server:\n        if ctx.server and ctx.server != \"auto\":\n            ctx.supervisor_server = ctx.server.rsplit(\":\", 1)[0]\n        else:\n            # Ensure resolved_server is set first\n            resolved = resolve_server(ctx)\n            ctx.supervisor_server = resolved.rsplit(\":\", 1)[0]\n\n    return cast(str, ctx.supervisor_server)\n\n\nclass Configuration:\n    \"\"\"The configuration context for the Home Assistant CLI.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize the configuration.\"\"\"\n        self.verbose = False  # type: bool\n        self.server = const.AUTO_SERVER  # type: str\n        self.resolved_server = None  # type: Optional[str]\n        self.supervisor_server = None  # type: Optional[str]\n        self.output = const.DEFAULT_OUTPUT  # type: str\n        self.token = None  # type: Optional[str]\n        self.supervisor_token = None  # type: Optional[str]\n        self.password = None  # type: Optional[str]\n        self.insecure = False  # type: bool\n        self.timeout = const.DEFAULT_TIMEOUT  # type: int\n        self.debug = False  # type: bool\n        self.showexceptions = False  # type: bool\n        self.session = None  # type: Optional[Session]\n        self.cert = None  # type: Optional[str]\n        self.columns = None  # type: Optional[List[Tuple[str, str]]]\n        self.no_headers = False\n        self.table_format = \"plain\"\n        self.sort_by = None\n\n    def echo(self, msg: str, *args: Any | None) -> None:\n        \"\"\"Put content message to stdout.\"\"\"\n        self.log(msg, *args)\n\n    def log(  # pylint: disable=no-self-use\n        self, msg: str, *args: str | None\n    ) -> None:  # pylint: disable=no-self-use\n        \"\"\"Log a message to stdout.\"\"\"\n        if args:\n            msg %= args\n        click.echo(msg, file=sys.stdout)\n\n    def vlog(self, msg: str, *args: str | None) -> None:\n        \"\"\"Log a message only if verbose is enabled.\"\"\"\n        if self.verbose:\n            self.log(msg, *args)\n\n    def __repr__(self) -> str:\n        \"\"\"Return the representation of the Configuration.\"\"\"\n        view = {\n            \"server\": self.server,\n            \"access-token\": \"yes\" if self.token is not None else \"no\",\n            \"api-password\": \"yes\" if self.password is not None else \"no\",\n            \"insecure\": self.insecure,\n            \"output\": self.output,\n            \"verbose\": self.verbose,\n        }\n\n        print(\"-------------------------------\")\n        print(view)\n\n        return f\"<Configuration({view})\"\n\n    def resolve_server(self) -> str:\n        \"\"\"Return resolved server (after resolving if needed).\"\"\"\n        return resolve_server(self)\n\n    def set_supervisor_server(self) -> str:\n        \"\"\"Return supervisor server.\"\"\"\n        return set_supervisor_server(self)\n\n    def auto_output(self, auto_output: str) -> str:\n        \"\"\"Configure output format.\"\"\"\n        if self.output == \"auto\":\n            if auto_output == \"data\":\n                auto_output = const.DEFAULT_DATAOUTPUT\n            _LOGGING.debug(\"Setting auto-output to: %s\", auto_output)\n            self.output = auto_output\n        return self.output\n\n    def yaml(self) -> YAML:\n        \"\"\"Create default yaml parser.\"\"\"\n        if self:\n            yaml.yaml()\n        return yaml.yaml()\n\n    def yamlload(self, source: str) -> Any:\n        \"\"\"Load YAML from source.\"\"\"\n        return self.yaml().load(source)\n\n    def yamldump(self, source: Any) -> str:\n        \"\"\"Dump dictionary to YAML string.\"\"\"\n        return cast(str, yaml.dumpyaml(self.yaml(), source))\n"
  },
  {
    "path": "homeassistant_cli/const.py",
    "content": "\"\"\"Constants used by Home Assistant CLI (hass-cli).\"\"\"\n\nPACKAGE_NAME = \"homeassistant_cli\"\n\n__version__ = \"1.0.1\"\n\nAUTO_SERVER = \"auto\"\nDEFAULT_SERVER = \"http://localhost:8123\"\nDEFAULT_SERVER_MDNS = \"http://homeassistant.local:8123\"\nDEFAULT_TIMEOUT = 5\nDEFAULT_OUTPUT = \"json\"  # TODO: Have default be human table relevant output\n\nDEFAULT_DATAOUTPUT = \"yaml\"\n\nCOLUMNS_DEFAULT = [(\"ALL\", \"$\")]\nCOLUMNS_ENTITIES = [\n    (\"ENTITY\", \"entity_id\"),\n    (\"DESCRIPTION\", \"attributes.friendly_name\"),\n    (\"STATE\", \"state\"),\n    (\"CHANGED\", \"last_changed\"),\n]\nCOLUMNS_SERVICES = [(\"DOMAIN\", \"domain\"), (\"SERVICE\", \"domain.services[*]\")]\n"
  },
  {
    "path": "homeassistant_cli/exceptions.py",
    "content": "\"\"\"The exceptions used by Home Assistant CLI.\"\"\"\n\n\nclass HomeAssistantCliError(Exception):\n    \"\"\"General Home Assistant CLI exception occurred.\"\"\"\n\n\nclass UnsafeTemplateError(HomeAssistantCliError):\n    \"\"\"Template contains unsafe operations.\"\"\"\n"
  },
  {
    "path": "homeassistant_cli/hassconst.py",
    "content": "\"\"\"Constants used by Home Assistant components.\n\nCopy of recent homeassistant.const to make hass-cli run\nwithout installing Home Assistant itself.\n\"\"\"\n# Home Assistant WS constants\n\n# Websocket API\nWS_TYPE_DEVICE_REGISTRY_LIST = \"config/device_registry/list\"\nWS_TYPE_AREA_REGISTRY_LIST = \"config/area_registry/list\"\nWS_TYPE_AREA_REGISTRY_CREATE = \"config/area_registry/create\"\nWS_TYPE_AREA_REGISTRY_DELETE = \"config/area_registry/delete\"\nWS_TYPE_AREA_REGISTRY_UPDATE = \"config/area_registry/update\"\nWS_TYPE_DEVICE_REGISTRY_UPDATE = \"config/device_registry/update\"\nWS_TYPE_ENTITY_REGISTRY_LIST = \"config/entity_registry/list\"\nWS_TYPE_ENTITY_REGISTRY_GET = \"config/entity_registry/get\"\nWS_TYPE_ENTITY_REGISTRY_UPDATE = \"config/entity_registry/update\"\nWS_TYPE_ENTITY_REGISTRY_REMOVE = \"config/entity_registry/remove\"\n\n# Config entries (integrations)\nWS_TYPE_CONFIG_ENTRIES_GET = \"config_entries/get\"\nWS_TYPE_CONFIG_ENTRIES_GET_SINGLE = \"config_entries/get_single\"\nWS_TYPE_CONFIG_ENTRIES_UPDATE = \"config_entries/update\"\nWS_TYPE_CONFIG_ENTRIES_DISABLE = \"config_entries/disable\"\n\n###############################################################################\n# Home Assistant constants\n\n# Format for platform files\nPLATFORM_FORMAT = \"{platform}.{domain}\"\n\n# Can be used to specify a catch all when registering state or event listeners.\nMATCH_ALL = \"*\"\n\n# Entity target all constant\nENTITY_MATCH_NONE = \"none\"\nENTITY_MATCH_ALL = \"all\"\n\n# If no name is specified\nDEVICE_DEFAULT_NAME = \"Unnamed Device\"\n\n# Sun events\nSUN_EVENT_SUNSET = \"sunset\"\nSUN_EVENT_SUNRISE = \"sunrise\"\n\n# #### CONFIG ####\nCONF_ABOVE = \"above\"\nCONF_ACCESS_TOKEN = \"access_token\"\nCONF_ADDRESS = \"address\"\nCONF_AFTER = \"after\"\nCONF_ALIAS = \"alias\"\nCONF_ALLOWLIST_EXTERNAL_URLS = \"allowlist_external_urls\"\nCONF_API_KEY = \"api_key\"\nCONF_API_TOKEN = \"api_token\"\nCONF_API_VERSION = \"api_version\"\nCONF_ARMING_TIME = \"arming_time\"\nCONF_AT = \"at\"\nCONF_ATTRIBUTE = \"attribute\"\nCONF_AUTH_MFA_MODULES = \"auth_mfa_modules\"\nCONF_AUTH_PROVIDERS = \"auth_providers\"\nCONF_AUTHENTICATION = \"authentication\"\nCONF_BASE = \"base\"\nCONF_BEFORE = \"before\"\nCONF_BELOW = \"below\"\nCONF_BINARY_SENSORS = \"binary_sensors\"\nCONF_BRIGHTNESS = \"brightness\"\nCONF_BROADCAST_ADDRESS = \"broadcast_address\"\nCONF_BROADCAST_PORT = \"broadcast_port\"\nCONF_CHOOSE = \"choose\"\nCONF_CLIENT_ID = \"client_id\"\nCONF_CLIENT_SECRET = \"client_secret\"\nCONF_CODE = \"code\"\nCONF_COLOR_TEMP = \"color_temp\"\nCONF_COMMAND = \"command\"\nCONF_COMMAND_CLOSE = \"command_close\"\nCONF_COMMAND_OFF = \"command_off\"\nCONF_COMMAND_ON = \"command_on\"\nCONF_COMMAND_OPEN = \"command_open\"\nCONF_COMMAND_STATE = \"command_state\"\nCONF_COMMAND_STOP = \"command_stop\"\nCONF_CONDITION = \"condition\"\nCONF_CONDITIONS = \"conditions\"\nCONF_CONTINUE_ON_TIMEOUT = \"continue_on_timeout\"\nCONF_COUNT = \"count\"\nCONF_COVERS = \"covers\"\nCONF_CURRENCY = \"currency\"\nCONF_CUSTOMIZE = \"customize\"\nCONF_CUSTOMIZE_DOMAIN = \"customize_domain\"\nCONF_CUSTOMIZE_GLOB = \"customize_glob\"\nCONF_DEFAULT = \"default\"\nCONF_DELAY = \"delay\"\nCONF_DELAY_TIME = \"delay_time\"\nCONF_DESCRIPTION = \"description\"\nCONF_DEVICE = \"device\"\nCONF_DEVICES = \"devices\"\nCONF_DEVICE_CLASS = \"device_class\"\nCONF_DEVICE_ID = \"device_id\"\nCONF_DISARM_AFTER_TRIGGER = \"disarm_after_trigger\"\nCONF_DISCOVERY = \"discovery\"\nCONF_DISKS = \"disks\"\nCONF_DISPLAY_CURRENCY = \"display_currency\"\nCONF_DISPLAY_OPTIONS = \"display_options\"\nCONF_DOMAIN = \"domain\"\nCONF_DOMAINS = \"domains\"\nCONF_EFFECT = \"effect\"\nCONF_ELEVATION = \"elevation\"\nCONF_EMAIL = \"email\"\nCONF_ENTITIES = \"entities\"\nCONF_ENTITY_ID = \"entity_id\"\nCONF_ENTITY_NAMESPACE = \"entity_namespace\"\nCONF_ENTITY_PICTURE_TEMPLATE = \"entity_picture_template\"\nCONF_EVENT = \"event\"\nCONF_EVENT_DATA = \"event_data\"\nCONF_EVENT_DATA_TEMPLATE = \"event_data_template\"\nCONF_EXCLUDE = \"exclude\"\nCONF_EXTERNAL_URL = \"external_url\"\nCONF_FILENAME = \"filename\"\nCONF_FILE_PATH = \"file_path\"\nCONF_FOR = \"for\"\nCONF_FORCE_UPDATE = \"force_update\"\nCONF_FRIENDLY_NAME = \"friendly_name\"\nCONF_FRIENDLY_NAME_TEMPLATE = \"friendly_name_template\"\nCONF_HEADERS = \"headers\"\nCONF_HOST = \"host\"\nCONF_HOSTS = \"hosts\"\nCONF_HS = \"hs\"\nCONF_ICON = \"icon\"\nCONF_ICON_TEMPLATE = \"icon_template\"\nCONF_ID = \"id\"\nCONF_INCLUDE = \"include\"\nCONF_INTERNAL_URL = \"internal_url\"\nCONF_IP_ADDRESS = \"ip_address\"\nCONF_LATITUDE = \"latitude\"\nCONF_LEGACY_TEMPLATES = \"legacy_templates\"\nCONF_LIGHTS = \"lights\"\nCONF_LONGITUDE = \"longitude\"\nCONF_MAC = \"mac\"\nCONF_MAXIMUM = \"maximum\"\nCONF_MEDIA_DIRS = \"media_dirs\"\nCONF_METHOD = \"method\"\nCONF_MINIMUM = \"minimum\"\nCONF_MODE = \"mode\"\nCONF_MONITORED_CONDITIONS = \"monitored_conditions\"\nCONF_MONITORED_VARIABLES = \"monitored_variables\"\nCONF_NAME = \"name\"\nCONF_OFFSET = \"offset\"\nCONF_OPTIMISTIC = \"optimistic\"\nCONF_PACKAGES = \"packages\"\nCONF_PARAMS = \"params\"\nCONF_PASSWORD = \"password\"\nCONF_PATH = \"path\"\nCONF_PAYLOAD = \"payload\"\nCONF_PAYLOAD_OFF = \"payload_off\"\nCONF_PAYLOAD_ON = \"payload_on\"\nCONF_PENDING_TIME = \"pending_time\"\nCONF_PIN = \"pin\"\nCONF_PLATFORM = \"platform\"\nCONF_PORT = \"port\"\nCONF_PREFIX = \"prefix\"\nCONF_PROFILE_NAME = \"profile_name\"\nCONF_PROTOCOL = \"protocol\"\nCONF_PROXY_SSL = \"proxy_ssl\"\nCONF_QUOTE = \"quote\"\nCONF_RADIUS = \"radius\"\nCONF_RECIPIENT = \"recipient\"\nCONF_REGION = \"region\"\nCONF_REPEAT = \"repeat\"\nCONF_RESOURCE = \"resource\"\nCONF_RESOURCES = \"resources\"\nCONF_RESOURCE_TEMPLATE = \"resource_template\"\nCONF_RGB = \"rgb\"\nCONF_ROOM = \"room\"\nCONF_SCAN_INTERVAL = \"scan_interval\"\nCONF_SCENE = \"scene\"\nCONF_SELECTOR = \"selector\"\nCONF_SENDER = \"sender\"\nCONF_SENSORS = \"sensors\"\nCONF_SENSOR_TYPE = \"sensor_type\"\nCONF_SEQUENCE = \"sequence\"\nCONF_SERVICE = \"service\"\nCONF_SERVICE_DATA = \"data\"\nCONF_SERVICE_TEMPLATE = \"service_template\"\nCONF_SHOW_ON_MAP = \"show_on_map\"\nCONF_SLAVE = \"slave\"\nCONF_SOURCE = \"source\"\nCONF_SSL = \"ssl\"\nCONF_STATE = \"state\"\nCONF_STATE_TEMPLATE = \"state_template\"\nCONF_STRUCTURE = \"structure\"\nCONF_SWITCHES = \"switches\"\nCONF_TARGET = \"target\"\nCONF_TEMPERATURE_UNIT = \"temperature_unit\"\nCONF_TIMEOUT = \"timeout\"\nCONF_TIME_ZONE = \"time_zone\"\nCONF_TOKEN = \"token\"\nCONF_TRIGGER_TIME = \"trigger_time\"\nCONF_TTL = \"ttl\"\nCONF_TYPE = \"type\"\nCONF_UNIQUE_ID = \"unique_id\"\nCONF_UNIT_OF_MEASUREMENT = \"unit_of_measurement\"\nCONF_UNIT_SYSTEM = \"unit_system\"\nCONF_UNTIL = \"until\"\nCONF_URL = \"url\"\nCONF_USERNAME = \"username\"\nCONF_VALUE_TEMPLATE = \"value_template\"\nCONF_VARIABLES = \"variables\"\nCONF_VERIFY_SSL = \"verify_ssl\"\nCONF_WAIT_FOR_TRIGGER = \"wait_for_trigger\"\nCONF_WAIT_TEMPLATE = \"wait_template\"\nCONF_WEBHOOK_ID = \"webhook_id\"\nCONF_WEEKDAY = \"weekday\"\nCONF_WHILE = \"while\"\nCONF_WHITELIST = \"whitelist\"\nCONF_ALLOWLIST_EXTERNAL_DIRS = \"allowlist_external_dirs\"\nLEGACY_CONF_WHITELIST_EXTERNAL_DIRS = \"whitelist_external_dirs\"\nCONF_WHITE_VALUE = \"white_value\"\nCONF_XY = \"xy\"\nCONF_ZONE = \"zone\"\n\n# #### EVENTS ####\nEVENT_CALL_SERVICE = \"call_service\"\nEVENT_COMPONENT_LOADED = \"component_loaded\"\nEVENT_CORE_CONFIG_UPDATE = \"core_config_updated\"\nEVENT_HOMEASSISTANT_CLOSE = \"homeassistant_close\"\nEVENT_HOMEASSISTANT_START = \"homeassistant_start\"\nEVENT_HOMEASSISTANT_STARTED = \"homeassistant_started\"\nEVENT_HOMEASSISTANT_STOP = \"homeassistant_stop\"\nEVENT_HOMEASSISTANT_FINAL_WRITE = \"homeassistant_final_write\"\nEVENT_LOGBOOK_ENTRY = \"logbook_entry\"\nEVENT_SERVICE_REGISTERED = \"service_registered\"\nEVENT_SERVICE_REMOVED = \"service_removed\"\nEVENT_STATE_CHANGED = \"state_changed\"\nEVENT_THEMES_UPDATED = \"themes_updated\"\nEVENT_TIMER_OUT_OF_SYNC = \"timer_out_of_sync\"\nEVENT_TIME_CHANGED = \"time_changed\"\n\n\n# #### DEVICE CLASSES ####\nDEVICE_CLASS_BATTERY = \"battery\"\nDEVICE_CLASS_CO = \"carbon_monoxide\"\nDEVICE_CLASS_CO2 = \"carbon_dioxide\"\nDEVICE_CLASS_HUMIDITY = \"humidity\"\nDEVICE_CLASS_ILLUMINANCE = \"illuminance\"\nDEVICE_CLASS_SIGNAL_STRENGTH = \"signal_strength\"\nDEVICE_CLASS_TEMPERATURE = \"temperature\"\nDEVICE_CLASS_TIMESTAMP = \"timestamp\"\nDEVICE_CLASS_PRESSURE = \"pressure\"\nDEVICE_CLASS_POWER = \"power\"\nDEVICE_CLASS_CURRENT = \"current\"\nDEVICE_CLASS_ENERGY = \"energy\"\nDEVICE_CLASS_POWER_FACTOR = \"power_factor\"\nDEVICE_CLASS_VOLTAGE = \"voltage\"\n\n# #### STATES ####\nSTATE_ON = \"on\"\nSTATE_OFF = \"off\"\nSTATE_HOME = \"home\"\nSTATE_NOT_HOME = \"not_home\"\nSTATE_UNKNOWN = \"unknown\"\nSTATE_OPEN = \"open\"\nSTATE_OPENING = \"opening\"\nSTATE_CLOSED = \"closed\"\nSTATE_CLOSING = \"closing\"\nSTATE_PLAYING = \"playing\"\nSTATE_PAUSED = \"paused\"\nSTATE_IDLE = \"idle\"\nSTATE_STANDBY = \"standby\"\nSTATE_ALARM_DISARMED = \"disarmed\"\nSTATE_ALARM_ARMED_HOME = \"armed_home\"\nSTATE_ALARM_ARMED_AWAY = \"armed_away\"\nSTATE_ALARM_ARMED_NIGHT = \"armed_night\"\nSTATE_ALARM_ARMED_CUSTOM_BYPASS = \"armed_custom_bypass\"\nSTATE_ALARM_PENDING = \"pending\"\nSTATE_ALARM_ARMING = \"arming\"\nSTATE_ALARM_DISARMING = \"disarming\"\nSTATE_ALARM_TRIGGERED = \"triggered\"\nSTATE_LOCKED = \"locked\"\nSTATE_UNLOCKED = \"unlocked\"\nSTATE_UNAVAILABLE = \"unavailable\"\nSTATE_OK = \"ok\"\nSTATE_PROBLEM = \"problem\"\n\n# #### STATE AND EVENT ATTRIBUTES ####\n# Attribution\nATTR_ATTRIBUTION = \"attribution\"\n\n# Credentials\nATTR_CREDENTIALS = \"credentials\"\n\n# Contains time-related attributes\nATTR_NOW = \"now\"\nATTR_DATE = \"date\"\nATTR_TIME = \"time\"\nATTR_SECONDS = \"seconds\"\n\n# Contains domain, service for a SERVICE_CALL event\nATTR_DOMAIN = \"domain\"\nATTR_SERVICE = \"service\"\nATTR_SERVICE_DATA = \"service_data\"\n\n# IDs\nATTR_ID = \"id\"\n\n# Name\nATTR_NAME = \"name\"\n\n# Contains one string or a list of strings, each being an entity id\nATTR_ENTITY_ID = \"entity_id\"\n\n# Contains one string or a list of strings, each being an area id\nATTR_AREA_ID = \"area_id\"\n\n# Contains one string, the device ID\nATTR_DEVICE_ID = \"device_id\"\n\n# String with a friendly name for the entity\nATTR_FRIENDLY_NAME = \"friendly_name\"\n\n# A picture to represent entity\nATTR_ENTITY_PICTURE = \"entity_picture\"\n\n# Icon to use in the frontend\nATTR_ICON = \"icon\"\n\n# The unit of measurement if applicable\nATTR_UNIT_OF_MEASUREMENT = \"unit_of_measurement\"\n\nCONF_UNIT_SYSTEM_METRIC: str = \"metric\"\nCONF_UNIT_SYSTEM_IMPERIAL: str = \"imperial\"\n\n# Electrical attributes\nATTR_VOLTAGE = \"voltage\"\n\n# Location of the device/sensor\nATTR_LOCATION = \"location\"\n\nATTR_MODE = \"mode\"\n\nATTR_BATTERY_CHARGING = \"battery_charging\"\nATTR_BATTERY_LEVEL = \"battery_level\"\nATTR_WAKEUP = \"wake_up_interval\"\n\n# For devices which support a code attribute\nATTR_CODE = \"code\"\nATTR_CODE_FORMAT = \"code_format\"\n\n# For calling a device specific command\nATTR_COMMAND = \"command\"\n\n# For devices which support an armed state\nATTR_ARMED = \"device_armed\"\n\n# For devices which support a locked state\nATTR_LOCKED = \"locked\"\n\n# For sensors that support 'tripping', eg. motion and door sensors\nATTR_TRIPPED = \"device_tripped\"\n\n# For sensors that support 'tripping' this holds the most recent\n# time the device was tripped\nATTR_LAST_TRIP_TIME = \"last_tripped_time\"\n\n# For all entity's, this hold whether or not it should be hidden\nATTR_HIDDEN = \"hidden\"\n\n# Location of the entity\nATTR_LATITUDE = \"latitude\"\nATTR_LONGITUDE = \"longitude\"\n\n# Accuracy of location in meters\nATTR_GPS_ACCURACY = \"gps_accuracy\"\n\n# If state is assumed\nATTR_ASSUMED_STATE = \"assumed_state\"\nATTR_STATE = \"state\"\n\nATTR_EDITABLE = \"editable\"\nATTR_OPTION = \"option\"\n\n# The entity has been restored with restore state\nATTR_RESTORED = \"restored\"\n\n# Bitfield of supported component features for the entity\nATTR_SUPPORTED_FEATURES = \"supported_features\"\n\n# Class of device within its domain\nATTR_DEVICE_CLASS = \"device_class\"\n\n# Temperature attribute\nATTR_TEMPERATURE = \"temperature\"\n\n# #### UNITS OF MEASUREMENT ####\n# Power units\nPOWER_WATT = \"W\"\nPOWER_KILO_WATT = \"kW\"\n\n# Voltage units\nVOLT = \"V\"\n\n# Energy units\nENERGY_WATT_HOUR = \"Wh\"\nENERGY_KILO_WATT_HOUR = \"kWh\"\n\n# Electrical units\nELECTRICAL_CURRENT_AMPERE = \"A\"\nELECTRICAL_VOLT_AMPERE = \"VA\"\n\n# Degree units\nDEGREE = \"°\"\n\n# Currency units\nCURRENCY_EURO = \"€\"\nCURRENCY_DOLLAR = \"$\"\nCURRENCY_CENT = \"¢\"\n\n# Temperature units\nTEMP_CELSIUS = \"°C\"\nTEMP_FAHRENHEIT = \"°F\"\nTEMP_KELVIN = \"K\"\n\n# Time units\nTIME_MICROSECONDS = \"μs\"\nTIME_MILLISECONDS = \"ms\"\nTIME_SECONDS = \"s\"\nTIME_MINUTES = \"min\"\nTIME_HOURS = \"h\"\nTIME_DAYS = \"d\"\nTIME_WEEKS = \"w\"\nTIME_MONTHS = \"m\"\nTIME_YEARS = \"y\"\n\n# Length units\nLENGTH_MILLIMETERS: str = \"mm\"\nLENGTH_CENTIMETERS: str = \"cm\"\nLENGTH_METERS: str = \"m\"\nLENGTH_KILOMETERS: str = \"km\"\n\nLENGTH_INCHES: str = \"in\"\nLENGTH_FEET: str = \"ft\"\nLENGTH_YARD: str = \"yd\"\nLENGTH_MILES: str = \"mi\"\n\n# Frequency units\nFREQUENCY_HERTZ = \"Hz\"\nFREQUENCY_GIGAHERTZ = \"GHz\"\n\n# Pressure units\nPRESSURE_PA: str = \"Pa\"\nPRESSURE_HPA: str = \"hPa\"\nPRESSURE_BAR: str = \"bar\"\nPRESSURE_MBAR: str = \"mbar\"\nPRESSURE_INHG: str = \"inHg\"\nPRESSURE_PSI: str = \"psi\"\n\n# Volume units\nVOLUME_LITERS: str = \"L\"\nVOLUME_MILLILITERS: str = \"mL\"\nVOLUME_CUBIC_METERS = \"m³\"\nVOLUME_CUBIC_FEET = \"ft³\"\n\nVOLUME_GALLONS: str = \"gal\"\nVOLUME_FLUID_OUNCE: str = \"fl. oz.\"\n\n# Volume Flow Rate units\nVOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR = \"m³/h\"\nVOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE = \"ft³/m\"\n\n# Area units\nAREA_SQUARE_METERS = \"m²\"\n\n# Mass units\nMASS_GRAMS: str = \"g\"\nMASS_KILOGRAMS: str = \"kg\"\nMASS_MILLIGRAMS = \"mg\"\nMASS_MICROGRAMS = \"µg\"\n\nMASS_OUNCES: str = \"oz\"\nMASS_POUNDS: str = \"lb\"\n\n# Conductivity units\nCONDUCTIVITY: str = \"µS/cm\"\n\n# Light units\nLIGHT_LUX: str = \"lx\"\n\n# UV Index units\nUV_INDEX: str = \"UV index\"\n\n# Percentage units\nPERCENTAGE = \"%\"\n\n# Irradiation units\nIRRADIATION_WATTS_PER_SQUARE_METER = \"W/m²\"\n\n# Precipitation units\nPRECIPITATION_MILLIMETERS_PER_HOUR = \"mm/h\"\n\n# Concentration units\nCONCENTRATION_MICROGRAMS_PER_CUBIC_METER = \"µg/m³\"\nCONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = \"mg/m³\"\nCONCENTRATION_PARTS_PER_CUBIC_METER = \"p/m³\"\nCONCENTRATION_PARTS_PER_MILLION = \"ppm\"\nCONCENTRATION_PARTS_PER_BILLION = \"ppb\"\n\n# Speed units\nSPEED_MILLIMETERS_PER_DAY = \"mm/d\"\nSPEED_INCHES_PER_DAY = \"in/d\"\nSPEED_METERS_PER_SECOND = \"m/s\"\nSPEED_INCHES_PER_HOUR = \"in/h\"\nSPEED_KILOMETERS_PER_HOUR = \"km/h\"\nSPEED_MILES_PER_HOUR = \"mph\"\n\n# Signal_strength units\nSIGNAL_STRENGTH_DECIBELS = \"dB\"\nSIGNAL_STRENGTH_DECIBELS_MILLIWATT = \"dBm\"\n\n# Data units\nDATA_BITS = \"bit\"\nDATA_KILOBITS = \"kbit\"\nDATA_MEGABITS = \"Mbit\"\nDATA_GIGABITS = \"Gbit\"\nDATA_BYTES = \"B\"\nDATA_KILOBYTES = \"kB\"\nDATA_MEGABYTES = \"MB\"\nDATA_GIGABYTES = \"GB\"\nDATA_TERABYTES = \"TB\"\nDATA_PETABYTES = \"PB\"\nDATA_EXABYTES = \"EB\"\nDATA_ZETTABYTES = \"ZB\"\nDATA_YOTTABYTES = \"YB\"\nDATA_KIBIBYTES = \"KiB\"\nDATA_MEBIBYTES = \"MiB\"\nDATA_GIBIBYTES = \"GiB\"\nDATA_TEBIBYTES = \"TiB\"\nDATA_PEBIBYTES = \"PiB\"\nDATA_EXBIBYTES = \"EiB\"\nDATA_ZEBIBYTES = \"ZiB\"\nDATA_YOBIBYTES = \"YiB\"\nDATA_RATE_BITS_PER_SECOND = \"bit/s\"\nDATA_RATE_KILOBITS_PER_SECOND = \"kbit/s\"\nDATA_RATE_MEGABITS_PER_SECOND = \"Mbit/s\"\nDATA_RATE_GIGABITS_PER_SECOND = \"Gbit/s\"\nDATA_RATE_BYTES_PER_SECOND = \"B/s\"\nDATA_RATE_KILOBYTES_PER_SECOND = \"kB/s\"\nDATA_RATE_MEGABYTES_PER_SECOND = \"MB/s\"\nDATA_RATE_GIGABYTES_PER_SECOND = \"GB/s\"\nDATA_RATE_KIBIBYTES_PER_SECOND = \"KiB/s\"\nDATA_RATE_MEBIBYTES_PER_SECOND = \"MiB/s\"\nDATA_RATE_GIBIBYTES_PER_SECOND = \"GiB/s\"\n\n# #### SERVICES ####\nSERVICE_HOMEASSISTANT_STOP = \"stop\"\nSERVICE_HOMEASSISTANT_RESTART = \"restart\"\n\nSERVICE_TURN_ON = \"turn_on\"\nSERVICE_TURN_OFF = \"turn_off\"\nSERVICE_TOGGLE = \"toggle\"\nSERVICE_RELOAD = \"reload\"\n\nSERVICE_VOLUME_UP = \"volume_up\"\nSERVICE_VOLUME_DOWN = \"volume_down\"\nSERVICE_VOLUME_MUTE = \"volume_mute\"\nSERVICE_VOLUME_SET = \"volume_set\"\nSERVICE_MEDIA_PLAY_PAUSE = \"media_play_pause\"\nSERVICE_MEDIA_PLAY = \"media_play\"\nSERVICE_MEDIA_PAUSE = \"media_pause\"\nSERVICE_MEDIA_STOP = \"media_stop\"\nSERVICE_MEDIA_NEXT_TRACK = \"media_next_track\"\nSERVICE_MEDIA_PREVIOUS_TRACK = \"media_previous_track\"\nSERVICE_MEDIA_SEEK = \"media_seek\"\nSERVICE_REPEAT_SET = \"repeat_set\"\nSERVICE_SHUFFLE_SET = \"shuffle_set\"\n\nSERVICE_ALARM_DISARM = \"alarm_disarm\"\nSERVICE_ALARM_ARM_HOME = \"alarm_arm_home\"\nSERVICE_ALARM_ARM_AWAY = \"alarm_arm_away\"\nSERVICE_ALARM_ARM_NIGHT = \"alarm_arm_night\"\nSERVICE_ALARM_ARM_CUSTOM_BYPASS = \"alarm_arm_custom_bypass\"\nSERVICE_ALARM_TRIGGER = \"alarm_trigger\"\n\n\nSERVICE_LOCK = \"lock\"\nSERVICE_UNLOCK = \"unlock\"\n\nSERVICE_OPEN = \"open\"\nSERVICE_CLOSE = \"close\"\n\nSERVICE_CLOSE_COVER = \"close_cover\"\nSERVICE_CLOSE_COVER_TILT = \"close_cover_tilt\"\nSERVICE_OPEN_COVER = \"open_cover\"\nSERVICE_OPEN_COVER_TILT = \"open_cover_tilt\"\nSERVICE_SET_COVER_POSITION = \"set_cover_position\"\nSERVICE_SET_COVER_TILT_POSITION = \"set_cover_tilt_position\"\nSERVICE_STOP_COVER = \"stop_cover\"\nSERVICE_STOP_COVER_TILT = \"stop_cover_tilt\"\nSERVICE_TOGGLE_COVER_TILT = \"toggle_cover_tilt\"\n\nSERVICE_SELECT_OPTION = \"select_option\"\n\n# #### API / REMOTE ####\nSERVER_PORT = 8123\n\nURL_ROOT = \"/\"\nURL_API = \"/api/\"\nURL_API_STREAM = \"/api/stream\"\nURL_API_CONFIG = \"/api/config\"\nURL_API_STATES = \"/api/states\"\nURL_API_STATES_ENTITY = \"/api/states/{}\"\nURL_API_EVENTS = \"/api/events\"\nURL_API_EVENTS_EVENT = \"/api/events/{}\"\nURL_API_SERVICES = \"/api/services\"\nURL_API_SERVICES_SERVICE = \"/api/services/{}/{}\"\nURL_API_COMPONENTS = \"/api/components\"\nURL_API_ERROR_LOG = \"/api/error_log\"\nURL_API_LOG_OUT = \"/api/log_out\"\nURL_API_TEMPLATE = \"/api/template\"\nURL_API_HISTORY_PERIOD = \"/api/history/period/{}\"\n\nHTTP_OK = 200\nHTTP_CREATED = 201\nHTTP_ACCEPTED = 202\nHTTP_MOVED_PERMANENTLY = 301\nHTTP_BAD_REQUEST = 400\nHTTP_UNAUTHORIZED = 401\nHTTP_FORBIDDEN = 403\nHTTP_NOT_FOUND = 404\nHTTP_METHOD_NOT_ALLOWED = 405\nHTTP_UNPROCESSABLE_ENTITY = 422\nHTTP_TOO_MANY_REQUESTS = 429\nHTTP_INTERNAL_SERVER_ERROR = 500\nHTTP_BAD_GATEWAY = 502\nHTTP_SERVICE_UNAVAILABLE = 503\n\nHTTP_BASIC_AUTHENTICATION = \"basic\"\nHTTP_DIGEST_AUTHENTICATION = \"digest\"\n\nHTTP_HEADER_X_REQUESTED_WITH = \"X-Requested-With\"\n\nCONTENT_TYPE_JSON = \"application/json\"\nCONTENT_TYPE_MULTIPART = \"multipart/x-mixed-replace; boundary={}\"\nCONTENT_TYPE_TEXT_PLAIN = \"text/plain\"\n\n# The exit code to send to request a restart\nRESTART_EXIT_CODE = 100\n\nUNIT_NOT_RECOGNIZED_TEMPLATE: str = \"{} is not a recognized {} unit.\"\n\nLENGTH: str = \"length\"\nMASS: str = \"mass\"\nPRESSURE: str = \"pressure\"\nVOLUME: str = \"volume\"\nTEMPERATURE: str = \"temperature\"\nSPEED_MS: str = \"speed_ms\"\nILLUMINANCE: str = \"illuminance\"\n\nWEEKDAYS = [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"]\n\n# The degree of precision for platforms\nPRECISION_WHOLE = 1\nPRECISION_HALVES = 0.5\nPRECISION_TENTHS = 0.1\n\n# Static list of entities that will never be exposed to\n# cloud, alexa, or google_home components\nCLOUD_NEVER_EXPOSED_ENTITIES = [\"group.all_locks\"]\n\n# The ID of the Home Assistant Cast App\nCAST_APP_ID_HOMEASSISTANT = \"B12CE3CA\"\n"
  },
  {
    "path": "homeassistant_cli/helper.py",
    "content": "\"\"\"Helpers used by Home Assistant CLI (hass-cli).\"\"\"\n\nimport ast\nimport contextlib\nimport json\nimport logging\nimport shlex\nfrom collections.abc import Generator\nfrom http.client import HTTPConnection\nfrom typing import Any, cast\n\nfrom ruamel.yaml import YAML\nfrom tabulate import tabulate\n\nimport homeassistant_cli.const as const\nimport homeassistant_cli.yaml as yaml\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\ndef to_attributes(entry: str) -> dict[str, str]:\n    \"\"\"Convert list of key=value pairs to dictionary.\"\"\"\n    if not entry:\n        return {}\n\n    lexer = shlex.shlex(entry, posix=True)\n    lexer.whitespace_split = True\n    lexer.whitespace = \",\"\n    attributes_dict = {}  # type: Dict[str, str]\n    for pair in lexer:\n        if \"=\" not in pair:\n            continue\n        key, value = pair.split(\"=\", 1)\n        if value.strip().startswith(\"[\") and value.strip().endswith(\"]\"):\n            try:\n                value = ast.literal_eval(value)\n            except Exception:\n                pass\n        attributes_dict[key] = value\n    return attributes_dict\n\n\ndef to_tuples(entry: str) -> list[tuple[str, str]]:\n    \"\"\"Convert list of key=value pairs to list of tuples.\"\"\"\n    if not entry:\n        return []\n\n    lexer = shlex.shlex(entry, posix=True)\n    lexer.whitespace_split = True\n    lexer.whitespace = \",\"\n    attributes_list = []  # type: List[Tuple[str,str]]\n    attributes_list = list(\n        tuple(pair.split(\"=\", 1))\n        for pair in lexer  # type: ignore\n    )\n    return attributes_list\n\n\ndef raw_format_output(\n    output: str,\n    data: dict[str, Any] | list[dict[str, Any]],\n    yamlparser: YAML,\n    columns: list | None = None,\n    no_headers: bool = False,\n    table_format: str = \"plain\",\n    sort_by: str | None = None,\n) -> str:\n    \"\"\"Format the raw output.\"\"\"\n    if output == \"auto\":\n        _LOGGING.debug(\"Output `auto` thus using %s\", const.DEFAULT_DATAOUTPUT)\n        output = const.DEFAULT_DATAOUTPUT\n\n    if sort_by and isinstance(data, list):\n        _sort_table(data, sort_by)\n\n    if output == \"json\":\n        try:\n            return json.dumps(data, indent=2, sort_keys=False)\n        except ValueError:\n            return str(data)\n    elif output == \"ndjson\":\n        try:\n            return json.dumps(data)\n        except ValueError:\n            return str(data)\n    elif output == \"yaml\":\n        try:\n            return cast(str, yaml.dumpyaml(yamlparser, data))\n        except ValueError:\n            return str(data)\n    elif output == \"table\":\n        from jsonpath_ng import parse\n\n        if not columns:\n            columns = const.COLUMNS_DEFAULT\n\n        fmt = [(v[0], parse(v[1] if len(v) > 1 else v[0])) for v in columns]\n\n        result = []\n\n        if no_headers:\n            headers = []  # type: List[str]\n        else:\n            headers = [v[0] for v in fmt]\n\n        # In case data passed in is a single element\n        # we turn it into a single item list for better table output\n        if not isinstance(data, list):\n            data = [data]\n\n        for item in data:\n            row = []\n            for fmtpair in fmt:\n                val = [match.value for match in fmtpair[1].find(item)]\n                row.append(\", \".join(map(str, val)))\n            result.append(row)\n\n        res = tabulate(result, headers=headers, tablefmt=table_format)  # type: str\n        return res\n    else:\n        raise ValueError(\n            f\"Output Format was {output}, expected either 'json' or 'yaml'\"\n        )\n\n\ndef _sort_table(result: list[Any], sort_by: str) -> list[Any]:\n    \"\"\"Sort the content of a table.\"\"\"\n    from jsonpath_ng import parse\n\n    expr = parse(sort_by)\n\n    def _internal_sort(row: dict[Any, str]) -> Any:\n        val = next(iter([match.value for match in expr.find(row)]), None)\n        return (val is None, val)\n\n    result.sort(key=_internal_sort)\n    return result\n\n\ndef format_output(\n    ctx: Configuration,\n    data: list[dict[str, Any]],\n    columns: list | None = None,\n) -> str:\n    \"\"\"Format data to output based on settings in ctx/Context.\"\"\"\n    return raw_format_output(\n        ctx.output,\n        data,\n        ctx.yaml(),\n        columns,\n        ctx.no_headers,\n        ctx.table_format,\n        ctx.sort_by,\n    )\n\n\ndef debug_requests_on() -> None:\n    \"\"\"Switch on logging of the requests module.\"\"\"\n    HTTPConnection.set_debuglevel(cast(HTTPConnection, HTTPConnection), 1)\n\n    logging.basicConfig()\n    logging.getLogger().setLevel(logging.DEBUG)\n    requests_log = logging.getLogger(\"requests.packages.urllib3\")\n    requests_log.setLevel(logging.DEBUG)\n    requests_log.propagate = True\n\n\ndef debug_requests_off() -> None:\n    \"\"\"Switch off logging of the requests module.\n\n    Might have some side-effects.\n    \"\"\"\n    HTTPConnection.set_debuglevel(cast(HTTPConnection, HTTPConnection), 1)\n\n    root_logger = logging.getLogger()\n    root_logger.setLevel(logging.WARNING)\n    root_logger.handlers = []\n    requests_log = logging.getLogger(\"requests.packages.urllib3\")\n    requests_log.setLevel(logging.WARNING)\n    requests_log.propagate = False\n\n\n@contextlib.contextmanager\ndef debug_requests() -> Generator:\n    \"\"\"Yieldable way to turn on debugs for requests.\n\n    with debug_requests(): <do things>\n    \"\"\"\n    debug_requests_on()\n    yield\n    debug_requests_off()\n"
  },
  {
    "path": "homeassistant_cli/plugins/area.py",
    "content": "\"\"\"Area (registry) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\nfrom re import Pattern\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.const as const\nimport homeassistant_cli.helper as helper\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"area\")\n@pass_context\ndef cli(ctx: Configuration) -> None:\n    \"\"\"Get info and operate on areas from Home Assistant (EXPERIMENTAL).\"\"\"\n\n\n@cli.command(\"list\")\n@click.argument(\"area_filter\", default=\".*\", required=False)\n@pass_context\ndef listcmd(ctx: Configuration, area_filter: str) -> None:\n    \"\"\"List all areas from Home Assistant.\n\n    AREA_FILTER - optional regex to filter by area name\n    \"\"\"\n    ctx.auto_output(\"table\")\n\n    areas = api.get_areas(ctx)\n\n    result: list[dict] = []\n    if area_filter == \".*\":\n        result = areas\n    else:\n        area_filter_regex: Pattern[str] = re.compile(area_filter)\n\n        for area in areas:\n            if area_filter_regex.search(area[\"name\"]):\n                result.append(area)\n\n    cols = [(\"ID\", \"area_id\"), (\"NAME\", \"name\")]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"create\")\n@click.argument(\"names\", nargs=-1, required=True)\n@pass_context\ndef create(ctx: Configuration, names: tuple[str, ...]) -> None:\n    \"\"\"Create an area.\n\n    NAMES - one or more area names to create\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    for name in names:\n        result = api.create_area(ctx, name)\n\n        ctx.echo(\n            helper.format_output(\n                ctx,\n                [result],\n                columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,\n            )\n        )\n\n\n@cli.command(\"delete\")\n@click.argument(\n    \"names\",\n    nargs=-1,\n    required=True,\n    shell_complete=autocompletion.areas,  # type: ignore\n)\n@click.option(\n    \"--confirm\",\n    is_flag=True,\n    default=False,\n    help=\"Confirm deletion without prompting\",\n)\n@pass_context\ndef delete(ctx: Configuration, names: tuple[str, ...], confirm: bool) -> None:\n    \"\"\"Delete an area.\n\n    NAMES - one or more area names or id to delete\n    \"\"\"\n    ctx.auto_output(\"data\")\n    excode = 0\n\n    for name in names:\n        area = api.find_area(ctx, name)\n        if not area:\n            _LOGGING.error(\"Could not find area with id or name: %s\", name)\n            excode = 1\n            continue\n\n        if not confirm:\n            click.confirm(\n                f\"Are you sure you want to delete '{area['name']}'\"\n                f\" ({area['area_id']})?\",\n                abort=True,\n            )\n\n        result = api.delete_area(ctx, area[\"area_id\"])\n\n        if result.get(\"success\"):\n            ctx.echo(f\"Successfully deleted area: {area['name']} ({area['area_id']})\")\n        else:\n            ctx.echo(helper.format_output(ctx, result))\n\n    if excode != 0:\n        sys.exit(excode)\n\n\n@cli.command(\"rename\")\n@click.argument(\n    \"old_name\",\n    required=True,\n    shell_complete=autocompletion.areas,  # type: ignore\n)\n@click.argument(\"new_name\", required=True)\n@pass_context\ndef rename(ctx: Configuration, old_name: str, new_name: str) -> None:\n    \"\"\"Rename an area.\"\"\"\n    ctx.auto_output(\"data\")\n\n    area = api.find_area(ctx, old_name)\n    if not area:\n        _LOGGING.error(\"Could not find area with id or name: %s\", old_name)\n        sys.exit(1)\n\n    result = api.rename_area(ctx, area[\"area_id\"], new_name)\n\n    ctx.echo(\n        helper.format_output(\n            ctx,\n            [result],\n            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,\n        )\n    )\n"
  },
  {
    "path": "homeassistant_cli/plugins/completion.py",
    "content": "\"\"\"Auto-completion for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\nfrom click._bashcomplete import get_completion_script\n\nfrom homeassistant_cli.cli import pass_context\n\n\n@click.group(\"completion\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Output shell completion code for the specified shell (bash or zsh).\"\"\"\n\n\ndef dump_script(shell: str) -> None:\n    \"\"\"Dump the script content.\"\"\"\n    # todo resolve actual script name in case user aliased it\n    prog_name = \"hass-cli\"\n    cvar = f\"_{prog_name.replace('-', '_').upper()}_COMPLETE\"\n\n    click.echo(get_completion_script(prog_name, cvar, shell))\n\n\n@cli.command()\n@pass_context\ndef bash(ctx):\n    \"\"\"Output shell completion code for bash.\"\"\"\n    dump_script(\"bash\")\n\n\n@cli.command()\n@pass_context\ndef zsh(ctx):\n    \"\"\"Output shell completion code for zsh.\"\"\"\n    dump_script(\"zsh\")\n"
  },
  {
    "path": "homeassistant_cli/plugins/config.py",
    "content": "\"\"\"Configuration plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\n\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import format_output\n\n\n@click.group(\"config\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Get configuration from a Home Assistant instance.\"\"\"\n    ctx.auto_output(\"table\")\n\n\nCOLUMNS_DETAILS = [\n    (\"VERSION\", \"version\"),\n    (\"CONFIG\", \"config_dir\"),\n    (\"TZ\", \"time_zone\"),\n    (\"LOCATION\", \"location_name\"),\n    (\"LONGITUDE\", \"longitude\"),\n    (\"LATITUDE\", \"latitude\"),\n    (\"ELEVATION\", \"elevation\"),\n    (\"TZ\", \"time_zone\"),\n    (\"UNITS\", \"unit_system\"),\n]\n\n\n@cli.command()\n@pass_context\ndef full(ctx: Configuration):\n    \"\"\"Get full details on the configuration from Home Assistant.\"\"\"\n    click.echo(\n        format_output(\n            ctx,\n            [api.get_config(ctx)],\n            columns=ctx.columns if ctx.columns else COLUMNS_DETAILS,\n        )\n    )\n\n\n@cli.command()\n@pass_context\ndef integrations(ctx: Configuration):\n    \"\"\"Get loaded integrations from Home Assistant.\"\"\"\n    click.echo(\n        format_output(\n            ctx,\n            api.get_config(ctx)[\"components\"],\n            columns=ctx.columns if ctx.columns else [(\"INTEGRATIONS\", \"$\")],\n        )\n    )\n\n\n@cli.command()\n@pass_context\ndef whitelist_dirs(ctx: Configuration):\n    \"\"\"Get the whitelisted directories from Home Assistant.\"\"\"\n    click.echo(\n        format_output(\n            ctx,\n            api.get_config(ctx)[\"whitelist_external_dirs\"],\n            columns=ctx.columns if ctx.columns else [(\"DIRECTORY\", \"$\")],\n        )\n    )\n\n\n@cli.command()\n@pass_context\ndef release(ctx: Configuration):\n    \"\"\"Get the release of Home Assistant.\"\"\"\n    click.echo(\n        format_output(\n            ctx,\n            [api.get_config(ctx)[\"version\"]],\n            columns=ctx.columns if ctx.columns else [(\"VERSION\", \"$\")],\n        )\n    )\n"
  },
  {
    "path": "homeassistant_cli/plugins/device.py",
    "content": "\"\"\"Device (registry) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\nfrom typing import Any, Dict, List, Optional\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.helper as helper\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"device\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Get info and operate on devices from Home Assistant.\"\"\"\n\n\n@cli.command(\"list\")\n@click.argument(\"device_filter\", default=\".*\", required=False)\n@pass_context\ndef list_cmd(ctx: Configuration, device_filter: str):\n    \"\"\"List all devices from Home Assistant.\n\n    DEVICE_FILTER - regular expression to filter devices by name\n    \"\"\"\n    ctx.auto_output(\"table\")\n\n    areas = api.get_areas(ctx)\n\n    devices = api.get_devices(ctx)\n\n    result = []  # type: List[Dict]\n    if device_filter == \".*\":\n        result = devices\n    else:\n        device_filter_regex = re.compile(device_filter)  # type: Pattern\n\n        for device in devices:\n            if device_filter_regex.search(device[\"name\"]):\n                result.append(device)\n\n    for device in devices:\n        area = next((a for a in areas if a[\"area_id\"] == device[\"area_id\"]), None)\n        if area:\n            device[\"area_name\"] = area[\"name\"]\n\n    cols = [\n        (\"ID\", \"id\"),\n        (\"NAME\", \"name\"),\n        (\"NAME BY USER\", \"name_by_user\"),\n        (\"MODEL\", \"model\"),\n        (\"MANUFACTURER\", \"manufacturer\"),\n        (\"AREA\", \"area_name\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"assign\")\n@click.argument(\n    \"area_id_or_name\",\n    required=True,\n    shell_complete=autocompletion.areas,  # type: ignore\n)\n@click.argument(\"names\", nargs=-1, required=False)\n@click.option(\"--match\", help=\"Expression used to find devices matching that name\")\n@pass_context\ndef assign(\n    ctx: Configuration,\n    area_id_or_name,\n    names: list[str],\n    match: str | None = None,\n):\n    \"\"\"Update area on one or more devices.\n\n    NAMES - one or more name or id (Optional)\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    devices = api.get_devices(ctx)\n\n    result = []  # type: List[Dict]\n\n    area = api.find_area(ctx, area_id_or_name)\n    if not area:\n        _LOGGING.error(\"Could not find area with id or name: %s\", area_id_or_name)\n        sys.exit(1)\n\n    if match:\n        if match == \".*\":\n            result = devices\n        else:\n            device_filter_regex = re.compile(match)  # type: Pattern\n\n            for device in devices:\n                if device_filter_regex.search(device[\"name\"]):\n                    result.append(device)\n\n    for id_or_name in names:\n        device = next(\n            (x for x in devices if x[\"id\"] == id_or_name),\n            None,  # type: ignore\n        )\n        if not device:\n            device = next(\n                (x for x in devices if x[\"name\"] == id_or_name),\n                None,  # type: ignore\n            )\n        if not device:\n            _LOGGING.error(\"Could not find device with id or name: %s\", id_or_name)\n            sys.exit(1)\n        result.append(device)\n\n    for device in result:\n        output = api.assign_area(ctx, device[\"id\"], area[\"area_id\"])\n        if output[\"success\"]:\n            ctx.echo(\n                \"Successfully assigned '{}' to '{}'\".format(\n                    area[\"name\"], device[\"name\"]\n                )\n            )\n        else:\n            _LOGGING.error(\n                \"Failed to assign '%s' to '%s'\", area[\"name\"], device[\"name\"]\n            )\n\n            ctx.echo(str(output))\n\n\n@cli.command(\"rename\")\n@click.argument(\"device_id_or_name\", required=True)\n@click.argument(\"new_name\", required=True)\n@pass_context\ndef rename(\n    ctx: Configuration,\n    device_id_or_name,\n    new_name,\n):\n    \"\"\"Update name of specified device.\"\"\"\n    ctx.auto_output(\"data\")\n\n    devices = api.get_devices(ctx)\n\n    device = next(\n        (x for x in devices if x[\"id\"] == device_id_or_name),\n        None,  # type: ignore\n    )\n    if not device:\n        device = next(\n            (x for x in devices if x[\"name\"] == device_id_or_name),\n            None,  # type: ignore\n        )\n    if not device:\n        _LOGGING.error(\"Could not find device with id or name: %s\", device_id_or_name)\n        sys.exit(1)\n\n    output = api.rename_device(ctx, device[\"id\"], new_name)\n    if output[\"success\"]:\n        ctx.echo(\n            \"Successfully renamed '{}' from {} to '{}'\".format(\n                device_id_or_name, device[\"name_by_user\"], new_name\n            )\n        )\n    else:\n        _LOGGING.error(\"Failed to rename '%s' to '%s'\", device_id_or_name, new_name)\n\n        ctx.echo(str(output))\n\n\n@cli.command(\"list-by-area\")\n@click.argument(\n    \"area_id_or_name\",\n    required=True,\n    shell_complete=autocompletion.areas,  # type: ignore\n)\n@pass_context\ndef list_by_area(ctx: Configuration, area_id_or_name: str):\n    \"\"\"List all devices in a specified area.\n\n    AREA_ID_OR_NAME - area id or name\n    \"\"\"\n    ctx.auto_output(\"table\")\n\n    area = api.find_area(ctx, area_id_or_name)\n    if not area:\n        _LOGGING.error(\"Could not find area with id or name: %s\", area_id_or_name)\n        sys.exit(1)\n\n    devices = api.get_devices(ctx)\n    result = [d for d in devices if d[\"area_id\"] == area[\"area_id\"]]\n\n    cols = [\n        (\"ID\", \"id\"),\n        (\"NAME\", \"name\"),\n        (\"MODEL\", \"model\"),\n        (\"MANUFACTURER\", \"manufacturer\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"delete\")\n@click.argument(\"device_id_or_name\", required=True)\n@click.option(\n    \"--confirm\",\n    is_flag=True,\n    default=False,\n    help=\"Confirm deletion without prompting\",\n)\n@pass_context\ndef delete(ctx: Configuration, device_id_or_name: str, confirm: bool):\n    \"\"\"Delete a specified device.\"\"\"\n    ctx.auto_output(\"data\")\n\n    devices = api.get_devices(ctx)\n\n    device = next(\n        (x for x in devices if x[\"id\"] == device_id_or_name),\n        None,  # type: ignore\n    )\n    if not device:\n        device = next(\n            (x for x in devices if x[\"name\"] == device_id_or_name),\n            None,  # type: ignore\n        )\n    if not device:\n        _LOGGING.error(\"Could not find device with id or name: %s\", device_id_or_name)\n        sys.exit(1)\n\n    if not confirm:\n        click.confirm(\n            f\"Are you sure you want to delete '{device['name']}' [{device['id']}]?\",\n            abort=True,\n        )\n\n    output = api.delete_device(ctx, device[\"id\"])\n    if output[\"success\"]:\n        ctx.echo(\"Successfully deleted device '{}'\".format(device[\"name\"]))\n    else:\n        _LOGGING.error(\"Failed to delete device '%s'\", device_id_or_name)\n        ctx.echo(str(output))\n"
  },
  {
    "path": "homeassistant_cli/plugins/discover.py",
    "content": "\"\"\"Discovery plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport click\n\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import format_output\n\n\n@click.command(\"discover\")\n@click.option(\"--raw\", is_flag=True, help=\"Include raw data found during scan.\")\n@pass_context\ndef cli(ctx: Configuration, raw):\n    \"\"\"Discovery for the local network.\"\"\"\n    from netdisco.discovery import NetworkDiscovery\n\n    click.echo(\"Running discovery on network (might take a while)...\")\n    netdiscovery = NetworkDiscovery()\n    netdiscovery.scan()\n\n    for device in netdiscovery.discover():\n        info = netdiscovery.get_info(device)\n        click.echo(f\"{device}:\\n{format_output(ctx, info)}\")\n\n    if raw:\n        click.echo(\"Raw data:\")\n        netdiscovery.print_raw_data()\n\n    netdiscovery.stop()\n"
  },
  {
    "path": "homeassistant_cli/plugins/entity.py",
    "content": "\"\"\"Entity plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.const as const\nimport homeassistant_cli.helper as helper\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"entity\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Get info on entities from Home Assistant.\"\"\"\n\n\n@cli.command(\"list\")\n@click.argument(\"entity_filter\", default=\".*\", required=False)\n@pass_context\ndef listcmd(ctx: Configuration, entity_filter: str):\n    \"\"\"List all entities from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n\n    areas = api.get_areas(ctx)\n\n    entities = api.get_entities(ctx)\n\n    result = []  # type: List[Dict]\n    if entity_filter == \".*\":\n        result = entities\n    else:\n        entity_filter_regex = re.compile(entity_filter)  # type: Pattern\n\n        for entity in entities:\n            if entity_filter_regex.search(entity[\"entity_id\"]):\n                result.append(entity)\n\n    for entity in entities:\n        area = next((a for a in areas if a[\"area_id\"] == entity[\"area_id\"]), None)\n        if area:\n            entity[\"area_name\"] = area[\"name\"]\n\n    cols = [\n        (\"ENTITY_ID\", \"entity_id\"),\n        (\"NAME\", \"name\"),\n        (\"DEVICE_ID\", \"device_id\"),\n        (\"PLATFORM\", \"platform\"),\n        (\"AREA\", \"area_name\"),\n        (\"CONFIG_ENTRY_ID\", \"config_entry_id\"),\n        (\"DISABLED_BY\", \"disabled_by\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"assign\")\n@click.argument(\n    \"area_id_or_name\",\n    required=True,\n    shell_complete=autocompletion.areas,  # type: ignore\n)\n@click.argument(\"names\", nargs=-1, required=False)\n@click.option(\"--match\", help=\"Expression used to find entities matching that name\")\n@pass_context\ndef assign(\n    ctx: Configuration,\n    area_id_or_name,\n    names: list[str],\n    match: str | None = None,\n):\n    \"\"\"Update area on one or more entities.\n\n    NAMES - one or more name or id (Optional)\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    entities = api.get_entities(ctx)\n\n    result = []  # type: List[Dict]\n\n    area = api.find_area(ctx, area_id_or_name)\n    if not area:\n        _LOGGING.error(\"Could not find area with id or name: %s\", area_id_or_name)\n        sys.exit(1)\n\n    if match:\n        if match == \".*\":\n            result = entities\n        else:\n            entity_filter_regex = re.compile(match)  # type: Pattern\n\n            for entity in entities:\n                if entity_filter_regex.search(entity[\"name\"]):\n                    result.append(entity)\n\n    for id_or_name in names:\n        entity = next(\n            (x for x in entities if x[\"entity_id\"] == id_or_name),\n            None,  # type: ignore\n        )\n        if not entity:\n            entity = next(\n                (x for x in entities if x[\"name\"] == id_or_name),\n                None,  # type: ignore\n            )\n        if not entity:\n            _LOGGING.error(\"Could not find entity with id or name: %s\", id_or_name)\n            sys.exit(1)\n        result.append(entity)\n\n    for entity in result:\n        output = api.assign_entity_area(ctx, entity[\"entity_id\"], area[\"area_id\"])\n        if output[\"success\"]:\n            ctx.echo(\n                \"Successfully assigned '{}' to '{}'\".format(\n                    area[\"name\"], entity[\"entity_id\"]\n                )\n            )\n        else:\n            _LOGGING.error(\n                \"Failed to assign '%s' to '%s'\",\n                area[\"name\"],\n                entity[\"entity_id\"],\n            )\n\n            ctx.echo(str(output))\n\n\n@cli.command(\"rename\")\n@click.argument(\n    \"old_id\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@click.option(\"--name\", required=False)\n@click.argument(\n    \"new_id\",\n    required=False,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef rename(ctx, old_id, new_id, name):\n    \"\"\"Rename a entity.\"\"\"\n    ctx.auto_output(\"data\")\n\n    if not new_id and not name:\n        _LOGGING.error(\"Need to at least specify either a new id or new name\")\n        sys.exit(1)\n\n    entity = api.get_entity(ctx, old_id)\n    if not entity:\n        _LOGGING.error(\"Could not find entity with ID: %s\", old_id)\n        sys.exit(1)\n\n    result = api.rename_entity(ctx, old_id, new_id, name)\n\n    ctx.echo(\n        helper.format_output(\n            ctx,\n            [result],\n            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,\n        )\n    )\n\n\n@cli.command(\"delete\")\n@click.argument(\n    \"entity_id\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@click.option(\n    \"--confirm\",\n    is_flag=True,\n    default=False,\n    help=\"Confirm deletion without prompting\",\n)\n@pass_context\ndef delete(ctx: Configuration, entity_id: str, confirm: bool) -> None:\n    \"\"\"Delete an entity.\n\n    ENTITY_ID - the entity_id of the entity to delete\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    entity = api.get_entity(ctx, entity_id)\n    if not entity:\n        _LOGGING.error(\"Could not find entity with ID: %s\", entity_id)\n        sys.exit(1)\n\n    if not confirm:\n        click.confirm(\n            f\"Are you sure you want to delete '{entity_id}'?\",\n            abort=True,\n        )\n\n    result = api.delete_entity(ctx, entity_id)\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully deleted entity '{entity_id}'\")\n    else:\n        _LOGGING.error(\"Failed to delete entity: %s\", entity_id)\n        ctx.echo(str(result))\n\n\n@cli.command(\"enable\")\n@click.argument(\n    \"entity_id\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef enable(ctx: Configuration, entity_id: str) -> None:\n    \"\"\"Enable an entity.\n\n    ENTITY_ID - the entity_id of the entity to enable\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    entity = api.get_entity(ctx, entity_id)\n    if not entity:\n        _LOGGING.error(\"Could not find entity with ID: %s\", entity_id)\n        sys.exit(1)\n\n    result = api.enable_entity(ctx, entity_id, None)\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully enabled entity '{entity_id}'\")\n    else:\n        _LOGGING.error(\"Failed to enable entity: %s\", entity_id)\n        ctx.echo(str(result))\n\n\n@cli.command(\"disable\")\n@click.argument(\n    \"entity_id\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef disable(ctx: Configuration, entity_id: str) -> None:\n    \"\"\"Disable an entity.\n\n    ENTITY_ID - the entity_id of the entity to disable\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    entity = api.get_entity(ctx, entity_id)\n    if not entity:\n        _LOGGING.error(\"Could not find entity with ID: %s\", entity_id)\n        sys.exit(1)\n\n    result = api.enable_entity(ctx, entity_id, \"user\")\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully disabled entity '{entity_id}'\")\n    else:\n        _LOGGING.error(\"Failed to disable entity: %s\", entity_id)\n        ctx.echo(str(result))\n"
  },
  {
    "path": "homeassistant_cli/plugins/event.py",
    "content": "\"\"\"Event plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.exceptions import HomeAssistantCliError\nfrom homeassistant_cli.helper import format_output, raw_format_output\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"event\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Interact with events.\"\"\"\n\n\n@cli.command()\n@click.argument(\n    \"event\",\n    required=True,\n    shell_complete=autocompletion.events,  # type: ignore\n)\n@click.option(\n    \"--json\",\n    help=\"Raw JSON state to use for event. Overrides any other statevalues provided.\",\n)\n@pass_context\ndef fire(ctx: Configuration, event, json):\n    \"\"\"Fire event in Home Assistant.\"\"\"\n    if json:\n        click.echo(f\"Fire {event}\")\n        response = api.fire_event(ctx, event, json_.loads(json))\n    else:\n        existing = raw_format_output(ctx.output, [{}], ctx.yaml())\n        new = click.edit(existing, extension=f\".{ctx.output}\")\n\n        if new:\n            click.echo(f\"Fire {event}\")\n            if ctx.output == \"yaml\":\n                data = ctx.yamlload(new)\n            else:\n                data = json_.loads(new)\n\n            response = api.fire_event(ctx, event, data)\n        else:\n            click.echo(\"No edits/changes.\")\n            return\n\n    if response:\n        ctx.echo(raw_format_output(ctx.output, [response], ctx.yaml()))\n\n\n@cli.command()\n@click.argument(\"event_type\", required=False)\n@pass_context\ndef watch(ctx: Configuration, event_type):\n    \"\"\"Subscribe and print events.\n\n    EVENT-TYPE even type to subscribe to. if empty subscribe to all.\n    \"\"\"\n    frame = {\"type\": \"subscribe_events\"}\n\n    cols = [(\"EVENT_TYPE\", \"event_type\"), (\"DATA\", \"$.data\")]\n\n    def _msghandler(msg: dict) -> None:\n        if msg[\"type\"] == \"event\":\n            ctx.echo(\n                format_output(\n                    ctx,\n                    msg[\"event\"],\n                    columns=ctx.columns if ctx.columns else cols,\n                )\n            )\n        elif msg[\"type\"] == \"auth_invalid\":\n            raise HomeAssistantCliError(msg.get(\"message\"))\n\n    if event_type:\n        frame[\"event_type\"] = event_type\n\n    api.wsapi(ctx, frame, _msghandler)\n"
  },
  {
    "path": "homeassistant_cli/plugins/ha.py",
    "content": "\"\"\"Home Assistant Operating System plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nfrom typing import Any, Dict, List, cast\n\nimport click\nfrom packaging.version import Version\nfrom requests.exceptions import HTTPError\n\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.exceptions import HomeAssistantCliError\nfrom homeassistant_cli.helper import format_output\n\n_LOGGING = logging.getLogger(__name__)\n\n# These commands loosely based on what can be found in\n# https://developers.home-assistant.io/docs/api/supervisor/endpoints\n\n\n@click.group(\"ha\")\n@pass_context\ndef cli(ctx: Configuration):\n    \"\"\"Home Assistant Operating System commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\ndef _report(ctx, cmd, method, response) -> None:\n    \"\"\"Create a report.\"\"\"\n    response.raise_for_status()\n\n    if response.ok:\n        try:\n            ctx.echo(format_output(ctx, response.json()))\n        except json_.decoder.JSONDecodeError:\n            _LOGGING.debug(\"Response could not be parsed as JSON\")\n            ctx.echo(response.text)\n    else:\n        _LOGGING.warning(\n            \"%s: <No output returned from %s %s>\",\n            response.status_code,\n            cmd,\n            method,\n        )\n\n\ndef _handle(ctx, method, httpmethod=\"get\", raw=False) -> None:\n    \"\"\"Handle the data.\"\"\"\n    method = f\"/{method}\"\n    response = api.restapi_supervisor(ctx, httpmethod, method)\n\n    _report(ctx, httpmethod, method, response)\n\n\ndef _handle_raw(ctx, method, httpmethod=\"get\") -> dict:\n    \"\"\"Handle raw data.\"\"\"\n    method = f\"/{method}\"\n    response = api.restapi_supervisor(ctx, httpmethod, method)\n    return response.json()\n\n\n# Addon/Apps endpoints\n#########################################################################\n@cli.group(\"addons\")\n@pass_context\ndef addons(ctx: Configuration):\n    \"\"\"Home Assistant addons commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@addons.command(\"all\")\n@pass_context\ndef addons_all(ctx: Configuration):\n    \"\"\"Home Assistant addons info.\"\"\"\n    _handle(ctx, \"addons\")\n\n\n@addons.command(\"reload\")\n@pass_context\ndef addons_reload(ctx: Configuration):\n    \"\"\"Home Assistant addons reload.\"\"\"\n    _handle(ctx, \"addons/reload\", \"post\")\n\n\n# Audio endpoints\n#########################################################################\n@cli.group(\"audio\")\n@pass_context\ndef audio(ctx: Configuration):\n    \"\"\"Home Assistant audio commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@audio.command(\"info\")\n@pass_context\ndef audio_info(ctx: Configuration):\n    \"\"\"Home Assistant audio info.\"\"\"\n    _handle(ctx, \"audio/info\")\n\n\n@audio.command(\"stats\")\n@pass_context\ndef audio_stats(ctx: Configuration):\n    \"\"\"Home Assistant audio stats.\"\"\"\n    _handle(ctx, \"audio/stats\")\n\n\n@audio.command(\"logs\")\n@pass_context\ndef audio_logs(ctx: Configuration):\n    \"\"\"Home Assistant audio logs.\"\"\"\n    _handle(ctx, \"audio/logs\")\n\n\n@audio.command(\"reload\")\n@pass_context\ndef audio_reload(ctx: Configuration):\n    \"\"\"Home Assistant audio reload.\"\"\"\n    _handle(ctx, \"audio/reload\", \"post\")\n\n\n@audio.command(\"restart\")\n@pass_context\ndef audio_restart(ctx: Configuration):\n    \"\"\"Home Assistant audio restart.\"\"\"\n    _handle(ctx, \"audio/restart\", \"post\")\n\n\n# Auth endpoints\n#########################################################################\n@cli.group(\"auth\")\n@pass_context\ndef auth(ctx: Configuration):\n    \"\"\"Home Assistant auth commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@auth.command(\"list\")\n@pass_context\ndef auth_list(ctx: Configuration):\n    \"\"\"Home Assistant auth list.\"\"\"\n    _handle(ctx, \"auth/list\")\n\n\n# Backup endpoints\n#########################################################################\n@cli.group(\"backup\")\n@pass_context\ndef backup(ctx: Configuration):\n    \"\"\"Home Assistant backup commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@backup.command(\"info\")\n@pass_context\ndef backup_info(ctx: Configuration):\n    \"\"\"Home Assistant backup info.\"\"\"\n    _handle(ctx, \"backups/info\")\n\n\n@backup.command(\"reload\")\n@pass_context\ndef backup_reload(ctx: Configuration):\n    \"\"\"Home Assistant backups reload.\"\"\"\n    _handle(ctx, \"backups/reload\", \"post\")\n\n\n# CLI endpoints\n#########################################################################\n@cli.group(\"ha-cli\")\n@pass_context\ndef ha_cli(ctx: Configuration):\n    \"\"\"Home Assistant ha-cli commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@ha_cli.command(\"info\")\n@pass_context\ndef ha_info(ctx: Configuration):\n    \"\"\"Home Assistant ha-cli info.\"\"\"\n    _handle(ctx, \"cli/info\")\n\n\n@ha_cli.command(\"update\")\n@pass_context\ndef ha_update(ctx: Configuration):\n    \"\"\"Home Assistant ha-cli update.\"\"\"\n    response = _handle_raw(ctx, \"cli/info\")\n    data = response[\"data\"]\n    current_version = int(data[\"version\"])\n    latest_version = int(data[\"version_latest\"])\n    if current_version == latest_version:\n        ctx.echo(\"Already running the latest release\")\n    else:\n        try:\n            _handle(ctx, \"cli/update\", \"post\")\n        except (HomeAssistantCliError, HTTPError):\n            pass\n\n\n@ha_cli.command(\"stats\")\n@pass_context\ndef ha_stats(ctx: Configuration):\n    \"\"\"Home Assistant ha-cli stats.\"\"\"\n    _handle(ctx, \"cli/stats\")\n\n\n# Core endpoints\n#########################################################################\n@cli.group(\"core\")\n@pass_context\ndef core(ctx: Configuration):\n    \"\"\"Home Assistant core commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@core.command(\"info\")\n@pass_context\ndef core_info(ctx: Configuration):\n    \"\"\"Home Assistant core info.\"\"\"\n    _handle(ctx, \"core/info\")\n\n\n@core.command(\"update\")\n@pass_context\ndef core_update(ctx: Configuration):\n    \"\"\"Home Assistant core update.\"\"\"\n    response = _handle_raw(ctx, \"core/info\")\n    data = response[\"data\"]\n    current_version = data[\"version\"]\n    latest_version = data[\"version_latest\"]\n    if Version(current_version) == Version(latest_version):\n        ctx.echo(\"Already running the latest release\")\n    else:\n        try:\n            _handle(ctx, \"core/update\", \"post\")\n        except (HomeAssistantCliError, HTTPError):\n            pass\n\n\n@core.command(\"logs\")\n@pass_context\ndef core_logs(ctx: Configuration):\n    \"\"\"Home Assistant core logs.\"\"\"\n    _handle(ctx, \"core/logs\")\n\n\n@core.command(\"restart\")\n@pass_context\ndef core_restart(ctx: Configuration):\n    \"\"\"Home Assistant core restart.\"\"\"\n    try:\n        _handle(ctx, \"core/restart\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@core.command(\"check\")\n@pass_context\ndef core_check(ctx: Configuration):\n    \"\"\"Home Assistant core check.\"\"\"\n    try:\n        _handle(ctx, \"core/check\", \"post\")\n    except (HomeAssistantCliError, HTTPError):\n        _handle(ctx, \"core/logs\")\n\n\n@core.command(\"start\")\n@pass_context\ndef core_start(ctx: Configuration):\n    \"\"\"Home Assistant core start.\"\"\"\n    try:\n        _handle(ctx, \"core/start\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@core.command(\"stop\")\n@pass_context\ndef core_stop(ctx: Configuration):\n    \"\"\"Home Assistant core stop.\"\"\"\n    try:\n        _handle(ctx, \"core/stop\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@core.command(\"rebuild\")\n@pass_context\ndef core_rebuild(ctx: Configuration):\n    \"\"\"Home Assistant core rebuild.\"\"\"\n    try:\n        _handle(ctx, \"core/rebuild\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@core.command(\"options\")\n@pass_context\ndef core_options(ctx: Configuration):\n    \"\"\"Home Assistant core options.\"\"\"\n    _handle(ctx, \"core/options\", \"post\")\n\n\n@core.command(\"websocket\")\n@pass_context\ndef core_websocket(ctx: Configuration):\n    \"\"\"Home Assistant core websocket.\"\"\"\n    try:\n        _handle(ctx, \"core/websocket\")\n    except (HomeAssistantCliError, HTTPError):\n        pass\n\n\n@core.command(\"stats\")\n@pass_context\ndef core_stats(ctx: Configuration):\n    \"\"\"Home Assistant core stats.\"\"\"\n    _handle(ctx, \"core/stats\")\n\n\n# Discovery endpoints\n#########################################################################\n# Not implemented\n# @cli.group(\"discovery\")\n# @pass_context\n# def discovery(ctx: Configuration):\n#     \"\"\"Home Assistant discovery commands.\"\"\"\n#     ctx.auto_output(\"data\")\n\n\n# DNS endpoints\n#########################################################################\n@cli.group(\"dns\")\n@pass_context\ndef dns(ctx: Configuration):\n    \"\"\"Home Assistant DNS commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@dns.command(\"info\")\n@pass_context\ndef dns_info(ctx: Configuration):\n    \"\"\"Home Assistant DNS info.\"\"\"\n    _handle(ctx, \"dns/info\")\n\n\n@dns.command(\"options\")\n@pass_context\ndef dns_options(ctx: Configuration):\n    \"\"\"Home Assistant DNS options.\"\"\"\n    _handle(ctx, \"dns/options\", \"post\")\n\n\n@dns.command(\"restart\")\n@pass_context\ndef dns_restart(ctx: Configuration):\n    \"\"\"Home Assistant DNS restart.\"\"\"\n    try:\n        _handle(ctx, \"dns/restart\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@dns.command(\"logs\")\n@pass_context\ndef dns_logs(ctx: Configuration):\n    \"\"\"Home Assistant DNS logs.\"\"\"\n    _handle(ctx, \"dns/logs\")\n\n\n@dns.command(\"stats\")\n@pass_context\ndef dns_stats(ctx: Configuration):\n    \"\"\"Home Assistant DNS stats.\"\"\"\n    _handle(ctx, \"dns/stats\")\n\n\n@dns.command(\"update\")\n@pass_context\ndef dns_update(ctx: Configuration):\n    \"\"\"Home Assistant DNS update.\"\"\"\n    try:\n        _handle(ctx, \"dns/update\", \"post\")\n    except (HomeAssistantCliError, HTTPError):\n        pass\n\n\n@dns.command(\"reset\")\n@pass_context\ndef dns_reset(ctx: Configuration):\n    \"\"\"Home Assistant DNS reset.\"\"\"\n    try:\n        _handle(ctx, \"dns/reset\", \"post\")\n    except (HomeAssistantCliError, HTTPError):\n        pass\n\n\n# Docker endpoints\n#########################################################################\n@cli.group(\"docker\")\n@pass_context\ndef docker(ctx: Configuration):\n    \"\"\"Home Assistant Docker commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@docker.command(\"info\")\n@pass_context\ndef docker_info(ctx: Configuration):\n    \"\"\"Home Assistant Docker info.\"\"\"\n    _handle(ctx, \"docker/info\")\n\n\n@docker.command(\"registries\")\n@pass_context\ndef docker_registries(ctx: Configuration):\n    \"\"\"Home Assistant Docker registries.\"\"\"\n    _handle(ctx, \"docker/registries\")\n\n\n# Hardware endpoints\n#########################################################################\n@cli.group(\"hardware\")\n@pass_context\ndef hardware(ctx: Configuration):\n    \"\"\"Home Assistant hardware info.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@hardware.command(\"info\")\n@pass_context\ndef hardware_info(ctx: Configuration):\n    \"\"\"Home Assistant hardware info.\"\"\"\n    _handle(ctx, \"hardware/info\")\n\n\n@hardware.command(\"audio\")\n@pass_context\ndef hardware_audio(ctx: Configuration):\n    \"\"\"Home Assistant hardware audio.\"\"\"\n    _handle(ctx, \"hardware/audio\")\n\n\n# Host endpoints\n#########################################################################\n@cli.group(\"host\")\n@pass_context\ndef host(ctx: Configuration):\n    \"\"\"Home Assistant host commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@host.command(\"reboot\")\n@pass_context\ndef host_reboot(ctx: Configuration):\n    \"\"\"Home Assistant host reboot.\"\"\"\n    _handle(ctx, \"host/reboot\", \"post\")\n\n\n@host.command(\"reload\")\n@pass_context\ndef host_reload(ctx: Configuration):\n    \"\"\"Home Assistant host reload.\"\"\"\n    _handle(ctx, \"host/reload\", \"post\")\n\n\n@host.command(\"shutdown\")\n@pass_context\ndef host_shutdown(ctx: Configuration):\n    \"\"\"Home Assistant host shutdown.\"\"\"\n    _handle(ctx, \"host/shutdown\", \"post\")\n\n\n@host.command(\"info\")\n@pass_context\ndef host_info(ctx: Configuration):\n    \"\"\"Home Assistant host info.\"\"\"\n    _handle(ctx, \"host/info\")\n\n\n@host.command(\"options\")\n@pass_context\ndef host_options(ctx: Configuration):\n    \"\"\"Home Assistant options shutdown.\"\"\"\n    _handle(ctx, \"host/options\", \"post\")\n\n\n@host.command(\"services\")\n@pass_context\ndef host_services(ctx: Configuration):\n    \"\"\"Home Assistant host reboot.\"\"\"\n    _handle(ctx, \"host/services\")\n\n\n# Ingress endpoints\n#########################################################################\n@cli.group(\"ingress\")\n@pass_context\ndef ingress(ctx: Configuration):\n    \"\"\"Home Assistant ingress info.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@ingress.command(\"info\")\n@pass_context\ndef ingress_info(ctx: Configuration):\n    \"\"\"Home Assistant ingress info.\"\"\"\n    _handle(ctx, \"ingress/panels\")\n\n\n# Jobs endpoints\n#########################################################################\n@cli.group(\"jobs\")\n@pass_context\ndef jobs(ctx: Configuration):\n    \"\"\"Home Assistant jobs info.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@jobs.command(\"info\")\n@pass_context\ndef jobs_info(ctx: Configuration):\n    \"\"\"Home Assistant jobs info.\"\"\"\n    _handle(ctx, \"jobs/info\")\n\n\n# Root endpoints\n#########################################################################\n@cli.group(\"root\")\n@pass_context\ndef root(ctx: Configuration):\n    \"\"\"Home Assistant root info.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@root.command(\"info\")\n@pass_context\ndef root_info(ctx: Configuration):\n    \"\"\"Home Assistant root info.\"\"\"\n    _handle(ctx, \"info\")\n\n\n@root.command(\"info\")\n@pass_context\ndef root_available_updates(ctx: Configuration):\n    \"\"\"Home Assistant root available updates.\"\"\"\n    _handle(ctx, \"available_updates\")\n\n\n# Mount endpoints\n#########################################################################\n@cli.group(\"mount\")\n@pass_context\ndef mount(ctx: Configuration):\n    \"\"\"Home Assistant mount info.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@mount.command(\"info\")\n@pass_context\ndef mount_info(ctx: Configuration):\n    \"\"\"Home Assistant mount info.\"\"\"\n    _handle(ctx, \"mounts\")\n\n\n# Multicast endpoints\n#########################################################################\n@cli.group(\"multicast\")\n@pass_context\ndef multicast(ctx: Configuration):\n    \"\"\"Home Assistant Multicast commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@multicast.command(\"info\")\n@pass_context\ndef multicast_info(ctx: Configuration):\n    \"\"\"Home Assistant Multicast info.\"\"\"\n    _handle(ctx, \"multicast/info\")\n\n\n@multicast.command(\"update\")\n@pass_context\ndef multicast_update(ctx: Configuration):\n    \"\"\"Home Assistant Multicast update.\"\"\"\n    response = _handle_raw(ctx, \"multicast/info\")\n    data = response[\"data\"]\n    current_version = int(data[\"version\"])\n    latest_version = int(data[\"version_latest\"])\n    if current_version == latest_version:\n        ctx.echo(\"Already running the latest release\")\n    else:\n        try:\n            _handle(ctx, \"multicast/update\", \"post\")\n        except (HomeAssistantCliError, HTTPError):\n            pass\n\n\n@multicast.command(\"restart\")\n@pass_context\ndef multicast_restart(ctx: Configuration):\n    \"\"\"Home Assistant Multicast restart.\"\"\"\n    try:\n        _handle(ctx, \"multicast/restart\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@multicast.command(\"logs\")\n@pass_context\ndef multicast_logs(ctx: Configuration):\n    \"\"\"Home Assistant DNS logs.\"\"\"\n    _handle(ctx, \"multicast/logs\")\n\n\n@multicast.command(\"stats\")\n@pass_context\ndef multicast_stats(ctx: Configuration):\n    \"\"\"Home Assistant Multicast stats.\"\"\"\n    _handle(ctx, \"multicast/stats\")\n\n\n# Network endpoints\n#########################################################################\n@cli.group(\"network\")\n@pass_context\ndef network(ctx: Configuration):\n    \"\"\"Home Assistant Network commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@network.command(\"info\")\n@pass_context\ndef network_info(ctx: Configuration):\n    \"\"\"Home Assistant network info.\"\"\"\n    _handle(ctx, \"network/info\")\n\n\n@network.command(\"reload\")\n@pass_context\ndef network_reload(ctx: Configuration):\n    \"\"\"Home Assistant Network reload.\"\"\"\n    try:\n        _handle(ctx, \"network/reload\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n# Observer endpoints\n#########################################################################\n@cli.group(\"observer\")\n@pass_context\ndef observer(ctx: Configuration):\n    \"\"\"Home Assistant Observer commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@observer.command(\"info\")\n@pass_context\ndef observer_info(ctx: Configuration):\n    \"\"\"Home Assistant observer info.\"\"\"\n    _handle(ctx, \"observer/info\")\n\n\n@observer.command(\"stats\")\n@pass_context\ndef observer_stats(ctx: Configuration):\n    \"\"\"Home Assistant observer stats.\"\"\"\n    _handle(ctx, \"observer/stats\")\n\n\n# OS endpoints\n#########################################################################\n@cli.group(\"os\")\n@pass_context\ndef os(ctx: Configuration):\n    \"\"\"Home Assistant Operating System commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@os.command(\"info\")\n@pass_context\ndef os_info(ctx: Configuration):\n    \"\"\"Home Assistant os info.\"\"\"\n    _handle(ctx, \"os/info\")\n\n\n@os.command(\"swap\")\n@pass_context\ndef os_swap(ctx: Configuration):\n    \"\"\"Home Assistant os swap.\"\"\"\n    _handle(ctx, \"os/config/swap\")\n\n\n@os.command(\"datadisk\")\n@pass_context\ndef os_datadisk(ctx: Configuration):\n    \"\"\"Home Assistant os datadisk.\"\"\"\n    _handle(ctx, \"os/datadisk/list\")\n\n\n@os.command(\"update\")\n@pass_context\ndef os_update(ctx: Configuration):\n    \"\"\"Home Assistant Operating System update.\"\"\"\n    response = _handle_raw(ctx, \"os/info\")\n    data = response[\"data\"]\n    current_version = data[\"version\"]\n    latest_version = data[\"version_latest\"]\n    if Version(current_version) == Version(latest_version):\n        ctx.echo(\"Already running the latest release\")\n    else:\n        try:\n            _handle(ctx, \"os/update\", \"post\")\n        except (HomeAssistantCliError, HTTPError):\n            pass\n\n\n# Resolution endpoints\n#########################################################################\n@cli.group(\"resolution\")\n@pass_context\ndef resolution(ctx: Configuration):\n    \"\"\"Home Assistant Resolution commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@resolution.command(\"info\")\n@pass_context\ndef resolution_info(ctx: Configuration):\n    \"\"\"Home Assistant resolution info.\"\"\"\n    _handle(ctx, \"resolution/info\")\n\n\n# Services endpoints\n#########################################################################\n@cli.group(\"service\")\n@pass_context\ndef service(ctx: Configuration):\n    \"\"\"Home Assistant Service commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@service.command(\"info\")\n@pass_context\ndef service_info(ctx: Configuration):\n    \"\"\"Home Assistant service info.\"\"\"\n    _handle(ctx, \"services\")\n\n\n@service.command(\"mqtt\")\n@pass_context\ndef service_mqtt(ctx: Configuration):\n    \"\"\"Home Assistant MQTT service info.\"\"\"\n    _handle(ctx, \"services/mqtt\")\n\n\n@service.command(\"mysql\")\n@pass_context\ndef service_mysql(ctx: Configuration):\n    \"\"\"Home Assistant MySQL service info.\"\"\"\n    _handle(ctx, \"services/mysql\")\n\n\n# Store endpoints\n#########################################################################\n@cli.group(\"store\")\n@pass_context\ndef store(ctx: Configuration):\n    \"\"\"Home Assistant Store commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@store.command(\"info\")\n@pass_context\ndef store_info(ctx: Configuration):\n    \"\"\"Home Assistant store info.\"\"\"\n    _handle(ctx, \"store/info\")\n\n\n@store.command(\"addon\")\n@pass_context\ndef store_addon(ctx: Configuration):\n    \"\"\"Home Assistant addon store info.\"\"\"\n    _handle(ctx, \"store/addons\")\n\n\n@store.command(\"repositories\")\n@pass_context\ndef store_repositories(ctx: Configuration):\n    \"\"\"Home Assistant store repositories info.\"\"\"\n    _handle(ctx, \"store/repositories\")\n\n\n@store.command(\"reload\")\n@pass_context\ndef store_reload(ctx: Configuration):\n    \"\"\"Home Assistant Store reload.\"\"\"\n    try:\n        _handle(ctx, \"store/reload\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n# Security endpoints\n#########################################################################\n@cli.group(\"security\")\n@pass_context\ndef security(ctx: Configuration):\n    \"\"\"Home Assistant Security commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@security.command(\"info\")\n@pass_context\ndef security_info(ctx: Configuration):\n    \"\"\"Home Assistant security info.\"\"\"\n    _handle(ctx, \"security/info\")\n\n\n# Supervisor endpoints\n#########################################################################\n@cli.group(\"supervisor\")\n@pass_context\ndef supervisor(ctx: Configuration):\n    \"\"\"Home Assistant supervisor commands.\"\"\"\n    ctx.auto_output(\"data\")\n\n\n@supervisor.command(\"ping\")\n@pass_context\ndef supervisor_ping(ctx: Configuration):\n    \"\"\"Home Assistant supervisor ping.\"\"\"\n    _handle(ctx, \"supervisor/ping\")\n\n\n@supervisor.command(\"info\")\n@pass_context\ndef supervisor_info(ctx: Configuration):\n    \"\"\"Home Assistant supervisor info.\"\"\"\n    _handle(ctx, \"supervisor/info\")\n\n\n@supervisor.command(\"update\")\n@pass_context\ndef supervisor_update(ctx: Configuration):\n    \"\"\"Home Assistant supervisor update.\"\"\"\n    response = _handle_raw(ctx, \"supervisor/info\")\n    data = response[\"data\"]\n    current_version = int(data[\"version\"])\n    latest_version = int(data[\"version_latest\"])\n    if current_version == latest_version:\n        ctx.echo(\"Already running the latest release\")\n    else:\n        try:\n            _handle(ctx, \"supervisor/update\", \"post\")\n        except (HomeAssistantCliError, HTTPError):\n            pass\n\n\n@supervisor.command(\"options\")\n@pass_context\ndef supervisor_options(ctx: Configuration):\n    \"\"\"Home Assistant supervisor options.\"\"\"\n    _handle(ctx, \"supervisor/options\", \"post\")\n\n\n@supervisor.command(\"reload\")\n@pass_context\ndef supervisor_reload(ctx: Configuration):\n    \"\"\"Home Assistant supervisor reload.\"\"\"\n    _handle(ctx, \"supervisor/reload\", \"post\")\n\n\n@supervisor.command(\"logs\")\n@pass_context\ndef supervisor_logs(ctx: Configuration):\n    \"\"\"Home Assistant supervisor logs.\"\"\"\n    _handle(ctx, \"supervisor/logs\")\n\n\n@supervisor.command(\"repair\")\n@pass_context\ndef supervisor_repair(ctx: Configuration):\n    \"\"\"Home Assistant supervisor repair.\"\"\"\n    _handle(ctx, \"supervisor/repair\", \"post\")\n\n\n@supervisor.command(\"restart\")\n@pass_context\ndef supervisor_restart(ctx: Configuration):\n    \"\"\"Home Assistant supervisor restart.\"\"\"\n    try:\n        _handle(ctx, \"supervisor/restart\", \"post\")\n    except HomeAssistantCliError:\n        pass\n\n\n@supervisor.command(\"stats\")\n@pass_context\ndef supervisor_stats(ctx: Configuration):\n    \"\"\"Home Assistant supervisor stats.\"\"\"\n    _handle(ctx, \"supervisor/stats\")\n"
  },
  {
    "path": "homeassistant_cli/plugins/info.py",
    "content": "\"\"\"Information plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nfrom typing import Any, Dict, List\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration, set_supervisor_server\nfrom homeassistant_cli.const import __version__ as cli_version\nfrom homeassistant_cli.helper import format_output, to_attributes\n\n_LOGGING = logging.getLogger(__name__)\n\n\ndef redacted_output(token: str) -> str:\n    \"\"\"Redact the token for display.\"\"\"\n    return (\n        token[:4] + \"*\" * (len(token) // 8 - 8) + token[-4:]\n        if token and len(token) > 8\n        else \"***\"\n    )\n\n\n@click.group(\"info\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Display information about Home Assistant CLI.\"\"\"\n\n\n@cli.command()\n@pass_context\ndef cli(ctx):\n    \"\"\"Show information about Home Assistant CLI.\"\"\"\n    information = {\n        \"Server URL\": ctx.resolved_server if ctx.resolved_server else ctx.server,\n        \"Password\": True if ctx.password else False,\n        \"Token\": redacted_output(ctx.token),\n        \"Supervisor URL\": set_supervisor_server(ctx),\n        \"Supervisor Token\": redacted_output(ctx.supervisor_token),\n        \"Verbose\": ctx.verbose,\n        \"Debug\": ctx.debug,\n        \"Insecure\": ctx.insecure,\n        \"Show Exceptions\": ctx.showexceptions,\n        \"Timeout\": ctx.timeout,\n        \"CLI version\": cli_version,\n    }\n\n    click.echo(format_output(ctx, information))\n"
  },
  {
    "path": "homeassistant_cli/plugins/integration.py",
    "content": "\"\"\"Integrations (config entries) plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re\nimport sys\n\nimport click\n\nimport homeassistant_cli.helper as helper\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"integration\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Get info and operate on integrations (config entries) from Home Assistant.\"\"\"\n\n\n@cli.command(\"list\")\n@click.argument(\"integration_filter\", default=\".*\", required=False)\n@pass_context\ndef list_integrations(ctx: Configuration, integration_filter: str):\n    \"\"\"List all integrations (config entries) from Home Assistant.\n\n    INTEGRATION_FILTER - optional regex to filter by domain or title\n    \"\"\"\n    ctx.auto_output(\"table\")\n\n    entries = api.get_config_entries(ctx)\n\n    result = []\n    if integration_filter == \".*\":\n        result = entries\n    else:\n        filter_regex = re.compile(integration_filter, re.IGNORECASE)\n\n        for entry in entries:\n            if filter_regex.search(entry.get(\"domain\", \"\")) or filter_regex.search(\n                entry.get(\"title\", \"\")\n            ):\n                result.append(entry)\n\n    cols = [\n        (\"ENTRY_ID\", \"entry_id\"),\n        (\"DOMAIN\", \"domain\"),\n        (\"TITLE\", \"title\"),\n        (\"STATE\", \"state\"),\n        (\"DISABLED_BY\", \"disabled_by\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"info\")\n@click.argument(\"entry_id\", required=True)\n@pass_context\ndef info(ctx: Configuration, entry_id: str):\n    \"\"\"Show detailed information about an integration.\n\n    ENTRY_ID - the entry_id of the config entry\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    entries = api.get_config_entries(ctx)\n\n    # Find by entry_id or partial match\n    entry = None\n    for e in entries:\n        if e.get(\"entry_id\") == entry_id or e.get(\"entry_id\", \"\").startswith(entry_id):\n            entry = e\n            break\n\n    if not entry:\n        _LOGGING.error(\"Could not find integration with entry_id: %s\", entry_id)\n        sys.exit(1)\n\n    ctx.echo(helper.format_output(ctx, entry))\n\n\n@cli.command(\"reload\")\n@click.argument(\"entry_id\", required=True)\n@pass_context\ndef reload(ctx: Configuration, entry_id: str):\n    \"\"\"Reload an integration.\n\n    ENTRY_ID - the entry_id of the config entry to reload\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    # Find full entry_id if partial match\n    entries = api.get_config_entries(ctx)\n    full_entry_id = None\n    for e in entries:\n        if e.get(\"entry_id\") == entry_id or e.get(\"entry_id\", \"\").startswith(entry_id):\n            full_entry_id = e.get(\"entry_id\")\n            break\n\n    if not full_entry_id:\n        _LOGGING.error(\"Could not find integration with entry_id: %s\", entry_id)\n        sys.exit(1)\n\n    result = api.reload_config_entry(ctx, full_entry_id)\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully reloaded integration: {full_entry_id}\")\n    else:\n        ctx.echo(helper.format_output(ctx, result))\n\n\n@cli.command(\"delete\")\n@click.argument(\"entry_id\", required=True)\n@click.option(\n    \"--confirm\",\n    is_flag=True,\n    default=False,\n    help=\"Confirm deletion without prompting\",\n)\n@pass_context\ndef delete(ctx: Configuration, entry_id: str, confirm: bool):\n    \"\"\"Delete an integration.\n\n    ENTRY_ID - the entry_id of the config entry to delete\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    # Find full entry_id if partial match\n    entries = api.get_config_entries(ctx)\n    entry = None\n    for e in entries:\n        if e.get(\"entry_id\") == entry_id or e.get(\"entry_id\", \"\").startswith(entry_id):\n            entry = e\n            break\n\n    if not entry:\n        _LOGGING.error(\"Could not find integration with entry_id: %s\", entry_id)\n        sys.exit(1)\n\n    full_entry_id = entry.get(\"entry_id\")\n    domain = entry.get(\"domain\", \"unknown\")\n    title = entry.get(\"title\", \"unknown\")\n\n    if not confirm:\n        click.confirm(\n            f\"Are you sure you want to delete '{domain}' ({title}) [{full_entry_id}]?\",\n            abort=True,\n        )\n\n    result = api.delete_config_entry(ctx, full_entry_id)\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully deleted integration: {domain} ({title})\")\n    else:\n        ctx.echo(helper.format_output(ctx, result))\n\n\n@cli.command(\"disable\")\n@click.argument(\"entry_id\", required=True)\n@pass_context\ndef disable(ctx: Configuration, entry_id: str):\n    \"\"\"Disable an integration.\n\n    ENTRY_ID - the entry_id of the config entry to disable\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    # Find full entry_id if partial match\n    entries = api.get_config_entries(ctx)\n    entry = None\n    for e in entries:\n        if e.get(\"entry_id\") == entry_id or e.get(\"entry_id\", \"\").startswith(entry_id):\n            entry = e\n            break\n\n    if not entry:\n        _LOGGING.error(\"Could not find integration with entry_id: %s\", entry_id)\n        sys.exit(1)\n\n    full_entry_id = entry.get(\"entry_id\")\n    result = api.disable_config_entry(ctx, full_entry_id, \"user\")\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully disabled integration: {full_entry_id}\")\n    else:\n        ctx.echo(helper.format_output(ctx, result))\n\n\n@cli.command(\"enable\")\n@click.argument(\"entry_id\", required=True)\n@pass_context\ndef enable(ctx: Configuration, entry_id: str):\n    \"\"\"Enable a disabled integration.\n\n    ENTRY_ID - the entry_id of the config entry to enable\n    \"\"\"\n    ctx.auto_output(\"data\")\n\n    # Find full entry_id if partial match\n    entries = api.get_config_entries(ctx)\n    entry = None\n    for e in entries:\n        if e.get(\"entry_id\") == entry_id or e.get(\"entry_id\", \"\").startswith(entry_id):\n            entry = e\n            break\n\n    if not entry:\n        _LOGGING.error(\"Could not find integration with entry_id: %s\", entry_id)\n        sys.exit(1)\n\n    full_entry_id = entry.get(\"entry_id\")\n    result = api.disable_config_entry(ctx, full_entry_id, None)\n\n    if result.get(\"success\"):\n        ctx.echo(f\"Successfully enabled integration: {full_entry_id}\")\n    else:\n        ctx.echo(helper.format_output(ctx, result))\n\n\n@cli.command(\"list-disabled\")\n@pass_context\ndef list_disabled(ctx: Configuration):\n    \"\"\"List all disabled integrations (config entries) from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n\n    entries = api.get_config_entries(ctx)\n\n    result = [entry for entry in entries if entry.get(\"disabled_by\")]\n\n    cols = [\n        (\"ENTRY_ID\", \"entry_id\"),\n        (\"DOMAIN\", \"domain\"),\n        (\"TITLE\", \"title\"),\n        (\"DISABLED_BY\", \"disabled_by\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n@cli.command(\"list-loaded\")\n@pass_context\ndef list_loaded(ctx: Configuration):\n    \"\"\"List all loaded integrations (config entries) from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n\n    entries = api.get_config_entries(ctx)\n\n    result = [entry for entry in entries if entry.get(\"state\") == \"loaded\"]\n\n    cols = [\n        (\"ENTRY_ID\", \"entry_id\"),\n        (\"DOMAIN\", \"domain\"),\n        (\"TITLE\", \"title\"),\n        (\"STATE\", \"state\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"list-unloaded\")\n@pass_context\ndef list_unloaded(ctx: Configuration):\n    \"\"\"List all unloaded integrations (config entries) from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n\n    entries = api.get_config_entries(ctx)\n\n    result = [entry for entry in entries if entry.get(\"state\") != \"loaded\"]\n\n    cols = [\n        (\"ENTRY_ID\", \"entry_id\"),\n        (\"DOMAIN\", \"domain\"),\n        (\"TITLE\", \"title\"),\n        (\"STATE\", \"state\"),\n    ]\n\n    ctx.echo(\n        helper.format_output(ctx, result, columns=ctx.columns if ctx.columns else cols)\n    )\n"
  },
  {
    "path": "homeassistant_cli/plugins/map.py",
    "content": "\"\"\"Map plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport sys\nimport webbrowser\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\nOSM_URL = \"https://www.openstreetmap.org/\"\nGOOGLE_URL = \"https://www.google.com/maps/search/\"\nBING_URL = \"https://www.bing.com/maps\"\nSERVICE = {\n    \"openstreetmap\": OSM_URL + \"?mlat={0}&mlon={1}#map=17/{0}/{1}\",\n    \"google\": GOOGLE_URL + \"?api=1&query={0},{1}\",\n    \"bing\": BING_URL + \"?v=2&cp={0}~{1}&lvl=17&sp=point.{0}_{1}_{2}\",\n}\n\n\n@click.command(\"map\")\n@click.argument(\n    \"entity\",\n    required=False,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@click.option(\"--service\", default=\"openstreetmap\", type=click.Choice(SERVICE.keys()))\n@pass_context\ndef cli(ctx: Configuration, service: str, entity: str) -> None:\n    \"\"\"Show the location of the config or an entity on a map.\"\"\"\n    latitude = None\n    longitude = None\n\n    if entity:\n        thing = entity\n        data = api.get_state(ctx, entity)\n        if data:\n            attr = data.get(\"attributes\", {})\n            latitude = attr.get(\"latitude\")\n            longitude = attr.get(\"longitude\")\n            thing = attr.get(\"friendly_name\", entity)\n    else:\n        thing = \"configuration\"\n        response = api.get_config(ctx)\n        if response:\n            latitude = response.get(\"latitude\")\n            longitude = response.get(\"longitude\")\n            thing = response.get(\"location_name\", thing)\n\n    if latitude and longitude:\n        urlpattern = SERVICE.get(service)\n        import urllib.parse\n\n        if urlpattern:\n            url = urlpattern.format(latitude, longitude, urllib.parse.quote_plus(thing))\n            ctx.echo(f\"{thing} location is at {latitude}, {longitude}\")\n            webbrowser.open_new_tab(url)\n        else:\n            ctx.echo(f\"Could not find URL pattern for service {service}\")\n    else:\n        ctx.echo(f\"No exact location info found in {thing}\")\n        sys.exit(2)\n"
  },
  {
    "path": "homeassistant_cli/plugins/raw.py",
    "content": "\"\"\"Raw plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nfrom typing import Any, Dict, List, cast\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import format_output\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"raw\")\n@pass_context\ndef cli(ctx: Configuration):\n    \"\"\"Call the raw API (advanced).\"\"\"\n    ctx.auto_output(\"data\")\n\n\ndef _report(ctx, cmd, method, response) -> None:\n    \"\"\"Create a report.\"\"\"\n    response.raise_for_status()\n\n    if response.ok:\n        try:\n            ctx.echo(format_output(ctx, response.json()))\n        except json_.decoder.JSONDecodeError:\n            _LOGGING.debug(\"Response could not be parsed as JSON\")\n            ctx.echo(response.text)\n    else:\n        _LOGGING.warning(\n            \"%s: <No output returned from %s %s>\",\n            response.status_code,\n            cmd,\n            method,\n        )\n\n\n@cli.command()\n@click.argument(\n    \"method\",\n    shell_complete=autocompletion.api_methods,  # type: ignore\n)\n@pass_context\ndef get(ctx: Configuration, method):\n    \"\"\"Do a GET request against api/<method>.\"\"\"\n    response = api.restapi(ctx, \"get\", method)\n\n    _report(ctx, \"GET\", method, response)\n\n\n@cli.command()\n@click.argument(\n    \"method\",\n    shell_complete=autocompletion.api_methods,  # type: ignore\n)\n@click.option(\"--json\")\n@pass_context\ndef post(ctx: Configuration, method, json):\n    \"\"\"Do a POST request against api/<method>.\"\"\"\n    if json:\n        data = json_.loads(\n            json if json != \"-\" else click.get_text_stream(\"stdin\").read()\n        )\n    else:\n        data = {}\n\n    response = api.restapi(ctx, \"post\", method, data)\n\n    _report(ctx, \"GET\", method, response)\n\n\n@cli.command(\"ws\")\n@click.argument(\n    \"wstype\",\n    shell_complete=autocompletion.wsapi_methods,  # type: ignore\n)\n@click.option(\"--json\")\n@pass_context\ndef websocket(ctx: Configuration, wstype, json):\n    r\"\"\"Send a websocket request against /api/websocket.\n\n    WSTYPE is name of websocket methods.\n\n    \\b\n    --json is dictionary to pass in addition to the type.\n           Example: --json='{ \"area_id\":\"2c8bf93c8082492f99c989896962f207\" }'\n    \"\"\"\n    if json:\n        data = json_.loads(\n            json if json != \"-\" else click.get_text_stream(\"stdin\").read()\n        )\n    else:\n        data = {}\n\n    frame = {\"type\": wstype}\n    frame = {**frame, **data}  # merging data into frame\n\n    response = cast(list[dict[str, Any]], api.wsapi(ctx, frame))\n\n    ctx.echo(format_output(ctx, response))\n"
  },
  {
    "path": "homeassistant_cli/plugins/service.py",
    "content": "\"\"\"Service plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport re as reg\nimport sys\nfrom typing import Any, Dict, List\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import format_output, to_attributes\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"service\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Call and work with services.\"\"\"\n\n\n@cli.command(\"list\")\n@click.argument(\"servicefilter\", default=\".*\", required=False)\n@pass_context\ndef list_cmd(ctx: Configuration, servicefilter):\n    \"\"\"Get list of services.\"\"\"\n    ctx.auto_output(\"table\")\n    services = api.get_services(ctx)\n    service_filter = servicefilter\n\n    result = []  # type: List[Dict[Any,Any]]\n    if service_filter == \".*\":\n        result = services\n    else:\n        result = services\n        service_filter_re = reg.compile(service_filter)  # type: Pattern\n\n        domains = []\n        for domain in services:\n            domain_name = domain[\"domain\"]\n            domain_data = {}\n            services_dict = domain[\"services\"]\n            service_data = {}\n            for service in services_dict:\n                if service_filter_re.search(f\"{domain_name}.{service}\"):\n                    service_data[service] = services_dict[service]\n\n            if service_data:\n                domain_data[\"services\"] = service_data\n                domain_data[\"domain\"] = domain_name\n                domains.append(domain_data)\n        result = domains\n\n    flatten_result = []  # type: List[Dict[str,Any]]\n    for domain in result:\n        for service in domain[\"services\"]:\n            item = {}\n            item[\"domain\"] = domain[\"domain\"]\n            item[\"service\"] = service\n            item = {**item, **domain[\"services\"][service]}\n            flatten_result.append(item)\n\n    cols = [\n        (\"DOMAIN\", \"domain\"),\n        (\"SERVICE\", \"service\"),\n        (\"DESCRIPTION\", \"description\"),\n    ]\n    ctx.echo(\n        format_output(ctx, flatten_result, columns=ctx.columns if ctx.columns else cols)\n    )\n\n\n@cli.command(\"call\")\n@click.argument(\n    \"service\",\n    required=True,\n    shell_complete=autocompletion.services,  # type: ignore\n)\n@click.option(\n    \"--arguments\", help=\"Comma separated key/value pairs to use as arguments.\"\n)\n@pass_context\ndef call(ctx: Configuration, service, arguments):\n    \"\"\"Call a service.\"\"\"\n    ctx.auto_output(\"data\")\n    _LOGGING.debug(\"service call <start>\")\n    parts = service.split(\".\")\n    if len(parts) != 2:\n        _LOGGING.error(\"Service name not following <domain>.<service> format\")\n        sys.exit(1)\n\n    _LOGGING.debug(\"Convert arguments %s to dict\", arguments)\n    data = to_attributes(arguments)\n\n    _LOGGING.debug(\"service call_service\")\n\n    result = api.call_service(ctx, parts[0], parts[1], data)\n\n    _LOGGING.debug(\"Formatting output\")\n    ctx.echo(format_output(ctx, result))\n"
  },
  {
    "path": "homeassistant_cli/plugins/state.py",
    "content": "\"\"\"Entity plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json as json_\nimport logging\nimport re\nfrom typing import Any, Dict, List\n\nimport click\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.const as const\nimport homeassistant_cli.helper as helper\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"state\")\n@pass_context\ndef cli(ctx):\n    \"\"\"Get info on entity state from Home Assistant.\"\"\"\n\n\n@cli.command()\n@click.argument(\n    \"entity\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef get(ctx: Configuration, entity):\n    \"\"\"Get/read entity state from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n    state = api.get_state(ctx, entity)\n\n    if state:\n        ctx.echo(\n            helper.format_output(\n                ctx,\n                [state],\n                columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,\n            )\n        )\n    else:\n        _LOGGING.error(\"Entity with ID: '%s' not found.\", entity)\n\n\n@cli.command()\n@click.argument(\n    \"entity\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef delete(ctx: Configuration, entity):\n    \"\"\"Delete entity state from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n    deleted = api.remove_state(ctx, entity)\n\n    if deleted:\n        ctx.echo(\"State for entity %s deleted.\", entity)\n    else:\n        ctx.echo(\"Entity %s not found.\", entity)\n\n\n@cli.command(\"list\")\n@click.argument(\"entityfilter\", default=\".*\", required=False)\n@pass_context\ndef list_command(ctx, entityfilter):\n    \"\"\"List all state from Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n    states = api.get_states(ctx)\n    entity_filter = entityfilter\n\n    result = []  # type: List[Dict]\n    if entity_filter == \".*\":\n        result = states\n    else:\n        entity_filter_re = re.compile(entity_filter)  # type: Pattern\n\n        for entity in states:\n            if entity_filter_re.search(entity[\"entity_id\"]):\n                result.append(entity)\n    ctx.echo(\n        helper.format_output(\n            ctx,\n            result,\n            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,\n        )\n    )\n\n\n@cli.command()\n@click.argument(\n    \"entity\",\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@click.argument(\"newstate\", required=False)\n@click.option(\n    \"--attributes\",\n    help=\"Comma separated key/value pairs to use as attributes.\",\n)\n@click.option(\n    \"--json\",\n    help=\"Raw JSON state to use for setting. Overrides any otherstate values provided.\",\n)\n@click.option(\n    \"--merge\",\n    is_flag=True,\n    default=False,\n    help=\"If set and the entity state exists the state and attributes will\"\n    \"be merged into the state rather than overwrite.\",\n    show_default=True,\n)\n@pass_context\ndef edit(ctx: Configuration, entity, newstate, attributes, merge, json):\n    \"\"\"Edit entity state from Home Assistant.\"\"\"\n    ctx.auto_output(\"data\")\n    new_state = newstate\n    if json:\n        _LOGGING.debug(\"JSON found overriding/creating new state for entity %s\", entity)\n        wanted_state = json_.loads(json)\n    elif new_state or attributes:\n        wanted_state = {}\n        existing_state = api.get_state(ctx, entity)\n\n        if existing_state:\n            ctx.echo(\"Existing state found for %s\", entity)\n            if merge:\n                wanted_state = existing_state\n        else:\n            ctx.echo(\"No existing state found for '%s'\", entity)\n\n        if attributes:\n            attributes_dict = helper.to_attributes(attributes)\n\n            new_attr = wanted_state.get(\"attributes\", {})\n            new_attr.update(attributes_dict)\n            # This is not honoring merge!\n            wanted_state[\"attributes\"] = new_attr\n\n        if newstate:\n            wanted_state[\"state\"] = newstate\n        else:\n            if not existing_state:\n                raise ValueError(\"No new or existing state provided.\")\n            wanted_state[\"state\"] = existing_state[\"state\"]\n\n    else:\n        existing = api.get_state(ctx, entity)\n        if existing:\n            existing_raw = helper.raw_format_output(ctx.output, existing, ctx.yaml())\n        else:\n            existing_raw = helper.raw_format_output(ctx.output, {}, ctx.yaml())\n\n        new = click.edit(existing_raw, extension=f\".{ctx.output}\")\n\n        if new is not None:\n            ctx.echo(\"Updating '%s'\", entity)\n            if ctx.output == \"yaml\":\n                wanted_state = ctx.yamlload(new)\n            if ctx.output == \"json\":\n                wanted_state = json_.loads(new)\n\n            api.set_state(ctx, entity, wanted_state)\n        else:\n            ctx.echo(\"No edits/changes returned from editor.\")\n            return\n\n    _LOGGING.debug(\"wanted: %s\", str(wanted_state))\n    result = api.set_state(ctx, entity, wanted_state)\n    ctx.echo(\"Entity %s updated successfully\", entity)\n    _LOGGING.debug(\"Updated to: %s\", result)\n\n\ndef _report(ctx: Configuration, result: list[dict[str, Any]], action: str):\n    \"\"\"Create a report.\"\"\"\n    ctx.echo(\n        helper.format_output(\n            ctx,\n            result,\n            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,\n        )\n    )\n    if ctx.verbose:\n        ctx.echo(\"%s entities reported to be %s\", len(result), action)\n\n\ndef _homeassistant_cmd(ctx: Configuration, entities, cmd, action):\n    \"\"\"Run command on Home Assistant.\"\"\"\n    data = {\"entity_id\": entities}\n    _LOGGING.debug(\"%s on %s\", cmd, entities)\n    result = api.call_service(ctx, \"homeassistant\", cmd, data)\n\n    _report(ctx, result, action)\n\n\n@cli.command()\n@click.argument(\n    \"entities\",\n    nargs=-1,\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef toggle(ctx: Configuration, entities):\n    \"\"\"Toggle state for one or more entities in Home Assistant.\"\"\"\n    ctx.auto_output(\"table\")\n    _homeassistant_cmd(ctx, entities, \"toggle\", \"toggled\")\n\n\n@cli.command(\"turn_off\")\n@click.argument(\n    \"entities\",\n    nargs=-1,\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef off_cmd(ctx: Configuration, entities):\n    \"\"\"Turn entity off.\"\"\"\n    ctx.auto_output(\"table\")\n    _homeassistant_cmd(ctx, entities, \"turn_off\", \"turned off\")\n\n\n@cli.command(\"turn_on\")\n@click.argument(\n    \"entities\",\n    nargs=-1,\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@pass_context\ndef on_cmd(ctx: Configuration, entities):\n    \"\"\"Turn entity on.\"\"\"\n    ctx.auto_output(\"table\")\n    _homeassistant_cmd(ctx, entities, \"turn_on\", \"turned on\")\n\n\n@cli.command()\n@click.argument(\n    \"entities\",\n    nargs=-1,\n    required=True,\n    shell_complete=autocompletion.entities,  # type: ignore\n)\n@click.option(\n    \"--since\",\n    required=False,\n    default=\"1d\",\n    help=\"Start of the period to get history from. A timestamp or relative \"\n    \"expression relative to now. Defaults to 1 day.\",\n)\n@click.option(\n    \"--end\",\n    required=False,\n    default=\"now\",\n    help=\"End of the period to query history from. A timestamp or relative \"\n    \"expression relative to now. Defaults to now.\",\n)\n@pass_context\ndef history(ctx: Configuration, entities: list, since: str, end: str):\n    \"\"\"Get state history from Home Assistant, all or per entity.\n\n    You can use `--since` and `--end` to narrow or expand the time period.\n\n    Both options accepts a full timestamp i.e. `2016-02-06T22:15:00+00:00`\n    or a relative expression i.e. `3m` for three minutes, `5d` for 5 days.\n    Even `3 minutes` or `5 days` will work.\n    See https://dateparser.readthedocs.io/en/latest/#features for examples.\n    \"\"\"\n    import dateparser\n\n    ctx.auto_output(\"table\")\n    settings = {\n        \"DATE_ORDER\": \"DMY\",\n        \"TIMEZONE\": \"UTC\",\n        \"RETURN_AS_TIMEZONE_AWARE\": True,\n    }\n\n    start_time = dateparser.parse(since, settings=settings)\n\n    end_time = dateparser.parse(end, settings=settings)\n\n    delta = end_time - start_time\n\n    if ctx.verbose:\n        click.echo(\n            f\"Querying from {since}:{start_time.isoformat()} to \"\n            f\"{end}:{end_time.isoformat()} a span of {delta}\"\n        )\n\n    data = api.get_history(ctx, list(entities), start_time, end_time)\n\n    result = []  # type: List[Dict[str, Any]]\n    entity_count = 0\n    for item in data:\n        result.extend(item)  # type: ignore\n        entity_count = entity_count + 1\n\n    click.echo(\n        helper.format_output(\n            ctx,\n            result,\n            columns=ctx.columns if ctx.columns else const.COLUMNS_ENTITIES,\n        )\n    )\n\n    if ctx.verbose:\n        click.echo(\n            f\"History with {len(result)} rows from {entity_count} entities found.\"\n        )\n"
  },
  {
    "path": "homeassistant_cli/plugins/system.py",
    "content": "\"\"\"System plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\n\nimport click\n\nimport homeassistant_cli.const as const\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.helper import format_output\n\n_LOGGING = logging.getLogger(__name__)\n\n\n@click.group(\"system\")\n@pass_context\ndef cli(ctx):\n    \"\"\"System details and operations for Home Assistant.\"\"\"\n\n\n@cli.command()\n@pass_context\ndef log(ctx):\n    \"\"\"Get errors from Home Assistant.\"\"\"\n    click.echo(api.get_raw_error_log(ctx))\n\n\n@cli.command()\n@pass_context\ndef health(ctx: Configuration):\n    \"\"\"Get system health from Home Assistant.\"\"\"\n    info = api.get_health(ctx)\n\n    ctx.echo(\n        format_output(\n            ctx,\n            [info],\n            columns=ctx.columns if ctx.columns else const.COLUMNS_DEFAULT,\n        )\n    )\n"
  },
  {
    "path": "homeassistant_cli/plugins/template.py",
    "content": "\"\"\"Template plugin for Home Assistant CLI (hass-cli).\"\"\"\n\nimport logging\nimport os\n\nimport click\nfrom jinja2 import FileSystemLoader\nfrom jinja2.exceptions import SecurityError\nfrom jinja2.sandbox import ImmutableSandboxedEnvironment\n\nimport homeassistant_cli.remote as api\nfrom homeassistant_cli.cli import pass_context\nfrom homeassistant_cli.config import Configuration\nfrom homeassistant_cli.exceptions import HomeAssistantCliError, UnsafeTemplateError\n\n_LOGGING = logging.getLogger(__name__)\n\n# Allowlist of environment variables accessible from templates\nSAFE_ENV_VARS = frozenset({\n    \"HASS_SERVER\",\n    \"LANG\",\n    \"TZ\",\n})\n\n\ndef _safe_environ_get(key: str, default: str | None = None) -> str | None:\n    \"\"\"Return env var only if it is in the allowlist.\"\"\"\n    if key in SAFE_ENV_VARS:\n        return os.environ.get(key, default)\n    return default\n\n\ndef render(template_path, data, strict=False) -> str:\n    \"\"\"Render template.\"\"\"\n    env = ImmutableSandboxedEnvironment(\n        loader=FileSystemLoader(os.path.dirname(template_path)),\n        keep_trailing_newline=True,\n    )\n    if strict:\n        from jinja2 import StrictUndefined\n\n        env.undefined = StrictUndefined\n\n    # Add environ global (allowlisted)\n    env.globals[\"environ\"] = _safe_environ_get\n\n    try:\n        output = env.get_template(os.path.basename(template_path)).render(data)\n    except SecurityError as err:\n        raise UnsafeTemplateError(\n            f\"Template '{os.path.basename(template_path)}' contains unsafe \"\n            f\"operations: {err}\"\n        ) from None\n    return output\n\n\n@click.command(\"template\")\n@click.argument(\"template\", required=True, type=click.File())\n@click.argument(\"datafile\", type=click.File(), required=False)\n@click.option(\n    \"--local\",\n    default=False,\n    is_flag=True,\n    help=\"If should render template locally.\",\n)\n@pass_context\ndef cli(ctx: Configuration, template, datafile, local: bool) -> None:\n    \"\"\"Render templates on server or locally.\n\n    TEMPLATE - jinja2 template file\n    DATAFILE - YAML file with variables to pass to rendering\n    \"\"\"\n    variables = {}  # type: Dict[str, Any]\n    if datafile:\n        variables = ctx.yamlload(datafile)\n\n    template_string = template.read()\n\n    _LOGGING.debug(\"Rendering: %s Variables: %s\", template_string, variables)\n\n    try:\n        if local:\n            output = render(template.name, variables, True)\n        else:\n            output = api.render_template(ctx, template_string, variables)\n    except (UnsafeTemplateError, HomeAssistantCliError) as err:\n        raise click.ClickException(str(err)) from None\n\n    ctx.echo(output)\n"
  },
  {
    "path": "homeassistant_cli/remote.py",
    "content": "\"\"\"\nBasic API to access remote instance of Home Assistant.\n\nIf a connection error occurs while communicating with the API a\nHomeAssistantCliError will be raised.\n\"\"\"\n\nimport asyncio\nimport collections\nimport enum\nimport json\nimport logging\nimport urllib.parse\nfrom collections.abc import Callable\nfrom datetime import datetime\nfrom typing import Any, cast\nfrom urllib.parse import urlencode\n\nimport aiohttp\nimport requests\n\nimport homeassistant_cli.hassconst as hass\nfrom homeassistant_cli.config import (\n    Configuration,\n    resolve_server,\n    set_supervisor_server,\n)\nfrom homeassistant_cli.exceptions import HomeAssistantCliError\n\n_LOGGER = logging.getLogger(__name__)\n\n# Copied from aiohttp.hdrs\nCONTENT_TYPE = \"Content-Type\"\nMETH_DELETE = \"DELETE\"\nMETH_GET = \"GET\"\nMETH_POST = \"POST\"\n\n\nclass APIStatus(enum.Enum):\n    \"\"\"Representation of an API status.\"\"\"\n\n    OK = \"ok\"\n    INVALID_PASSWORD = \"invalid_password\"\n    CANNOT_CONNECT = \"cannot_connect\"\n    UNKNOWN = \"unknown\"\n\n    def __str__(self) -> str:\n        \"\"\"Return the state.\"\"\"\n        return self.value  # type: ignore\n\n\ndef restapi(\n    ctx: Configuration, method: str, path: str, data: dict | None = None\n) -> requests.Response:\n    \"\"\"Make a call to the Home Assistant REST API.\"\"\"\n    if data is None:\n        data_str = None\n    else:\n        data_str = json.dumps(data, cls=JSONEncoder)\n\n    if not ctx.session:\n        ctx.session = requests.Session()\n        ctx.session.verify = not ctx.insecure\n        if ctx.cert:\n            ctx.session.cert = ctx.cert\n\n        _LOGGER.debug(\n            \"Session: verify(%s), cert(%s)\",\n            ctx.session.verify,\n            ctx.session.cert,\n        )\n\n    headers = {CONTENT_TYPE: hass.CONTENT_TYPE_JSON}  # type: Dict[str, Any]\n\n    if ctx.token:\n        headers[\"Authorization\"] = f\"Bearer {ctx.token}\"\n    if ctx.password:\n        headers[\"x-ha-access\"] = ctx.password\n\n    url = urllib.parse.urljoin(resolve_server(ctx) + path, \"\")\n\n    try:\n        if method == METH_GET:\n            return requests.get(url, params=data_str, headers=headers)\n\n        return requests.request(method, url, data=data_str, headers=headers)\n\n    except requests.exceptions.ConnectionError:\n        raise HomeAssistantCliError(f\"Error connecting to {url}\") from None\n\n    except requests.exceptions.Timeout:\n        error = f\"Timeout when talking to {url}\"\n        _LOGGER.exception(error)\n        raise HomeAssistantCliError(error) from None\n\n\ndef restapi_supervisor(\n    ctx: Configuration, method: str, path: str, data: dict | None = None\n) -> requests.Response:\n    \"\"\"Make a call to the Supervisor REST API.\"\"\"\n    if data is None:\n        data_str = None\n    else:\n        data_str = json.dumps(data, cls=JSONEncoder)\n\n    if not ctx.session:\n        ctx.session = requests.Session()\n        ctx.session.verify = not ctx.insecure\n        if ctx.cert:\n            ctx.session.cert = ctx.cert\n\n        _LOGGER.debug(\n            \"Session: verify(%s), cert(%s)\",\n            ctx.session.verify,\n            ctx.session.cert,\n        )\n\n    headers = {CONTENT_TYPE: hass.CONTENT_TYPE_JSON}  # type: Dict[str, Any]\n\n    if ctx.token:\n        headers[\"Authorization\"] = f\"Bearer {ctx.supervisor_token}\"\n\n    url = urllib.parse.urljoin(set_supervisor_server(ctx) + path, \"\")\n\n    try:\n        if method == METH_GET:\n            return requests.get(url, params=data_str, headers=headers)\n\n        return requests.request(method, url, data=data_str, headers=headers)\n\n    except requests.exceptions.ConnectionError:\n        raise HomeAssistantCliError(f\"Error connecting to {url}\") from None\n\n    except requests.exceptions.Timeout:\n        error = f\"Timeout when talking to {url}\"\n        _LOGGER.exception(error)\n        raise HomeAssistantCliError(error) from None\n\n\ndef wsapi(\n    ctx: Configuration,\n    frame: dict,\n    callback: Callable[[dict], Any] | None = None,\n) -> dict | None:\n    \"\"\"Make a call to Home Assistant using WS API.\n\n    if callback provided will keep listening and call\n    on every message.\n\n    If no callback return data returned.\n    \"\"\"\n    async def fetcher() -> dict | None:\n        \"\"\"Fetch data from WS API.\"\"\"\n        async with aiohttp.ClientSession() as session:\n            async with session.ws_connect(\n                resolve_server(ctx) + \"/api/websocket\",\n                max_msg_size=16 * 1024 * 1024,  # 16MB to handle large responses\n            ) as wsconn:\n                await wsconn.send_str(\n                    json.dumps({\"type\": \"auth\", \"access_token\": ctx.token})\n                )\n\n                frame[\"id\"] = 1\n\n                await wsconn.send_str(json.dumps(frame))\n\n                while True:\n                    msg = await wsconn.receive()\n                    if msg.type == aiohttp.WSMsgType.ERROR:\n                        break\n                    elif msg.type == aiohttp.WSMsgType.CLOSED:\n                        break\n                    elif msg.type == aiohttp.WSMsgType.TEXT:\n                        mydata = json.loads(msg.data)  # type: Dict\n\n                        if callback:\n                            callback(mydata)\n                        elif mydata[\"type\"] == \"result\":\n                            return mydata\n                        elif mydata[\"type\"] == \"auth_invalid\":\n                            raise HomeAssistantCliError(mydata.get(\"message\"))\n        return None\n\n    result = asyncio.run(fetcher())\n    return result\n\n\nclass JSONEncoder(json.JSONEncoder):\n    \"\"\"JSONEncoder that supports Home Assistant objects.\"\"\"\n\n    # pylint: disable=method-hidden\n    def default(self, o: Any) -> Any:\n        \"\"\"Convert Home Assistant objects.\n\n        Hand other objects to the original method.\n        \"\"\"\n        if isinstance(o, datetime):\n            return o.isoformat()\n        if isinstance(o, set):\n            return list(o)\n        if hasattr(o, \"as_dict\"):\n            return o.as_dict()\n\n        return json.JSONEncoder.default(self, o)\n\n\ndef get_areas(ctx: Configuration) -> list[dict[str, Any]]:\n    \"\"\"Return all areas.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_AREA_REGISTRY_LIST}\n\n    areas = cast(dict, wsapi(ctx, frame))[\"result\"]  # type: List[Dict[str, Any]]\n\n    return areas\n\n\ndef find_area(ctx: Configuration, id_or_name: str) -> dict[str, str] | None:\n    \"\"\"Find area first by id and if no match by name.\"\"\"\n    areas = get_areas(ctx)\n\n    area = next((x for x in areas if x[\"area_id\"] == id_or_name), None)\n    if not area:\n        area = next((x for x in areas if x[\"name\"] == id_or_name), None)\n\n    return area\n\n\ndef create_area(ctx: Configuration, name: str) -> dict[str, Any]:\n    \"\"\"Create area.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_AREA_REGISTRY_CREATE, \"name\": name}\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef delete_area(ctx: Configuration, area_id: str) -> dict[str, Any]:\n    \"\"\"Delete area.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_AREA_REGISTRY_DELETE, \"area_id\": area_id}\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef rename_area(ctx: Configuration, area_id: str, new_name: str) -> dict[str, Any]:\n    \"\"\"Rename area.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_AREA_REGISTRY_UPDATE,\n        \"area_id\": area_id,\n        \"name\": new_name,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef rename_entity(\n    ctx: Configuration,\n    entity_id: str,\n    new_id: str | None,\n    new_name: str | None,\n) -> dict[str, Any]:\n    \"\"\"Rename entity.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,\n        \"entity_id\": entity_id,\n    }\n\n    if new_name:\n        frame[\"name\"] = new_name\n    if new_id:\n        frame[\"new_entity_id\"] = new_id\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef rename_device(ctx: Configuration, device_id: str, new_name: str) -> dict[str, Any]:\n    \"\"\"Rename device.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_DEVICE_REGISTRY_UPDATE,\n        \"device_id\": device_id,\n        \"name_by_user\": new_name,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef assign_area(ctx: Configuration, device_id: str, area_id: str) -> dict[str, Any]:\n    \"\"\"Assign area.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_DEVICE_REGISTRY_UPDATE,\n        \"area_id\": area_id,\n        \"device_id\": device_id,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef assign_entity_area(\n    ctx: Configuration, entity_id: str, area_id: str\n) -> dict[str, Any]:\n    \"\"\"Assign area to entity.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,\n        \"area_id\": area_id,\n        \"entity_id\": entity_id,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef delete_entity(ctx: Configuration, entity_id: str) -> dict[str, Any]:\n    \"\"\"Delete entity from registry.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_ENTITY_REGISTRY_REMOVE,\n        \"entity_id\": entity_id,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef enable_entity(\n    ctx: Configuration, entity_id: str, disabled_by: str | None\n) -> dict[str, Any]:\n    \"\"\"Enable or disable an entity.\"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_ENTITY_REGISTRY_UPDATE,\n        \"entity_id\": entity_id,\n        \"disabled_by\": disabled_by,\n    }\n\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef get_health(ctx: Configuration) -> dict[str, Any]:\n    \"\"\"Get system Health.\"\"\"\n    frame = {\"type\": \"system_health/info\"}\n\n    info = cast(dict[str, dict[str, Any]], wsapi(ctx, frame))[\"result\"]\n\n    return info\n\n\ndef get_devices(ctx: Configuration) -> list[dict[str, Any]]:\n    \"\"\"Return all devices.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_DEVICE_REGISTRY_LIST}\n\n    devices = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))[\"result\"]\n\n    return devices\n\n\ndef get_entities(ctx: Configuration) -> list[dict[str, Any]]:\n    \"\"\"Return all entities.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_ENTITY_REGISTRY_LIST}\n\n    devices = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))[\"result\"]\n\n    return devices\n\n\ndef get_entity(ctx: Configuration, entity_id: str) -> list[dict[str, Any]]:\n    \"\"\"Return id.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_ENTITY_REGISTRY_GET, \"entity_id\": entity_id}\n\n    result = cast(dict[str, list[dict[str, Any]]], wsapi(ctx, frame))\n\n    return result[\"id\"]\n\n\ndef get_config_entries(ctx: Configuration) -> list[dict[str, Any]]:\n    \"\"\"Return all config entries (integrations).\"\"\"\n    req = restapi(ctx, METH_GET, \"/api/config/config_entries/entry\")\n    req.raise_for_status()\n    return cast(list[dict[str, Any]], req.json())\n\n\ndef get_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:\n    \"\"\"Return a specific config entry.\"\"\"\n    frame = {\"type\": hass.WS_TYPE_CONFIG_ENTRIES_GET_SINGLE, \"entry_id\": entry_id}\n    result = cast(dict[str, Any], wsapi(ctx, frame))\n    return result.get(\"result\", {}).get(\"config_entry\", {})\n\n\ndef reload_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:\n    \"\"\"Reload a config entry via REST API.\"\"\"\n    req = restapi(ctx, METH_POST, f\"/api/config/config_entries/entry/{entry_id}/reload\")\n    if req.status_code == 200:\n        return {\"success\": True, **req.json()}\n    elif req.status_code == 404:\n        return {\"success\": False, \"error\": \"Config entry not found\"}\n    elif req.status_code == 403:\n        return {\"success\": False, \"error\": \"Entry cannot be reloaded\"}\n    else:\n        req.raise_for_status()\n        return {\"success\": False}\n\n\ndef delete_config_entry(ctx: Configuration, entry_id: str) -> dict[str, Any]:\n    \"\"\"Delete a config entry via REST API.\"\"\"\n    req = restapi(ctx, METH_DELETE, f\"/api/config/config_entries/entry/{entry_id}\")\n    if req.status_code == 200:\n        return {\"success\": True, **req.json()}\n    elif req.status_code == 404:\n        return {\"success\": False, \"error\": \"Config entry not found\"}\n    else:\n        req.raise_for_status()\n        return {\"success\": False}\n\n\ndef disable_config_entry(\n    ctx: Configuration, entry_id: str, disabled_by: str | None\n) -> dict[str, Any]:\n    \"\"\"Enable or disable a config entry.\n\n    Set disabled_by to \"user\" to disable, or None to enable.\n    \"\"\"\n    frame = {\n        \"type\": hass.WS_TYPE_CONFIG_ENTRIES_DISABLE,\n        \"entry_id\": entry_id,\n        \"disabled_by\": disabled_by,\n    }\n    return cast(dict[str, Any], wsapi(ctx, frame))\n\n\ndef validate_api(ctx: Configuration) -> APIStatus:\n    \"\"\"Make a call to validate API.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API)\n\n        if req.status_code == 200:\n            return APIStatus.OK\n\n        if req.status_code == 401:\n            return APIStatus.INVALID_PASSWORD\n\n        return APIStatus.UNKNOWN\n\n    except HomeAssistantCliError:\n        return APIStatus.CANNOT_CONNECT\n\n\ndef get_info(ctx: Configuration) -> dict[str, Any]:\n    \"\"\"Get basic info about the Home Assistant instance.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_CONFIG)\n\n        req.raise_for_status()\n\n        return cast(dict[str, Any], req.json()) if req.status_code == 200 else {}\n\n    except (HomeAssistantCliError, ValueError) as exception:\n        raise HomeAssistantCliError(\n            \"Unexpected error retrieving information\"\n        ) from exception\n        # ValueError if req.json() can't parse the json\n\n\ndef get_events(ctx: Configuration) -> dict[str, Any]:\n    \"\"\"Return all events.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_EVENTS)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting events: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        return cast(dict[str, Any], req.json())\n\n    raise HomeAssistantCliError(f\"Error while getting all events: {req.text}\")\n\n\ndef get_history(\n    ctx: Configuration,\n    entities: list | None = None,\n    start_time: datetime | None = None,\n    end_time: datetime | None = None,\n) -> list[dict[str, Any]]:\n    \"\"\"Return History.\"\"\"\n    try:\n        if start_time:\n            method = hass.URL_API_HISTORY_PERIOD.format(start_time.isoformat())\n        else:\n            method = hass.URL_API_HISTORY\n\n        params = collections.OrderedDict()  # type: Dict[str, str]\n\n        if entities:\n            params[\"filter_entity_id\"] = \",\".join(entities)\n        if end_time:\n            params[\"end_time\"] = end_time.isoformat()\n\n        if params:\n            method = f\"{method}?{urlencode(params)}\"\n\n        req = restapi(ctx, METH_GET, method)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting history: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        return cast(list[dict[str, Any]], req.json())\n\n    raise HomeAssistantCliError(f\"Error while getting all events: {req.text}\")\n\n\ndef get_states(ctx: Configuration) -> list[dict[str, Any]]:\n    \"\"\"Return all states.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_STATES)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting state: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        data = req.json()  # type: List[Dict[str, Any]]\n        return data\n\n    raise HomeAssistantCliError(f\"Error while getting all states: {req.text}\")\n\n\ndef get_raw_error_log(ctx: Configuration) -> str:\n    \"\"\"Return the error log.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_ERROR_LOG)\n        req.raise_for_status()\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting error log: {exception}\"\n        ) from exception\n\n    return req.text\n\n\ndef get_config(ctx: Configuration) -> dict[str, Any]:\n    \"\"\"Return the running configuration.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_CONFIG)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting configuration: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        return cast(dict[str, str], req.json())\n\n    raise HomeAssistantCliError(f\"Error while getting all configuration: {req.text}\")\n\n\ndef get_state(ctx: Configuration, entity_id: str) -> dict[str, Any] | None:\n    \"\"\"Get entity state. If ok, return dictionary with state.\n\n    If no entity found return None - otherwise exception raised\n    with details.\n    \"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_STATES_ENTITY.format(entity_id))\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting state: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        return cast(dict[str, Any], req.json())\n    if req.status_code == 404:\n        return None\n\n    raise HomeAssistantCliError(f\"Error while getting Entity {entity_id}: {req.text}\")\n\n\ndef remove_state(ctx: Configuration, entity_id: str) -> bool:\n    \"\"\"Call API to remove state for entity_id.\n\n    If success return True, if could not find the entity return False.\n    Otherwise raise exception with details.\n    \"\"\"\n    try:\n        req = restapi(ctx, METH_DELETE, hass.URL_API_STATES_ENTITY.format(entity_id))\n\n        if req.status_code == 200:\n            return True\n        if req.status_code == 404:\n            return False\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\"Unexpected error removing state\") from exception\n\n    raise HomeAssistantCliError(f\"Error removing state: {req.status_code} - {req.text}\")\n\n\ndef set_state(ctx: Configuration, entity_id: str, data: dict) -> dict[str, Any]:\n    \"\"\"Set/update state for entity id.\"\"\"\n    try:\n        req = restapi(\n            ctx, METH_POST, hass.URL_API_STATES_ENTITY.format(entity_id), data\n        )\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Error updating state for entity {entity_id}: {exception}\"\n        ) from exception\n\n    if req.status_code not in (200, 201):\n        raise HomeAssistantCliError(\n            f\"Error changing state for entity {entity_id}: {req.status_code} - \"\n            f\"{req.text}\"\n        )\n    return cast(dict[str, Any], req.json())\n\n\ndef render_template(ctx: Configuration, template: str, variables: dict) -> str:\n    \"\"\"Render template.\"\"\"\n    data = {\"template\": template, \"variables\": variables}\n\n    try:\n        req = restapi(ctx, METH_POST, hass.URL_API_TEMPLATE, data)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Error applying template: {exception}\"\n        ) from exception\n\n    if req.status_code not in (200, 201):\n        raise HomeAssistantCliError(\n            f\"Error applying template: {req.status_code} - {req.text}\"\n        )\n    return req.text\n\n\ndef get_event_listeners(ctx: Configuration) -> dict:\n    \"\"\"List of events that is being listened for.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_EVENTS)\n\n        return req.json() if req.status_code == 200 else {}  # type: ignore\n\n    except (HomeAssistantCliError, ValueError):\n        # ValueError if req.json() can't parse the json\n        _LOGGER.exception(\"Unexpected result retrieving event listeners\")\n\n        return {}\n\n\ndef fire_event(\n    ctx: Configuration, event_type: str, data: dict[str, Any] | None = None\n) -> dict[str, Any] | None:\n    \"\"\"Fire an event at remote API.\"\"\"\n    try:\n        req = restapi(\n            ctx, METH_POST, hass.URL_API_EVENTS_EVENT.format(event_type), data\n        )\n\n        if req.status_code != 200:\n            _LOGGER.error(\"Error firing event: %d - %s\", req.status_code, req.text)\n\n        return cast(dict[str, Any], req.json())\n\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(f\"Error firing event: {exception}\") from exception\n\n\ndef call_service(\n    ctx: Configuration,\n    domain: str,\n    service: str,\n    service_data: dict | None = None,\n) -> list[dict[str, Any]]:\n    \"\"\"Call a service.\"\"\"\n    try:\n        req = restapi(\n            ctx,\n            METH_POST,\n            hass.URL_API_SERVICES_SERVICE.format(domain, service),\n            service_data,\n        )\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Error calling service: {exception}\"\n        ) from exception\n\n    if req.status_code != 200:\n        raise HomeAssistantCliError(\n            f\"Error calling service: {req.status_code} - {req.text}\"\n        )\n\n    return cast(list[dict[str, Any]], req.json())\n\n\ndef get_services(\n    ctx: Configuration,\n) -> list[dict[str, Any]]:\n    \"\"\"Get list of services.\"\"\"\n    try:\n        req = restapi(ctx, METH_GET, hass.URL_API_SERVICES)\n    except HomeAssistantCliError as exception:\n        raise HomeAssistantCliError(\n            f\"Unexpected error getting services: {exception}\"\n        ) from exception\n\n    if req.status_code == 200:\n        return cast(list[dict[str, Any]], req.json())\n\n    raise HomeAssistantCliError(f\"Error while getting all services: {req.text}\")\n\n\ndef get_network(ctx: Configuration) -> dict[str, Any]:\n    \"\"\"Get network information.\"\"\"\n    frame = {\"type\": \"config/network\"}\n\n    print(wsapi(ctx, frame))\n\n    network = cast(dict[str, dict[str, Any]], wsapi(ctx, frame))[\"result\"]\n\n    return network\n"
  },
  {
    "path": "homeassistant_cli/yaml.py",
    "content": "\"\"\"Yaml utility for hass-cli.\"\"\"\n\nfrom typing import Any, cast\n\nfrom ruamel.yaml import YAML\nfrom ruamel.yaml.compat import StringIO\n\n\ndef yaml() -> YAML:\n    \"\"\"Return default YAML parser.\"\"\"\n    yamlp = YAML(typ=\"safe\", pure=True)\n    yamlp.preserve_quotes = cast(None, True)\n    yamlp.default_flow_style = False\n    return yamlp\n\n\ndef loadyaml(yamlp: YAML, source: str) -> Any:\n    \"\"\"Load YAML.\"\"\"\n    return yamlp.load(source)\n\n\ndef dumpyaml(yamlp: YAML, data: Any, stream: Any = None, **kw: Any) -> str | None:\n    \"\"\"Dump YAML to string.\"\"\"\n    inefficient = False\n    if stream is None:\n        inefficient = True\n        stream = StringIO()\n    # overriding here to get dumping to\n    # not sort keys.\n    yamlp = YAML()\n    yamlp.indent(mapping=4, sequence=6, offset=3)\n    # yamlp.compact(seq_seq=False, seq_map=False)\n    yamlp.dump(data, stream, **kw)\n    if inefficient:\n        return cast(str, stream.getvalue())\n    return None\n"
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\ncheck_untyped_defs = true\ndisallow_untyped_calls = true\nfollow_imports = silent\nignore_missing_imports = true\nno_implicit_optional = true\nwarn_incomplete_stub = true\nwarn_redundant_casts = true\nwarn_return_any = true\nwarn_unused_configs = true\nwarn_unused_ignores = true\n\n[mypy-homeassistant_cli.*]\ndisallow_untyped_defs = true\n"
  },
  {
    "path": "mypyrc",
    "content": "homeassistant_cli\nhomeassistant_cli/plugins\ntests\n"
  },
  {
    "path": "pylintrc",
    "content": "[MESSAGES CONTROL]\n# Reasons disabled:\n# locally-disabled - it spams too much\n# duplicate-code - unavoidable\n# cyclic-import - doesn't test if both import on load\n# abstract-class-little-used - prevents from setting right foundation\n# unused-argument - generic callbacks and setup methods create a lot of warnings\n# global-statement - used for the on-demand requirement installation\n# redefined-variable-type - this is Python, we're duck typing!\n# too-many-* - are not enforced for the sake of readability\n# too-few-* - same as too-many-*\n# abstract-method - with intro of async there are always methods missing\n# inconsistent-return-statements - doesn't handle raise\n# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311\n# C0330 - https://github.com/PyCQA/pylint/issues/289\n# C0414 - https://github.com/PyCQA/pylint/issues/2423 can be re-added when upgrading to pylint 2.2\n# fixme - for now it is useful having these. When getting more stable lets remove it.\n# unused-import - flake8 does this anyway\ndisable=\n  abstract-class-little-used,\n  abstract-method,\n  cyclic-import,\n  duplicate-code,\n  global-statement,\n  inconsistent-return-statements,\n  locally-disabled,\n  not-an-iterable,\n  not-context-manager,\n  redefined-variable-type,\n  too-few-public-methods,\n  too-many-arguments,\n  too-many-branches,\n  too-many-instance-attributes,\n  too-many-lines,\n  too-many-locals,\n  too-many-public-methods,\n  too-many-return-statements,\n  too-many-statements,\n  unused-argument,\n  C0330,\n  C0414,\n  fixme,\n  unused-import\n\n[REPORTS]\nreports=no\n\n[TYPECHECK]\n# For attrs\nignored-classes=_CountingAttr\ngenerated-members=botocore.errorfactory\n\n[FORMAT]\nexpected-line-ending-format=LF\n\n[EXCEPTIONS]\novergeneral-exceptions=Exception,HomeAssistantError\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"homeassistant-cli\"\nversion = \"1.0.1\"\ndescription  = \"Command-line tool for Home Assistant.\"\nlicense = \"Apache Software License 2.0\"\nreadme = \"README.rst\"\nauthors = [\"The Home Assistant CLI Authors <fabian@affolter-engineering.ch>\"]\nkeywords = [ \"home\", \"automation\" ]\nhomepage = \"https://github.com/home-assistant-ecosystem/home-assistant-cli\"\nrepository = \"https://github.com/home-assistant-ecosystem/home-assistant-cli\"\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: End Users/Desktop\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: Apache Software License\",\n    \"Operating System :: OS Independent\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Home Automation\",\n]\nexclude = [\"tests\", \"tests.*\"]\n\n[tool.poetry.scripts]\nhass-cli = \"homeassistant_cli.cli:run\"\n\n[tool.poetry.dependencies]\npython = \"^3.13\"\naiohttp = \"^3.13.3\"\ndateparser = \"^1.2.0\"\njsonpath-ng = \"^1.6.1\"\njinja2 = \"^3.1.4\"\nrequests = \"^2.31.0\"\ntabulate = \"^0.9.0\"\nruamel-yaml = \"^0.19.0\"\nclick = \"^8.1.7\"\nclick-log = \"^0.4.0\"\nnetdisco = \"^3.0.0\"\npackaging = \"^25.0\"\nzeroconf = \">=0.148.0\"\n\n[dependency-groups]\ntest = [\n    \"pytest (>=9.0.0,<10.0.0)\",\n    \"mypy (>=1.10.0,<2.0.0)\",\n    \"pytest-timeout (>=2.3.1,<3.0.0)\",\n    \"requests-mock (>=1.12.1,<2.0.0)\",\n    \"pytest-sugar (>=1.0.0,<2.0.0)\",\n    \"pytest-cov (>=5.0.0,<6.0.0)\",\n    \"types-requests (>=2.31.0.20240406,<3.0.0.0)\",\n    \"types-dateparser (>=1.2.0.20240420,<2.0.0.0)\",\n    \"types-tabulate (>=0.9.0.20240106,<0.10.0.0)\",\n    \"pre-commit (>=3.0.0,<4.0.0)\",\n    \"ruff (>=0.15.10,<0.16.0)\"\n]\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.ruff]\nsrc = [\"homeassistant_cli\", \"tests\"]\nline-length = 88\n\n[tool.ruff.format]\ndocstring-code-format = true\nskip-magic-trailing-comma = true\n\n[tool.ruff.lint]\nselect = [\n    \"B\",  # bugbear\n    \"D\",  # pydocstyle\n    \"E\",  # pycodestyle\n    \"F\",  # pyflakes\n    \"I\",  # isort\n    \"PYI\", # flake8-pyi\n    \"UP\", # pyupgrade\n    \"RUF\", # ruff\n    \"W\",  # pycodestyle\n    \"PIE\", # flake8-pie\n    \"PGH004\", # pygrep-hooks - Use specific rule codes when using noqa\n    \"PLE\", # pylint error\n    \"PLW\", # pylint warning\n    \"PLR1714\", # Consider merging multiple comparisons\n    \"T100\", # flake8-debugger\n]\nignore = [\n    \"B004\", # Using `hasattr(x, \"__call__\")` to test if x is callable is unreliable.\n    \"B007\", # Loop control variable `i` not used within loop body\n    \"B009\", # Do not call `getattr` with a constant attribute value\n    \"B010\", # [*] Do not call `setattr` with a constant attribute value.\n    \"B011\", # Do not `assert False` (`python -O` removes these calls)\n    \"B028\", # No explicit `stacklevel` keyword argument found\n    \"D203\", # 1 blank line required before class docstring\n    \"D212\", # Multi-line docstring summary should start at the first line\n    \"E721\", # Do not compare types, use `isinstance()`\n    \"F401\", # Module imported but unused\n    \"D415\", # First line should end with a period, question mark, or exclamation point\n    \"RUF012\", # Mutable class attributes should be annotated with `typing.ClassVar`\n    \"PLW0603\",  # Using the global statement\n    \"PLW0120\",  # remove the else and dedent its contents\n    \"PLW2901\",  # for loop variable overwritten by assignment target\n    \"PLR5501\",  # Use `elif` instead of `else` then `if`\n    \"UP035\",  # Syntax features not supported by minimum Python version\n]\n\n[tool.ruff.lint.pydocstyle]\nconvention = \"google\"\n\n[tool.ruff.lint.isort]\nknown-first-party = [\"homeassistant_cli\"]\nsplit-on-trailing-comma = false\n\n[tool.mypy]\nfiles = \"homeassistant_cli, tests\"\nmypy_path = \"homeassistant_cli\"\ncheck_untyped_defs = true\ndisallow_untyped_calls = true\nexplicit_package_bases = true\nignore_missing_imports = true\nnamespace_packages = true\nno_implicit_optional = true\nshow_error_codes = true\nstrict = true\nwarn_incomplete_stub = true\nwarn_redundant_casts = true\nwarn_return_any = true\nwarn_unused_configs = true\nwarn_unused_ignores = true\nenable_error_code = [\n  \"ignore-without-code\",\n  \"redundant-expr\",\n  \"truthy-bool\",\n  \"no-any-return\",\n  \"no-untyped-def\",\n]\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "\"\"\"Init file for Home Assistant CLI tests.\"\"\"\n"
  },
  {
    "path": "tests/bandit.yaml",
    "content": "# https://bandit.readthedocs.io/en/latest/config.html\n\ntests:\n  - B108\n  - B306\n  - B307\n  - B313\n  - B314\n  - B315\n  - B316\n  - B317\n  - B318\n  - B319\n  - B320\n  - B325\n  - B602\n  - B604\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"conftest.py loads all fixtures found in fixtures/.\n\nEach file are made available as follows:\n\nGiven a file named: `mydata.json`\nit will be available as:\n\nmydata_text - str with the raw text\nmydata      - Dict with the content parsed from json\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\n\nimport click_log.core as logcore\nimport pytest\n\nFIXTURES_PATH = Path(__file__).parent / \"fixtures\"\n\n\nlogcore.basic_config()\n\n\n# Environment variables that should be cleared during tests\nHASS_ENV_VARS = [\n    \"HASS_SERVER\",\n    \"HASS_TOKEN\",\n    \"HASS_PASSWORD\",\n    \"HASSIO_TOKEN\",\n]\n\n\n@pytest.fixture(autouse=True)\ndef clean_hass_env(monkeypatch):\n    \"\"\"Clear Home Assistant environment variables for test isolation.\"\"\"\n    for var in HASS_ENV_VARS:\n        monkeypatch.delenv(var, raising=False)\n\n\ndef generate_fixture(content: str):\n    \"\"\"Generate the individual fixtures.\"\"\"\n    # pylint: disable=unnecessary-pass\n\n    @pytest.fixture(scope=\"module\")\n    def my_fixture():\n        return content\n\n    return my_fixture\n\n\ndef _inject_fixture(name: str, someparam: str):\n    globals()[name] = generate_fixture(someparam)\n\n\ndef _all_fixtures():\n    for fname in os.listdir(FIXTURES_PATH):\n        name, ext = os.path.splitext(fname)\n\n        with open(FIXTURES_PATH / fname) as file:\n            content = file.read()\n\n        _inject_fixture(name + \"_text\", content)\n        if ext == \".json\":\n            _inject_fixture(name, json.loads(content))\n\n\n_all_fixtures()  # type: ignore\n"
  },
  {
    "path": "tests/fixtures/basic_entities.json",
    "content": "[{\n    \"attributes\": {\n      \"auto\": true,\n      \"entity_id\": [\n        \"remote.tv\"\n      ],\n      \"friendly_name\": \"friendly long name\",\n      \"hidden\": true,\n      \"order\": 16\n    },\n    \"context\": {\n      \"id\": \"4c511277c55647eb8e7e4acf10fcd617\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"sensor.one\",\n    \"last_changed\": \"2018-12-02T10:13:05.914548+00:00\",\n    \"last_updated\": \"2018-12-04T10:13:05.914548+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"event_data\": 1002,\n      \"event_received\": \"2018-12-05 13:17:51.905847\"\n    },\n    \"context\": {\n      \"id\": \"b0e24511a0fd4eb69ab5afeac0082993\",\n      \"user_id\": \"2b0f58a02c35408c86e9e34f1d6e141d\"\n    },\n    \"entity_id\": \"sensor.two\",\n    \"last_changed\": \"2018-12-01T12:17:52.434229+00:00\",\n    \"last_updated\": \"2018-12-05T12:17:52.434229+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n    },\n    \"context\": {\n      \"id\": \"b0e24511a0fd4eb69ab5afeac0082993\",\n      \"user_id\": \"2b0f58a02c35408c86e9e34f1d6e141d\"\n    },\n    \"entity_id\": \"sensor.three\",\n    \"last_changed\": \"2018-12-03T12:17:52.434229+00:00\",\n    \"last_updated\": \"2018-12-05T12:17:52.434229+00:00\",\n    \"state\": \"off\"\n  }\n]\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table.txt",
    "content": "ENTITY        DESCRIPTION         STATE    CHANGED\nsensor.one    friendly long name  on       2018-12-02T10:13:05.914548+00:00\nsensor.two                        off      2018-12-01T12:17:52.434229+00:00\nsensor.three                      off      2018-12-03T12:17:52.434229+00:00\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table_columns.txt",
    "content": "entity              state\nfriendly long name  on\n                    off\n                    off\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table_format.txt",
    "content": "<table>\n<thead>\n<tr><th>ENTITY      </th><th>DESCRIPTION       </th><th>STATE  </th><th>CHANGED                         </th></tr>\n</thead>\n<tbody>\n<tr><td>sensor.one  </td><td>friendly long name</td><td>on     </td><td>2018-12-02T10:13:05.914548+00:00</td></tr>\n<tr><td>sensor.two  </td><td>                  </td><td>off    </td><td>2018-12-01T12:17:52.434229+00:00</td></tr>\n<tr><td>sensor.three</td><td>                  </td><td>off    </td><td>2018-12-03T12:17:52.434229+00:00</td></tr>\n</tbody>\n</table>\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table_no_header.txt",
    "content": "sensor.one    friendly long name  on   2018-12-02T10:13:05.914548+00:00\nsensor.two                        off  2018-12-01T12:17:52.434229+00:00\nsensor.three                      off  2018-12-03T12:17:52.434229+00:00\n"
  },
  {
    "path": "tests/fixtures/basic_entities_table_sorted.txt",
    "content": "entity              state    last_changed\n                    off      2018-12-01T12:17:52.434229+00:00\nfriendly long name  on       2018-12-02T10:13:05.914548+00:00\n                    off      2018-12-03T12:17:52.434229+00:00\n"
  },
  {
    "path": "tests/fixtures/default_areas.json",
    "content": "[\n    { \"area_id\": 1, \"name\": \"Kitchen\"},\n    { \"area_id\": 2, \"name\": \"Kitchen Light\"},\n    { \"area_id\": 3, \"name\": \"Bedroom\"}\n]\n"
  },
  {
    "path": "tests/fixtures/default_devices.json",
    "content": "[\n  {\n    \"config_entries\": [\n      \"424ae83a64a54fa8b6b01d71aa7d9b3d\"\n    ],\n    \"connections\": [],\n    \"manufacturer\": \"Sonos\",\n    \"model\": \"Play:5\",\n    \"name\": \"Kitchen\",\n    \"sw_version\": null,\n    \"id\": \"fa56ea5934f44fa19161bbf2a3d33732\",\n    \"hub_device_id\": null,\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color lamp\",\n    \"name\": \"Kitchen table left\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"b6f1087b94c84bc8bbe2a01adbd014d8\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color spot\",\n    \"name\": \"Kitchen front right at table\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"c022c2a832194a9aadbc40d39e7d5ee7\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color spot\",\n    \"name\": \"Kitchen left back at hub\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"cabab7fdfc97462f959aec7434989c82\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color spot\",\n    \"name\": \"Kitchen left back at bar\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"a5ffc6e863b1478d8f91b414014138d9\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color spot\",\n    \"name\": \"Kitchen right back at sink\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"63b899e6357d43879a7f356b31c233ae\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [],\n    \"connections\": [],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"Hue color spot\",\n    \"name\": \"Kitchen right middle at oven\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"43e5a30659cd4837b7ecaa5447d8be67\",\n    \"hub_device_id\": \"3e2f3eaccc0a4dedbbc86c32275e6249\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [\n      \"1a64b7d520ad44fab8622bc4efb64e88\"\n    ],\n    \"connections\": [\n      [\n        \"zigbee\",\n        \"00:17:88:01:00:f0:b1:28\"\n      ]\n    ],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"LCT003\",\n    \"name\": \"Kitchen Light 2\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"f9cad07069c74d519fbe84811c91f1fb\",\n    \"hub_device_id\": \"ff7da1f735c14b2f865b33615e359474\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  },\n  {\n    \"config_entries\": [\n      \"1a64b7d520ad44fab8622bc4efb64e88\"\n    ],\n    \"connections\": [\n      [\n        \"zigbee\",\n        \"00:17:88:01:00:f0:b2:c8\"\n      ]\n    ],\n    \"manufacturer\": \"Philips\",\n    \"model\": \"LCT003\",\n    \"name\": \"Kitchen Light 3\",\n    \"sw_version\": \"5.105.0.21536\",\n    \"id\": \"d02ec64623ae4407a80b903cbc061511\",\n    \"hub_device_id\": \"ff7da1f735c14b2f865b33615e359474\",\n    \"area_id\": \"e6ebd3e6f6e04b63a0e4a109b4748584\",\n    \"area_name\": \"Bedroom\"\n  }\n]\n"
  },
  {
    "path": "tests/fixtures/default_entities.json",
    "content": "[\n  {\n    \"attributes\": {\n      \"azimuth\": 342.38,\n      \"elevation\": -65.59,\n      \"friendly_name\": \"Sun\",\n      \"next_dawn\": \"2018-12-19T06:38:32+00:00\",\n      \"next_dusk\": \"2018-12-19T16:20:13+00:00\",\n      \"next_midnight\": \"2018-12-18T23:29:38+00:00\",\n      \"next_noon\": \"2018-12-19T11:29:23+00:00\",\n      \"next_rising\": \"2018-12-19T07:14:02+00:00\",\n      \"next_setting\": \"2018-12-19T15:44:43+00:00\"\n    },\n    \"context\": {\n      \"id\": \"1aa8b8ca7e2e45ca9f9a93ccdcdfa4e7\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"sun.sun\",\n    \"last_changed\": \"2018-12-18T15:44:23.012263+00:00\",\n    \"last_updated\": \"2018-12-18T22:58:30.010420+00:00\",\n    \"state\": \"below_horizon\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"School\",\n      \"hidden\": true,\n      \"icon\": \"mdi:school\",\n      \"latitude\": 47.011023,\n      \"longitude\": 6.858151,\n      \"radius\": 50.0\n    },\n    \"context\": {\n      \"id\": \"debb96b0d8e24c4a96212c9bc2edea1c\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"zone.school\",\n    \"last_changed\": \"2018-12-18T14:02:14.299171+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:14.299171+00:00\",\n    \"state\": \"zoning\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Unnamed zone\",\n      \"hidden\": true,\n      \"icon\": \"mdi:home\",\n      \"latitude\": 47.006476,\n      \"longitude\": 6.861699,\n      \"radius\": 50.0\n    },\n    \"context\": {\n      \"id\": \"8f768bc29bb441ca8237c3db952b2bad\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"zone.unnamed_zone\",\n    \"last_changed\": \"2018-12-18T14:02:14.299868+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:14.299868+00:00\",\n    \"state\": \"zoning\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"light.kitchen_light_1\",\n        \"light.kitchen_light_2\",\n        \"light.kitchen_light_3\",\n        \"light.kitchen_light_4\",\n        \"light.kitchen_light_5\",\n        \"light.kitchen_light_6\"\n      ],\n      \"friendly_name\": \"Kitchen Lights\",\n      \"order\": 0\n    },\n    \"context\": {\n      \"id\": \"4aa3551d07b54f0ca6bef3c27e37c522\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.kitchen_lights\",\n    \"last_changed\": \"2018-12-18T22:06:34.225732+00:00\",\n    \"last_updated\": \"2018-12-18T22:06:34.225732+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Flag to test\",\n      \"icon\": \"mdi:motion\"\n    },\n    \"context\": {\n      \"id\": \"36e903d454db49218ac61a2922eb5089\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"input_boolean.test_motion\",\n    \"last_changed\": \"2018-12-18T14:02:19.868832+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:19.868832+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:00:00\"\n    },\n    \"context\": {\n      \"id\": \"4b29669eaec04f63905d63e5b47f6905\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_small_bathroom\",\n    \"last_changed\": \"2018-12-18T19:48:31.011517+00:00\",\n    \"last_updated\": \"2018-12-18T19:48:31.011517+00:00\",\n    \"state\": \"idle\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:15:00\"\n    },\n    \"context\": {\n      \"id\": \"287036f5ec674aa09dd9d8c6862221b3\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_office_lights\",\n    \"last_changed\": \"2018-12-18T14:02:19.876167+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:19.876167+00:00\",\n    \"state\": \"idle\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:00:00\"\n    },\n    \"context\": {\n      \"id\": \"a737c5dd9b8549a2bcbe8b4d086ed0c9\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_basement\",\n    \"last_changed\": \"2018-12-18T22:37:52.011460+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.011460+00:00\",\n    \"state\": \"idle\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:00:00\"\n    },\n    \"context\": {\n      \"id\": \"d7d87d908bd344179874c42287af8e5d\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_winter_garden\",\n    \"last_changed\": \"2018-12-18T19:58:39.008777+00:00\",\n    \"last_updated\": \"2018-12-18T19:58:39.008777+00:00\",\n    \"state\": \"idle\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:00:00\"\n    },\n    \"context\": {\n      \"id\": \"cf0b226a445d439cbf1757b4d7affc6a\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_hallway\",\n    \"last_changed\": \"2018-12-18T22:50:24.006583+00:00\",\n    \"last_updated\": \"2018-12-18T22:50:24.006583+00:00\",\n    \"state\": \"idle\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:15:00\"\n    },\n    \"context\": {\n      \"id\": \"0cab71b04d41484caadc2a542142168e\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_kitchen\",\n    \"last_changed\": \"2018-12-18T22:06:34.147971+00:00\",\n    \"last_updated\": \"2018-12-18T22:06:34.147971+00:00\",\n    \"state\": \"active\"\n  },\n  {\n    \"attributes\": {\n      \"duration\": \"0:15:00\",\n      \"remaining\": \"0:15:00\"\n    },\n    \"context\": {\n      \"id\": \"a059faa2b851457f92c7d43972739233\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"timer.timer_dinner_table\",\n    \"last_changed\": \"2018-12-18T14:03:35.771700+00:00\",\n    \"last_updated\": \"2018-12-18T14:03:35.771700+00:00\",\n    \"state\": \"active\"\n  },\n  {\n    \"attributes\": {\n      \"attribution\": \"Weather forecast from met.no, delivered by the Norwegian Meteorological Institute.\",\n      \"entity_picture\": \"https://api.met.no/weatherapi/weathericon/1.1/?symbol=4;content_type=image/png\",\n      \"friendly_name\": \"yr Symbol\"\n    },\n    \"context\": {\n      \"id\": \"0aba76ed4b61458a9841324dcbb909c9\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"sensor.yr_symbol\",\n    \"last_changed\": \"2018-12-18T15:02:20.884137+00:00\",\n    \"last_updated\": \"2018-12-18T15:02:20.884137+00:00\",\n    \"state\": \"4\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Basement Motion Anywhere\"\n    },\n    \"context\": {\n      \"id\": \"17e7e13fb9aa44748fbcf1ca8b38c60c\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"binary_sensor.presence_basement_combined\",\n    \"last_changed\": \"2018-12-18T22:23:05.949232+00:00\",\n    \"last_updated\": \"2018-12-18T22:23:05.949232+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Dinner table Bright\"\n    },\n    \"context\": {\n      \"id\": \"db18bbed4f614533b18f16be07d89de8\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"scene.dinner_table_bright\",\n    \"last_changed\": \"2018-12-18T14:02:19.942400+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:19.942400+00:00\",\n    \"state\": \"scening\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Dinner table Dimmed\"\n    },\n    \"context\": {\n      \"id\": \"6f6ffd7de8cc4f6680f1b74054962f61\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"scene.dinner_table_dimmed\",\n    \"last_changed\": \"2018-12-18T14:02:19.944701+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:19.944701+00:00\",\n    \"state\": \"scening\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"light.basement_light_1\",\n        \"light.basement_light_2\",\n        \"light.basement_light_3\",\n        \"light.basement_light_4\",\n        \"light.basement_light_5\",\n        \"light.basement_light_6\",\n        \"light.basement_light_7\",\n        \"light.basement_light_8\",\n        \"light.basement_light_9\",\n        \"light.basement_light_10\",\n        \"light.basement_light_stairs\"\n      ],\n      \"friendly_name\": \"Basement Lights\",\n      \"order\": 1\n    },\n    \"context\": {\n      \"id\": \"56ebb925e75841f6b5d1a34a33e2cba0\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.basement_lights\",\n    \"last_changed\": \"2018-12-18T22:37:52.490972+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.490972+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"light.dinner_table_light_1\",\n        \"light.dinner_table_light_2\",\n        \"light.dinner_table_light_3\",\n        \"light.dinner_table_light_4\",\n        \"light.dinner_table_light_5\",\n        \"light.dinner_table_light_6\"\n      ],\n      \"friendly_name\": \"Dinner Table Lights\",\n      \"order\": 2\n    },\n    \"context\": {\n      \"id\": \"9e0e67e606c742c097871c91c204d3cd\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.dinner_table_lights\",\n    \"last_changed\": \"2018-12-18T14:02:28.743753+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:28.743753+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"light.winter_garden_light_1\",\n        \"light.winter_garden_light_2\",\n        \"light.winter_garden_light_3\",\n        \"light.winter_garden_light_4\",\n        \"light.winter_garden_light_5\"\n      ],\n      \"friendly_name\": \"Winter Garden Lights\",\n      \"order\": 3\n    },\n    \"context\": {\n      \"id\": \"83536d299e254afd8b6da2425ea70226\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.winter_garden_lights\",\n    \"last_changed\": \"2018-12-18T19:58:39.231240+00:00\",\n    \"last_updated\": \"2018-12-18T19:58:39.231240+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"light.hallway_light_1\",\n        \"light.hallway_light_2\",\n        \"light.hallway_light_3\",\n        \"light.hallway_light_4\",\n        \"light.hallway_light_5\"\n      ],\n      \"friendly_name\": \"Hallway Lights\",\n      \"order\": 4\n    },\n    \"context\": {\n      \"id\": \"3d254734f971401a8e6ed29dd319fdbd\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.hallway_lights\",\n    \"last_changed\": \"2018-12-18T14:02:20.033363+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:20.033363+00:00\",\n    \"state\": \"unknown\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_winter_garden\",\n        \"timer.timer_winter_garden\",\n        \"group.winter_garden_lights\"\n      ],\n      \"friendly_name\": \"winter garden\",\n      \"order\": 5\n    },\n    \"context\": {\n      \"id\": \"344d4bd18fe5402da58760db61b0c880\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.winter_garden_motionview\",\n    \"last_changed\": \"2018-12-18T19:58:39.251782+00:00\",\n    \"last_updated\": \"2018-12-18T19:58:39.251782+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_small_bathroom\",\n        \"timer.timer_small_bathroom\",\n        \"light.small_bathroom_light\"\n      ],\n      \"friendly_name\": \"small bathroom\",\n      \"order\": 6\n    },\n    \"context\": {\n      \"id\": \"0c2e0021f06749329a0f54b9faff589a\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.small_bathroom_motionview\",\n    \"last_changed\": \"2018-12-18T21:00:09.573499+00:00\",\n    \"last_updated\": \"2018-12-18T21:00:09.573499+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"input_boolean.test_motion\",\n        \"timer.timer_office_lights\",\n        \"light.office_light\"\n      ],\n      \"friendly_name\": \"office lights\",\n      \"order\": 7\n    },\n    \"context\": {\n      \"id\": \"30889cab566f431c9edb487511de5cdc\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.office_lights_motionview\",\n    \"last_changed\": \"2018-12-18T16:27:35.722235+00:00\",\n    \"last_updated\": \"2018-12-18T16:27:35.722235+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_basement_combined\",\n        \"timer.timer_basement\",\n        \"group.basement_lights\"\n      ],\n      \"friendly_name\": \"basement\",\n      \"order\": 8\n    },\n    \"context\": {\n      \"id\": \"658e61b35f97402093e4ea89beed58ca\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.basement_motionview\",\n    \"last_changed\": \"2018-12-18T22:37:52.510621+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.510621+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_hallway\",\n        \"timer.timer_hallway\",\n        \"group.hallway_lights\"\n      ],\n      \"friendly_name\": \"hallway\",\n      \"order\": 9\n    },\n    \"context\": {\n      \"id\": \"dcb0150012514aecbe7f5fe1aa9d5a16\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.hallway_motionview\",\n    \"last_changed\": \"2018-12-18T22:35:45.991232+00:00\",\n    \"last_updated\": \"2018-12-18T22:35:45.991232+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_kitchen\",\n        \"timer.timer_kitchen\",\n        \"group.kitchen_lights\"\n      ],\n      \"friendly_name\": \"kitchen\",\n      \"order\": 10\n    },\n    \"context\": {\n      \"id\": \"afa1715ade6541608eea07e40b61a498\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.kitchen_motionview\",\n    \"last_changed\": \"2018-12-18T22:06:34.120687+00:00\",\n    \"last_updated\": \"2018-12-18T22:06:34.120687+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"entity_id\": [\n        \"binary_sensor.presence_dinner_table\",\n        \"timer.timer_dinner_table\",\n        \"group.dinner_table_lights\"\n      ],\n      \"friendly_name\": \"dinner table\",\n      \"order\": 11\n    },\n    \"context\": {\n      \"id\": \"7d6d413228cd4a1aa1b23ee468aa9963\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"group.dinner_table_motionview\",\n    \"last_changed\": \"2018-12-18T14:02:29.245690+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:29.245690+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"Delivered\": 29,\n      \"Expired\": 1,\n      \"InTransit\": 2,\n      \"OutForDelivery\": 1,\n      \"attribution\": \"Information provided by AfterShip\",\n      \"friendly_name\": \"aftership\",\n      \"icon\": \"mdi:package-variant-closed\",\n      \"unit_of_measurement\": \"packages\"\n    },\n    \"context\": {\n      \"id\": \"3e437ab5be514a3583aacf5f7583f8ae\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"sensor.aftership\",\n    \"last_changed\": \"2018-12-18T14:02:27.579862+00:00\",\n    \"last_updated\": \"2018-12-18T14:02:27.579862+00:00\",\n    \"state\": \"4\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn off basement lights at end of timer\",\n      \"last_triggered\": \"2018-12-18T22:37:52.381699+00:00\"\n    },\n    \"context\": {\n      \"id\": \"3aec6c4d38744650a678936deb07fb51\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_off_basement_lights_at_end_of_timer\",\n    \"last_changed\": \"2018-12-18T14:02:53.021580+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.382204+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn on hallway when there is movement\",\n      \"last_triggered\": \"2018-12-18T22:35:23.614914+00:00\"\n    },\n    \"context\": {\n      \"id\": \"bfb5dd223da8426da3bf1e47c71b32ed\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_on_hallway_when_there_is_movement\",\n    \"last_changed\": \"2018-12-18T14:02:53.044372+00:00\",\n    \"last_updated\": \"2018-12-18T22:35:23.615491+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn off hallway lights at end of timer\",\n      \"last_triggered\": \"2018-12-18T22:50:24.056345+00:00\"\n    },\n    \"context\": {\n      \"id\": \"be3a93679fac421aa7ee49f9c29fc507\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_off_hallway_lights_at_end_of_timer\",\n    \"last_changed\": \"2018-12-18T14:02:53.049397+00:00\",\n    \"last_updated\": \"2018-12-18T22:50:24.056994+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn on kitchen when there is movement\",\n      \"last_triggered\": \"2018-12-18T22:47:57.261986+00:00\"\n    },\n    \"context\": {\n      \"id\": \"4b46d82b228740c892f3c5273e8b320c\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_on_kitchen_when_there_is_movement\",\n    \"last_changed\": \"2018-12-18T14:02:53.062443+00:00\",\n    \"last_updated\": \"2018-12-18T22:47:57.262561+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn on winter garden when there is movement\",\n      \"last_triggered\": \"2018-12-18T19:43:39.012591+00:00\"\n    },\n    \"context\": {\n      \"id\": \"341993015c5e4acba2e6a40019c0ed77\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_on_winter_garden_when_there_is_movement\",\n    \"last_changed\": \"2018-12-18T14:02:53.067628+00:00\",\n    \"last_updated\": \"2018-12-18T19:43:39.013194+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Turn off kitchen lights at end of timer\",\n      \"last_triggered\": \"2018-12-18T22:04:10.897212+00:00\"\n    },\n    \"context\": {\n      \"id\": \"07f9cc9a49f64c84a05f3e5579dec923\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"automation.turn_off_kitchen_lights_at_end_of_timer\",\n    \"last_changed\": \"2018-12-18T14:02:53.092096+00:00\",\n    \"last_updated\": \"2018-12-18T22:04:10.897616+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Hallway Light 2\",\n      \"is_deconz_group\": false,\n      \"supported_features\": 41\n    },\n    \"context\": {\n      \"id\": \"612782c5951f42fc83dcb9a4ddf91996\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.hallroom_light_2\",\n    \"last_changed\": \"2018-12-18T17:57:31.691709+00:00\",\n    \"last_updated\": \"2018-12-18T17:57:31.691709+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Hallway Light 1\",\n      \"is_deconz_group\": false,\n      \"supported_features\": 41\n    },\n    \"context\": {\n      \"id\": \"d7246c6bf11a453e83926bb0383bc0bc\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.hallroom_light_1\",\n    \"last_changed\": \"2018-12-18T17:57:31.781389+00:00\",\n    \"last_updated\": \"2018-12-18T17:57:31.781389+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Basement Light 1\",\n      \"is_deconz_group\": false,\n      \"max_mireds\": 500,\n      \"min_mireds\": 153,\n      \"supported_features\": 43\n    },\n    \"context\": {\n      \"id\": \"c5de2f982cef47e6a866c5aa6cdcf6e5\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.basement_light_1\",\n    \"last_changed\": \"2018-12-18T22:37:52.088528+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.088528+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Basement Light Stairs\",\n      \"is_deconz_group\": false,\n      \"max_mireds\": 500,\n      \"min_mireds\": 153,\n      \"supported_features\": 43\n    },\n    \"context\": {\n      \"id\": \"c5de2f982cef47e6a866c5aa6cdcf6e5\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.basement_light_stairs\",\n    \"last_changed\": \"2018-12-18T22:37:52.115905+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.115905+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"brightness\": 122,\n      \"color_temp\": 366,\n      \"friendly_name\": \"Dinner table Light 1\",\n      \"is_deconz_group\": false,\n      \"max_mireds\": 500,\n      \"min_mireds\": 153,\n      \"supported_features\": 43\n    },\n    \"context\": {\n      \"id\": \"b01d42017c5b401789843bda65419ffd\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.dinner_table_light_1\",\n    \"last_changed\": \"2018-12-18T14:02:27.987936+00:00\",\n    \"last_updated\": \"2018-12-18T18:02:09.065835+00:00\",\n    \"state\": \"on\"\n  },\n  {\n    \"attributes\": {\n      \"friendly_name\": \"Basement Light 3\",\n      \"is_deconz_group\": false,\n      \"max_mireds\": 500,\n      \"min_mireds\": 153,\n      \"supported_features\": 43\n    },\n    \"context\": {\n      \"id\": \"c5de2f982cef47e6a866c5aa6cdcf6e5\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.basement_light_3\",\n    \"last_changed\": \"2018-12-18T22:37:52.163524+00:00\",\n    \"last_updated\": \"2018-12-18T22:37:52.163524+00:00\",\n    \"state\": \"off\"\n  },\n  {\n    \"attributes\": {\n      \"brightness\": 211,\n      \"color_temp\": 454,\n      \"friendly_name\": \"Small Bathroom Light\",\n      \"is_deconz_group\": false,\n      \"max_mireds\": 500,\n      \"min_mireds\": 153,\n      \"supported_features\": 43\n    },\n    \"context\": {\n      \"id\": \"5bdccd8faa944249a2553ff82ddf8750\",\n      \"user_id\": null\n    },\n    \"entity_id\": \"light.small_bathroom_light\",\n    \"last_changed\": \"2018-12-18T21:00:09.555823+00:00\",\n    \"last_updated\": \"2018-12-18T21:00:09.555823+00:00\",\n    \"state\": \"on\"\n  }\n]\n"
  },
  {
    "path": "tests/fixtures/default_events.json",
    "content": "[\n  {\n    \"event\": \"homeassistant_close\",\n    \"listener_count\": 3\n  },\n  {\n    \"event\": \"call_service\",\n    \"listener_count\": 1\n  },\n  {\n    \"event\": \"*\",\n    \"listener_count\": 3\n  },\n  {\n    \"event\": \"homeassistant_stop\",\n    \"listener_count\": 13\n  },\n  {\n    \"event\": \"time_changed\",\n    \"listener_count\": 18\n  },\n  {\n    \"event\": \"component_loaded\",\n    \"listener_count\": 2\n  },\n  {\n    \"event\": \"platform_discovered\",\n    \"listener_count\": 12\n  },\n  {\n    \"event\": \"state_changed\",\n    \"listener_count\": 113\n  },\n  {\n    \"event\": \"timer.finished\",\n    \"listener_count\": 7\n  },\n  {\n    \"event\": \"service_registered\",\n    \"listener_count\": 1\n  },\n  {\n    \"event\": \"service_removed\",\n    \"listener_count\": 1\n  }\n]\n"
  },
  {
    "path": "tests/fixtures/default_services.json",
    "content": "[\n  {\n    \"domain\": \"homeassistant\",\n    \"services\": {\n      \"check_config\": {\n        \"description\": \"Check the Home Assistant configuration files for errors. Errors will be displayed in the Home Assistant log.\",\n        \"fields\": {}\n      },\n      \"reload_core_config\": {\n        \"description\": \"Reload the core configuration.\",\n        \"fields\": {}\n      },\n      \"restart\": {\n        \"description\": \"Restart the Home Assistant service.\",\n        \"fields\": {}\n      },\n      \"stop\": {\n        \"description\": \"Stop the Home Assistant service.\",\n        \"fields\": {}\n      },\n      \"toggle\": {\n        \"description\": \"Generic service to toggle devices on/off under any domain. Same usage as the light.turn_on, switch.turn_on, etc. services.\",\n        \"fields\": {\n          \"entity_id\": {\n            \"description\": \"The entity_id of the device to toggle on/off.\",\n            \"example\": \"light.living_room\"\n          }\n        }\n      }\n    }\n  },\n  {\n    \"domain\": \"group\",\n    \"services\": {\n      \"reload\": {\n        \"description\": \"Reload group configuration.\",\n        \"fields\": {}\n      },\n      \"remove\": {\n        \"description\": \"Remove a user group.\",\n        \"fields\": {\n          \"object_id\": {\n            \"description\": \"Group id and part of entity id.\",\n            \"example\": \"test_group\"\n          }\n        }\n      },\n      \"set\": {\n        \"description\": \"Create/Update a user group.\",\n        \"fields\": {\n          \"add_entities\": {\n            \"description\": \"List of members they will change on group listening.\",\n            \"example\": \"domain.entity_id1, domain.entity_id2\"\n          },\n          \"all\": {\n            \"description\": \"Enable this option if the group should only turn on when all entities are on.\",\n            \"example\": true\n          },\n          \"control\": {\n            \"description\": \"Value for control the group control.\",\n            \"example\": \"hidden\"\n          },\n          \"entities\": {\n            \"description\": \"List of all members in the group. Not compatible with 'delta'.\",\n            \"example\": \"domain.entity_id1, domain.entity_id2\"\n          },\n          \"icon\": {\n            \"description\": \"Name of icon for the group.\",\n            \"example\": \"mdi:camera\"\n          },\n          \"name\": {\n            \"description\": \"Name of group\",\n            \"example\": \"My test group\"\n          },\n          \"object_id\": {\n            \"description\": \"Group id and part of entity id.\",\n            \"example\": \"test_group\"\n          },\n          \"view\": {\n            \"description\": \"Boolean for if the group is a view.\",\n            \"example\": true\n          },\n          \"visible\": {\n            \"description\": \"If the group is visible on UI.\",\n            \"example\": true\n          }\n        }\n      },\n      \"set_visibility\": {\n        \"description\": \"Hide or show a group.\",\n        \"fields\": {\n          \"entity_id\": {\n            \"description\": \"Name(s) of entities to set value.\",\n            \"example\": \"group.travel\"\n          },\n          \"visible\": {\n            \"description\": \"True if group should be shown or False if it should be hidden.\",\n            \"example\": true\n          }\n        }\n      }\n    }\n  },\n  {\n    \"domain\": \"light\",\n    \"services\": {\n      \"toggle\": {\n        \"description\": \"Toggles a light.\",\n        \"fields\": {\n          \"entity_id\": {\n            \"description\": \"Name(s) of entities to toggle.\",\n            \"example\": \"light.kitchen\"\n          },\n          \"transition\": {\n            \"description\": \"Duration in seconds it takes to get to next state.\",\n            \"example\": 60\n          }\n        }\n      },\n      \"turn_off\": {\n        \"description\": \"Turn a light off.\",\n        \"fields\": {\n          \"entity_id\": {\n            \"description\": \"Name(s) of entities to turn off.\",\n            \"example\": \"light.kitchen\"\n          },\n          \"flash\": {\n            \"description\": \"If the light should flash.\",\n            \"values\": [\n              \"short\",\n              \"long\"\n            ]\n          },\n          \"transition\": {\n            \"description\": \"Duration in seconds it takes to get to next state.\",\n            \"example\": 60\n          }\n        }\n      },\n      \"turn_on\": {\n        \"description\": \"Turn a light on.\",\n        \"fields\": {\n          \"brightness\": {\n            \"description\": \"Number between 0..255 indicating brightness.\",\n            \"example\": 120\n          },\n          \"brightness_pct\": {\n            \"description\": \"Number between 0..100 indicating percentage of full brightness.\",\n            \"example\": 47\n          },\n          \"color_name\": {\n            \"description\": \"A human readable color name.\",\n            \"example\": \"red\"\n          },\n          \"color_temp\": {\n            \"description\": \"Color temperature for the light in mireds.\",\n            \"example\": 250\n          },\n          \"effect\": {\n            \"description\": \"Light effect.\",\n            \"values\": [\n              \"colorloop\",\n              \"random\"\n            ]\n          },\n          \"entity_id\": {\n            \"description\": \"Name(s) of entities to turn on\",\n            \"example\": \"light.kitchen\"\n          },\n          \"flash\": {\n            \"description\": \"If the light should flash.\",\n            \"values\": [\n              \"short\",\n              \"long\"\n            ]\n          },\n          \"hs_color\": {\n            \"description\": \"Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.\",\n            \"example\": \"[300, 70]\"\n          },\n          \"kelvin\": {\n            \"description\": \"Color temperature for the light in Kelvin.\",\n            \"example\": 4000\n          },\n          \"profile\": {\n            \"description\": \"Name of a light profile to use.\",\n            \"example\": \"relax\"\n          },\n          \"rgb_color\": {\n            \"description\": \"Color for the light in RGB-format.\",\n            \"example\": \"[255, 100, 100]\"\n          },\n          \"transition\": {\n            \"description\": \"Duration in seconds it takes to get to next state\",\n            \"example\": 60\n          },\n          \"white_value\": {\n            \"description\": \"Number between 0..255 indicating level of white.\",\n            \"example\": \"250\"\n          },\n          \"xy_color\": {\n            \"description\": \"Color for the light in XY-format.\",\n            \"example\": \"[0.52, 0.43]\"\n          }\n        }\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "tests/test_area.py",
    "content": "\"\"\"Testing Area operations.\"\"\"\n\nimport json\nimport unittest.mock as mock\n\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n\ndef test_area_list(default_areas) -> None:\n    \"\"\"Test Area List.\"\"\"\n    with mock.patch(\"homeassistant_cli.remote.get_areas\", return_value=default_areas):\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli, [\"--output=json\", \"area\", \"list\"], catch_exceptions=False\n        )\n        assert result.exit_code == 0\n\n        data = json.loads(result.output)\n        assert len(data) == 3\n\n\ndef test_area_list_filter(default_areas) -> None:\n    \"\"\"Test Area List.\"\"\"\n    with mock.patch(\"homeassistant_cli.remote.get_areas\", return_value=default_areas):\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"area\", \"list\", \"Bed.*\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert data[0][\"name\"] == \"Bedroom\"\n"
  },
  {
    "path": "tests/test_completion.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nfrom typing import cast\n\nimport requests_mock\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.cli as cli\nfrom homeassistant_cli.config import Configuration\n\n\ndef test_entity_completion(basic_entities_text) -> None:\n    \"\"\"Test completion for entities.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/states\",\n            text=basic_entities_text,\n            status_code=200,\n        )\n\n        cfg = cli.cli.make_context(\"hass-cli\", [\"entity\", \"get\"])\n        result = autocompletion.entities(\n            cast(cfg, Configuration),\n            [\"entity\", \"get\"],\n            \"\",  # type: ignore\n        )\n        assert len(result) == 3\n\n        resultdict = dict(result)\n\n        assert \"sensor.one\" in resultdict\n        assert resultdict[\"sensor.one\"] == \"friendly long name\"\n\n\ndef test_service_completion(default_services_text) -> None:\n    \"\"\"Test completion for services.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/services\",\n            text=default_services_text,\n            status_code=200,\n        )\n\n        cfg = cli.cli.make_context(\"hass-cli\", [\"service\", \"list\"])\n\n        result = autocompletion.services(\n            cfg,\n            [\"service\", \"list\"],\n            \"\",  # type: ignore\n        )\n        assert len(result) == 12\n\n        resultdict = dict(result)\n\n        assert \"group.remove\" in resultdict\n        val = resultdict[\"group.remove\"]\n        assert val == \"Remove a user group.\"\n\n\ndef test_event_completion(default_events_text) -> None:\n    \"\"\"Test completion for events.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/events\",\n            text=default_events_text,\n            status_code=200,\n        )\n\n        cfg = cli.cli.make_context(\"hass-cli\", [\"events\", \"list\"])\n\n        result = autocompletion.events(\n            cfg,\n            [\"events\", \"list\"],\n            \"\",  # type: ignore\n        )\n        assert len(result) == 11\n\n        resultdict = dict(result)\n\n        assert \"component_loaded\" in resultdict\n        assert resultdict[\"component_loaded\"] == \"\"\n\n\ndef test_area_completion(default_events_text) -> None:\n    \"\"\"Test completion for Area.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/events\",\n            text=default_events_text,\n            status_code=200,\n        )\n\n        cfg = cli.cli.make_context(\"hass-cli\", [\"events\", \"list\"])\n\n        result = autocompletion.events(\n            cfg,\n            [\"events\", \"list\"],\n            \"\",  # type: ignore\n        )\n        assert len(result) == 11\n\n        resultdict = dict(result)\n\n        assert \"component_loaded\" in resultdict\n        assert resultdict[\"component_loaded\"] == \"\"\n"
  },
  {
    "path": "tests/test_defaults.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport os\nfrom unittest import mock\n\nimport pytest\nimport requests_mock\n\nimport homeassistant_cli.cli as cli\n\nMDNS_SERVER_FALLBACK = \"http://homeassistant.local:8123\"\nHASS_SERVER = \"http://localhost:8123\"\n\n\n@pytest.mark.parametrize(\n    \"description,env,expected_server,expected_resolved_server,\\\n    expected_token,expected_password\",\n    [\n        (\n            \"No env set, all should be defaults\",\n            {},\n            \"auto\",\n            HASS_SERVER,\n            None,\n            None,\n        ),\n        (\n            \"If only HASSIO_TOKEN, use default hassio\",\n            {\"HASSIO_TOKEN\": \"supersecret\"},\n            \"auto\",\n            MDNS_SERVER_FALLBACK,\n            \"supersecret\",\n            None,\n        ),\n        (\n            \"Honor HASS_SERVER together with HASSIO_TOKEN\",\n            {\n                \"HASSIO_TOKEN\": \"supersecret\",\n                \"HASS_SERVER\": \"http://localhost:63333\",\n            },\n            \"http://localhost:63333\",\n            \"http://localhost:63333\",\n            \"supersecret\",\n            None,\n        ),\n        (\n            \"HASS_TOKEN should win over HASSIO_TOKEN\",\n            {\"HASSIO_TOKEN\": \"supersecret\", \"HASS_TOKEN\": \"I Win!\"},\n            \"auto\",\n            HASS_SERVER,\n            \"I Win!\",\n            None,\n        ),\n        (\n            \"HASS_PASSWORD should be honored\",\n            {\"HASS_PASSWORD\": \"supersecret\"},\n            \"auto\",\n            HASS_SERVER,\n            None,\n            \"supersecret\",\n        ),\n    ],\n)\ndef test_defaults(\n    description: str,\n    env: dict[str, str],\n    expected_resolved_server,\n    expected_server: str,\n    expected_token: str | None,\n    expected_password: str | None,\n) -> None:\n    \"\"\"Test defaults applied correctly for server, token and password.\"\"\"\n    mockenv = mock.patch.dict(os.environ, env)\n\n    try:\n        mockenv.start()\n        with requests_mock.mock() as mockhttp:\n            expserver = f\"{expected_resolved_server}/api/config\"\n            mockhttp.get(\n                expserver,\n                json={\"name\": \"mock response\", \"version\": \"1.0.0\"},\n                status_code=200,\n            )\n            ctx = cli.cli.make_context(\n                \"hass-cli\", [\"--timeout\", \"1\", \"config\", \"release\"]\n            )\n            with ctx:  # type: ignore\n                cli.cli.invoke(ctx)\n\n            cfg = ctx.obj\n\n            assert cfg.server == expected_server\n            assert cfg.resolve_server() == expected_resolved_server\n            assert cfg.token == expected_token\n\n            assert mockhttp.call_count == 1\n\n            assert mockhttp.request_history[0].url.startswith(expected_resolved_server)\n\n            if expected_token:\n                auth = mockhttp.request_history[0].headers[\"Authorization\"]\n                assert auth == \"Bearer \" + expected_token\n            elif expected_password:\n                password = mockhttp.request_history[0].headers[\"x-ha-access\"]\n                assert password == expected_password\n            else:\n                assert \"Authorization\" not in mockhttp.request_history[0].headers\n                assert \"x-ha-access\" not in mockhttp.request_history[0].headers\n\n    finally:\n        mockenv.stop()\n"
  },
  {
    "path": "tests/test_device.py",
    "content": "\"\"\"Testing Device operations.\"\"\"\n\nimport json\nimport unittest.mock as mock\n\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n\ndef test_device_list(default_devices, default_areas) -> None:\n    \"\"\"Test Device List.\"\"\"\n    with mock.patch(\n        \"homeassistant_cli.remote.get_devices\", return_value=default_devices\n    ):\n        with mock.patch(\n            \"homeassistant_cli.remote.get_areas\", return_value=default_areas\n        ):\n            runner = CliRunner()\n            result = runner.invoke(\n                cli.cli,\n                [\"--output=json\", \"device\", \"list\"],\n                catch_exceptions=False,\n            )\n            assert result.exit_code == 0\n\n            data = json.loads(result.output)\n            assert len(data) == 9\n\n\ndef test_device_list_filter(default_devices, default_areas) -> None:\n    \"\"\"Test Device List.\"\"\"\n    with mock.patch(\n        \"homeassistant_cli.remote.get_devices\", return_value=default_devices\n    ):\n        with mock.patch(\n            \"homeassistant_cli.remote.get_areas\", return_value=default_areas\n        ):\n            runner = CliRunner()\n            result = runner.invoke(\n                cli.cli,\n                [\"--output=json\", \"device\", \"list\", \"table\"],\n                catch_exceptions=False,\n            )\n            assert result.exit_code == 0\n\n            data = json.loads(result.output)\n            assert len(data) == 2\n            assert data[0][\"name\"] == \"Kitchen table left\"\n            assert data[1][\"name\"] == \"Kitchen front right at table\"\n\n\ndef test_device_assign(default_areas, default_devices) -> None:\n    \"\"\"Test basic device assign.\"\"\"\n    with mock.patch(\n        \"homeassistant_cli.remote.get_devices\", return_value=default_devices\n    ):\n        with mock.patch(\n            \"homeassistant_cli.remote.get_areas\", return_value=default_areas\n        ):\n            with mock.patch(\n                \"homeassistant_cli.remote.assign_area\",\n                return_value={\"success\": True},\n            ):\n                runner = CliRunner()\n                result = runner.invoke(\n                    cli.cli,\n                    [\"device\", \"assign\", \"Kitchen\", \"Kitchen table left\"],\n                    catch_exceptions=False,\n                )\n                # print(result.output)\n                assert result.exit_code == 0\n                expected = \"Successfully assigned 'Kitchen' to 'Kitchen table left'\\n\"\n                assert result.output == expected\n"
  },
  {
    "path": "tests/test_ha.py",
    "content": "\"\"\"Tests for Home Assistant Operating System plugin (ha.py).\"\"\"\n\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n\ndef test_os_update_already_latest() -> None:\n    \"\"\"Test os update when already on latest version.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost/os/info\",\n            json={\n                \"result\": \"ok\",\n                \"data\": {\"version\": \"12.0\", \"version_latest\": \"12.0\"},\n            },\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"ha\", \"os\", \"update\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Already running the latest release\" in result.output\n\n\ndef test_os_update_needs_update() -> None:\n    \"\"\"Test os update when newer version available.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost/os/info\",\n            json={\n                \"result\": \"ok\",\n                \"data\": {\"version\": \"11.0\", \"version_latest\": \"12.0\"},\n            },\n            status_code=200,\n        )\n        mock.post(\n            \"http://localhost/os/update\",\n            json={\"result\": \"ok\", \"data\": {}},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"ha\", \"os\", \"update\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Already running the latest release\" not in result.output\n\n\ndef test_core_update_already_latest() -> None:\n    \"\"\"Test core update when already on latest version.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost/core/info\",\n            json={\n                \"result\": \"ok\",\n                \"data\": {\"version\": \"2024.4.0\", \"version_latest\": \"2024.4.0\"},\n            },\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"ha\", \"core\", \"update\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Already running the latest release\" in result.output\n\n\ndef test_core_update_needs_update() -> None:\n    \"\"\"Test core update when newer version available.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost/core/info\",\n            json={\n                \"result\": \"ok\",\n                \"data\": {\"version\": \"2024.3.0\", \"version_latest\": \"2024.4.0\"},\n            },\n            status_code=200,\n        )\n        mock.post(\n            \"http://localhost/core/update\",\n            json={\"result\": \"ok\", \"data\": {}},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"ha\", \"core\", \"update\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Already running the latest release\" not in result.output\n"
  },
  {
    "path": "tests/test_helper.py",
    "content": "\"\"\"Tests for helper.\"\"\"\n\nfrom collections.abc import Sized\nfrom typing import cast\n\nimport homeassistant_cli.helper as helper\n\n\ndef test_to_attributes_multiples():\n    \"\"\"Basic assertions on to_attributes.\"\"\"\n    data = helper.to_attributes(\"entity_id=entityone,attr1=val1\")\n    assert len(cast(Sized, data)) == 2\n\n    assert data[\"entity_id\"] == \"entityone\"\n    assert data[\"attr1\"] == \"val1\"\n\n\ndef test_to_attributes_none():\n    \"\"\"Basic assertions on to_attributes.\"\"\"\n    data = helper.to_attributes(\"\")\n    assert data == {}\n\n\ndef test_to_tuples():\n    \"\"\"Basic title test on to_tuples.\"\"\"\n    data = helper.to_tuples(\"a=entity_id,b=state\")\n    assert len(data) == 2\n    assert data[0] == (\"a\", \"entity_id\")\n    assert data[1] == (\"b\", \"state\")\n\n\ndef test_to_tuples_no_header():\n    \"\"\"Test to_tuples without header.\"\"\"\n    data = helper.to_tuples(\"entity_id,state\")\n    assert len(data) == 2\n    assert data[0] == (\"entity_id\",)\n    assert data[1] == (\"state\",)\n\n\ndef test_sorting_by_jsonpath():\n    \"\"\"Test sorting function by jsonpath works.\"\"\"\n    result = [\n        {\"id\": \"Uno\", \"no\": 3},\n        {\"id\": \"Duo\", \"no\": 2},\n        {\"id\": \"Trio\", \"no\": 1},\n        {\"id\": None, \"no\": 0},\n    ]  # type: List\n    helper._sort_table(result, \"id\")  # pylint: disable=W0212\n\n    assert result[0].get(\"id\") == \"Duo\"\n    assert result[1].get(\"id\") == \"Trio\"\n    assert result[2].get(\"id\") == \"Uno\"\n    assert result[3].get(\"id\") is None\n\n    helper._sort_table(result, \"no\")  # pylint: disable=W0212\n\n    assert result[0].get(\"id\") is None\n    assert result[1].get(\"id\") == \"Trio\"\n    assert result[2].get(\"id\") == \"Duo\"\n    assert result[3].get(\"id\") == \"Uno\"\n"
  },
  {
    "path": "tests/test_info.py",
    "content": "\"\"\"Tests for the info plugin.\"\"\"\n\nimport pytest\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\nfrom homeassistant_cli.plugins.info import redacted_output\n\n\nclass TestRedactedOutput:\n    \"\"\"Tests for the redacted_output function.\"\"\"\n\n    def test_none_token(self) -> None:\n        \"\"\"Test with None token.\"\"\"\n        assert redacted_output(None) == \"***\"\n\n    def test_empty_token(self) -> None:\n        \"\"\"Test with empty token.\"\"\"\n        assert redacted_output(\"\") == \"***\"\n\n    def test_short_token(self) -> None:\n        \"\"\"Test with token 8 chars or less.\"\"\"\n        assert redacted_output(\"12345678\") == \"***\"\n        assert redacted_output(\"1234567\") == \"***\"\n        assert redacted_output(\"abc\") == \"***\"\n\n    def test_long_token(self) -> None:\n        \"\"\"Test with token longer than 8 chars.\"\"\"\n        # For a 16-char token: first 4, then (16//8 - 8) = -6 stars (so 0), then last 4\n        # That's likely a bug in the logic, but let's test what it does\n        token = \"abcd1234efgh5678\"  # 16 chars\n        result = redacted_output(token)\n        assert result.startswith(\"abcd\")\n        assert result.endswith(\"5678\")\n\n    def test_very_long_token(self) -> None:\n        \"\"\"Test with a very long token (typical JWT).\"\"\"\n        # 128-char token: first 4, then (128//8 - 8) = 8 stars, then last 4\n        token = \"a\" * 4 + \"x\" * 120 + \"z\" * 4\n        result = redacted_output(token)\n        assert result.startswith(\"aaaa\")\n        assert result.endswith(\"zzzz\")\n        assert \"*\" in result\n\n\nclass TestInfoCliCommand:\n    \"\"\"Tests for the info cli command.\"\"\"\n\n    def test_info_cli_output(self) -> None:\n        \"\"\"Test that info cli shows expected fields.\"\"\"\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"--output=yaml\", \"info\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        # Check expected fields are present\n        assert \"Server URL\" in result.output\n        assert \"http://localhost:8123\" in result.output\n        assert \"CLI version\" in result.output\n        assert \"Timeout\" in result.output\n\n    def test_info_cli_with_token(self) -> None:\n        \"\"\"Test that info cli redacts token.\"\"\"\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\n                \"--server\",\n                \"http://localhost:8123\",\n                \"--token\",\n                \"supersecretlongtoken123456\",\n                \"--output=yaml\",\n                \"info\",\n            ],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        # Token should be redacted, not shown in full\n        assert \"supersecretlongtoken123456\" not in result.output\n        assert \"Token\" in result.output\n\n    def test_info_cli_json_output(self) -> None:\n        \"\"\"Test info cli with JSON output.\"\"\"\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"--output=json\", \"info\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"{\" in result.output\n        assert \"Server URL\" in result.output\n\n    def test_info_cli_shows_version(self) -> None:\n        \"\"\"Test that CLI version is displayed.\"\"\"\n        from homeassistant_cli.const import __version__\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--server\", \"http://localhost:8123\", \"--output=yaml\", \"info\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert __version__ in result.output\n"
  },
  {
    "path": "tests/test_integration.py",
    "content": "\"\"\"Tests for the integration plugin.\"\"\"\n\nimport json\nfrom unittest import mock\n\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n# Sample config entries data\nSAMPLE_CONFIG_ENTRIES = [\n    {\n        \"entry_id\": \"01JS6FQ9VD5A1CR30KE4F4MWXG\",\n        \"domain\": \"hue\",\n        \"title\": \"Philips Hue\",\n        \"state\": \"loaded\",\n        \"disabled_by\": None,\n    },\n    {\n        \"entry_id\": \"02AB7GR0WE6B2DS41LF5G5NXYH\",\n        \"domain\": \"mqtt\",\n        \"title\": \"MQTT Broker\",\n        \"state\": \"loaded\",\n        \"disabled_by\": None,\n    },\n    {\n        \"entry_id\": \"03CD8HS1XF7C3ET52MG6H6OYZI\",\n        \"domain\": \"zwave_js\",\n        \"title\": \"Z-Wave JS\",\n        \"state\": \"not_loaded\",\n        \"disabled_by\": \"user\",\n    },\n]\n\n\ndef test_integration_list() -> None:\n    \"\"\"Test listing all integrations.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 3\n\n\ndef test_integration_list_with_filter() -> None:\n    \"\"\"Test listing integrations with a filter.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list\", \"hue\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert data[0][\"domain\"] == \"hue\"\n\n\ndef test_integration_list_filter_by_title() -> None:\n    \"\"\"Test listing integrations filtered by title.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list\", \"Philips\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert data[0][\"title\"] == \"Philips Hue\"\n\n\ndef test_integration_info() -> None:\n    \"\"\"Test getting info for a specific integration.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"info\", \"01JS6FQ9VD5A1CR30KE4F4MWXG\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert data[\"domain\"] == \"hue\"\n        assert data[\"title\"] == \"Philips Hue\"\n\n\ndef test_integration_info_partial_match() -> None:\n    \"\"\"Test getting info with partial entry_id match.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"info\", \"01JS6\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert data[\"entry_id\"] == \"01JS6FQ9VD5A1CR30KE4F4MWXG\"\n\n\ndef test_integration_reload_success() -> None:\n    \"\"\"Test reloading an integration successfully.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n        mock_req.post(\n            \"http://localhost:8123/api/config/config_entries/entry/01JS6FQ9VD5A1CR30KE4F4MWXG/reload\",\n            json={\"require_restart\": False},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"integration\", \"reload\", \"01JS6FQ9VD5A1CR30KE4F4MWXG\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Successfully reloaded\" in result.output\n\n\ndef test_integration_reload_partial_id() -> None:\n    \"\"\"Test reloading an integration with partial entry_id.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n        mock_req.post(\n            \"http://localhost:8123/api/config/config_entries/entry/01JS6FQ9VD5A1CR30KE4F4MWXG/reload\",\n            json={\"require_restart\": False},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"integration\", \"reload\", \"01JS6\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Successfully reloaded\" in result.output\n\n\ndef test_integration_delete_with_confirm() -> None:\n    \"\"\"Test deleting an integration with --confirm flag.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n        mock_req.delete(\n            \"http://localhost:8123/api/config/config_entries/entry/01JS6FQ9VD5A1CR30KE4F4MWXG\",\n            json={\"require_restart\": False},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"integration\", \"delete\", \"01JS6FQ9VD5A1CR30KE4F4MWXG\", \"--confirm\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        assert \"Successfully deleted\" in result.output\n\n\ndef test_integration_disable() -> None:\n    \"\"\"Test disabling an integration.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        with mock.patch(\n            \"homeassistant_cli.remote.wsapi\",\n            return_value={\"success\": True, \"result\": {\"require_restart\": False}},\n        ):\n            runner = CliRunner()\n            result = runner.invoke(\n                cli.cli,\n                [\"integration\", \"disable\", \"01JS6FQ9VD5A1CR30KE4F4MWXG\"],\n                catch_exceptions=False,\n            )\n            assert result.exit_code == 0\n            assert \"Successfully disabled\" in result.output\n\n\ndef test_integration_enable() -> None:\n    \"\"\"Test enabling a disabled integration.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        with mock.patch(\n            \"homeassistant_cli.remote.wsapi\",\n            return_value={\"success\": True, \"result\": {\"require_restart\": False}},\n        ):\n            runner = CliRunner()\n            result = runner.invoke(\n                cli.cli,\n                [\"integration\", \"enable\", \"03CD8HS1XF7C3ET52MG6H6OYZI\"],\n                catch_exceptions=False,\n            )\n            assert result.exit_code == 0\n            assert \"Successfully enabled\" in result.output\n\n\ndef test_integration_list_disabled() -> None:\n    \"\"\"Test listing only disabled integrations.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list-disabled\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert data[0][\"domain\"] == \"zwave_js\"\n        assert data[0][\"disabled_by\"] == \"user\"\n\n\ndef test_integration_list_loaded() -> None:\n    \"\"\"Test listing only loaded integrations.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list-loaded\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 2\n        assert all(entry[\"state\"] == \"loaded\" for entry in data)\n\n\ndef test_integration_list_unloaded() -> None:\n    \"\"\"Test listing only unloaded integrations.\"\"\"\n    with requests_mock.Mocker() as mock_req:\n        mock_req.get(\n            \"http://localhost:8123/api/config/config_entries/entry\",\n            json=SAMPLE_CONFIG_ENTRIES,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"integration\", \"list-unloaded\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert data[0][\"state\"] == \"not_loaded\"\n"
  },
  {
    "path": "tests/test_map.py",
    "content": "\"\"\"Tests file for hass-cli map.\"\"\"\n\nfrom typing import no_type_check\nfrom unittest.mock import patch\n\nimport pytest\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n\n@no_type_check\n@pytest.mark.parametrize(\n    \"service,url\",\n    [\n        (\"openstreetmap\", \"https://www.openstreetmap.org\"),\n        (\"bing\", \"https://www.bing.com\"),\n        (\"google\", \"https://www.google.com\"),\n    ],\n)\ndef test_map_services(service, url, default_entities) -> None:\n    \"\"\"Test map feature.\"\"\"\n    entity_id = \"zone.school\"\n    school = next(\n        (x for x in default_entities if x[\"entity_id\"] == entity_id), \"ERROR!\"\n    )\n\n    print(school)\n    with (\n        requests_mock.Mocker() as mock,\n        patch(\"webbrowser.open_new_tab\") as mocked_browser,\n    ):\n        mock.get(\n            f\"http://localhost:8123/api/states/{entity_id}\",\n            json=school,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"map\", \"--service\", service, \"zone.school\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n\n        callurl = mocked_browser.call_args[0][0]\n\n        assert callurl.startswith(url)\n        assert str(school.get(\"attributes\").get(\"latitude\")) in callurl\n        assert (\n            str(school.get(\"attributes\").get(\"longitude\")) in callurl\n        )  # typing: ignore\n"
  },
  {
    "path": "tests/test_plugins.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport pytest\n\nfrom homeassistant_cli.cli import HomeAssistantCli, cli\n\nDFEAULT_PLUGINS = [\n    \"completion\",\n    \"config\",\n    \"discover\",\n    \"state\",\n    \"entity\",\n    \"event\",\n    \"ha\",\n    \"info\",\n    \"integration\",\n    \"map\",\n    \"raw\",\n    \"service\",\n    \"system\",\n    \"template\",\n    \"area\",\n    \"device\",\n]\n\nDFEAULT_PLUGINS.sort()\n\n\n@pytest.fixture(name=\"defaultplugins_sorted\")\ndef defaultplugins_fixture() -> list[str]:\n    \"\"\"Return the expected default list of plugins.\"\"\"\n    return DFEAULT_PLUGINS\n\n\ndef test_commands_match_expected(defaultplugins_sorted) -> None:\n    \"\"\"Test plugin discovery.\"\"\"\n    hac = HomeAssistantCli()\n\n    ctx = cli.make_context(\"hass-cli\", [\"info\"])\n\n    cmds = hac.list_commands(ctx)\n\n    cmds.sort()\n\n    diff = set(cmds).difference(set(defaultplugins_sorted))\n\n    assert not diff\n\n\n@pytest.mark.parametrize(\n    \"plugin\",\n    [\n        \"service\",\n        \"state\",\n        \"system\",\n        \"template\",\n    ],\n)\ndef test_commands_loads(plugin) -> None:\n    \"\"\"Test loading of command.\"\"\"\n    hac = HomeAssistantCli()\n\n    ctx = cli.make_context(\"hass-cli\", [\"info\"])\n\n    cmd = hac.get_command(ctx, plugin)\n\n    assert cmd\n"
  },
  {
    "path": "tests/test_raw.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\nimport unittest.mock as mocker\nfrom unittest.mock import ANY\n\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.cli as cli\nfrom homeassistant_cli.config import Configuration\n\n\ndef test_raw_get() -> None:\n    \"\"\"Test raw.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/anything\",\n            json={\"message\": \"success\"},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"raw\", \"get\", \"/api/anything\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert data[\"message\"] == \"success\"\n\n\ndef test_raw_post() -> None:\n    \"\"\"Test raw.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.post(\n            \"http://localhost:8123/api/anything\",\n            json={\"message\": \"success\"},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"raw\", \"post\", \"/api/anything\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert data[\"message\"] == \"success\"\n\n\ndef test_apimethod_completion(default_services) -> None:\n    \"\"\"Test completion for raw API methods.\"\"\"\n    cfg = Configuration()\n\n    result = autocompletion.api_methods(cfg, [\"raw\", \"get\"], \"/api/conf\")\n    assert len(result) == 1\n\n    result_dict = dict(result)\n\n    assert \"/api/config\" in result_dict\n\n\n# def test_wsapimethod_completion(default_services) -> None:\n#     \"\"\"Test completion for raw ws API methods.\"\"\"\n#     cfg = Configuration()\n\n#     result = autocompletion.wsapi_methods(\n#         cfg, [\"raw\", \"get\"], \"config/device_registry/l\"\n#     )\n#     assert len(result) == 1\n\n#     result_dict = dict(result)\n\n#     assert \"config/device_registry/list\" in result_dict\n\n\ndef test_raw_ws() -> None:\n    \"\"\"Test websocket.\"\"\"\n    with mocker.patch(\n        \"homeassistant_cli.remote.wsapi\", return_value={\"result\": \"worked\"}\n    ) as mockmethod:\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"raw\", \"ws\", \"config/wsmethod\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n\n        mockmethod.assert_called_once()\n        mockmethod.assert_called_with(ANY, {\"type\": \"config/wsmethod\"})\n\n        data = json.loads(result.output)\n        assert len(data) == 1\n\n\ndef test_raw_ws_data() -> None:\n    \"\"\"Test websocket with data.\"\"\"\n    with mocker.patch(\n        \"homeassistant_cli.remote.wsapi\", return_value={\"result\": \"worked\"}\n    ) as mockmethod:\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\n                \"--output=json\",\n                \"raw\",\n                \"ws\",\n                \"config/wsmethod\",\n                \"--json\",\n                '{ \"id\":\"secret\"}',\n            ],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n\n        mockmethod.assert_called_with(ANY, {\"type\": \"config/wsmethod\", \"id\": \"secret\"})\n\n        data = json.loads(result.output)\n        assert len(data) == 1\n"
  },
  {
    "path": "tests/test_service.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\n\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.autocompletion as autocompletion\nimport homeassistant_cli.cli as cli\nfrom homeassistant_cli.config import Configuration\n\n\ndef test_service_list(default_services) -> None:\n    \"\"\"Test services can be listed.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/services\",\n            json=default_services,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"service\", \"list\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 12\n\n\ndef test_service_filter(default_services) -> None:\n    \"\"\"Test services can be listed.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/services\",\n            json=default_services,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"service\", \"list\", \"homeassistant\\\\..*config.*\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 2\n\n\ndef test_service_completion(default_services) -> None:\n    \"\"\"Test completion for services with filter.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/services\",\n            json=default_services,\n            status_code=200,\n        )\n\n        cfg = Configuration()\n\n        result = autocompletion.services(cfg, [\"service\", \"call\"], \"light.turn\")\n        assert len(result) == 2\n\n        resultdict = dict(result)\n\n        assert \"light.turn_on\" in resultdict\n        assert \"light.turn_off\" in resultdict\n\n\ndef test_service_call(default_services) -> None:\n    \"\"\"Test basic call of a service.\"\"\"\n    with requests_mock.Mocker() as mock:\n        post = mock.post(\n            \"http://localhost:8123/api/services/homeassistant/restart\",\n            json={\"result\": \"bogus\"},\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"service\", \"call\", \"homeassistant.restart\"],\n            catch_exceptions=False,\n        )\n\n        assert result.exit_code == 0\n\n        assert post.call_count == 1\n"
  },
  {
    "path": "tests/test_state.py",
    "content": "\"\"\"Tests file for Home Assistant CLI (hass-cli).\"\"\"\n\nimport json\n\nimport requests_mock\nfrom click.testing import CliRunner\n\nimport homeassistant_cli.cli as cli\n\n# import re\n\n\nEDITED_ENTITY = \"\"\"\n{\n  \"attributes\": {\n    \"auto\": false,\n    \"entity_id\": [\n      \"remote.tv\"\n    ],\n    \"friendly_name\": \"all remotes\",\n    \"hidden\": true,\n    \"order\": 16\n  },\n  \"context\": {\n    \"id\": \"4c511277c55647eb8e7e4acf10fcd617\",\n    \"user_id\": null\n  },\n  \"entity_id\": \"group.all_remotes\",\n  \"last_changed\": \"2018-12-04T10:13:05.914548+00:00\",\n  \"last_updated\": \"2018-12-04T10:13:05.914548+00:00\",\n  \"state\": \"off\"\n}\n\"\"\"\n\nLIST_EDITED_ENTITY = f\"[{EDITED_ENTITY}]\"\n\n\ndef test_state_list(basic_entities_text) -> None:\n    \"\"\"Test entities can be listed.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/states\",\n            text=basic_entities_text,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli, [\"--output=json\", \"state\", \"list\"], catch_exceptions=False\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert json.loads(basic_entities_text) == data\n        assert len(data) == 3\n\n\ndef output_formats(cmd, data, output) -> None:\n    \"\"\"Test output formats.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\"http://localhost:8123/api/states\", text=data, status_code=200)\n\n        runner = CliRunner()\n        result = runner.invoke(cli.cli, cmd, catch_exceptions=False)\n        print(\"--seen--\")\n        print(result.output)\n        print(\"----\")\n        print(\"---expected---\")\n        print(output)\n        print(\"----\")\n        assert result.exit_code == 0\n        assert result.output == output\n\n\ndef test_state_list_table(basic_entities_text, basic_entities_table_text) -> None:\n    \"\"\"Test table.\"\"\"\n    output_formats(\n        [\"--output=table\", \"state\", \"list\"],\n        basic_entities_text,\n        basic_entities_table_text,\n    )\n\n\ndef test_state_default_list_table(\n    basic_entities_text, basic_entities_table_text\n) -> None:\n    \"\"\"Test table.\"\"\"\n    output_formats([\"state\", \"list\"], basic_entities_text, basic_entities_table_text)\n\n\ndef test_state_list_tblformat(\n    basic_entities_text, basic_entities_table_format_text\n) -> None:\n    \"\"\"Test table format.\"\"\"\n    output_formats(\n        [\"--output=table\", \"--table-format=html\", \"state\", \"list\"],\n        basic_entities_text,\n        basic_entities_table_format_text,\n    )\n\n\ndef test_state_list_table_columns(\n    basic_entities_text, basic_entities_table_columns_text\n) -> None:\n    \"\"\"Test table columns.\"\"\"\n    output_formats(\n        [\n            \"--output=table\",\n            \"--columns=entity=attributes.friendly_name,state=state\",\n            \"state\",\n            \"list\",\n        ],\n        basic_entities_text,\n        basic_entities_table_columns_text,\n    )\n\n\ndef test_state_list_table_columns_sortby(\n    basic_entities_text, basic_entities_table_sorted_text\n) -> None:\n    \"\"\"Test table columns.\"\"\"\n    output_formats(\n        [\n            \"--output=table\",\n            (\"--columns=entity=attributes.friendly_name,state=state,last_changed\"),\n            \"--sort-by=last_changed\",\n            \"state\",\n            \"list\",\n        ],\n        basic_entities_text,\n        basic_entities_table_sorted_text,\n    )\n\n\ndef test_state_list_no_header(\n    basic_entities_text, basic_entities_table_no_header_text\n) -> None:\n    \"\"\"Test table no header.\"\"\"\n    output_formats(\n        [\"--output=table\", \"--no-headers\", \"state\", \"list\"],\n        basic_entities_text,\n        basic_entities_table_no_header_text,\n    )\n\n\ndef test_state_get(basic_entities_text, basic_entities) -> None:\n    \"\"\"Test entity get.\"\"\"\n    with requests_mock.Mocker() as mock:\n        sensorone = next(x for x in basic_entities if x[\"entity_id\"] == \"sensor.one\")\n        mock.get(\n            \"http://localhost:8123/api/states/sensor.one\",\n            json=sensorone,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"state\", \"get\", \"sensor.one\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n\n        data = json.loads(result.output)\n        assert len(data) == 1\n        assert \"entity_id\" in data[0]\n        assert data[0][\"entity_id\"] == \"sensor.one\"\n\n\ndef test_state_edit(basic_entities_text, basic_entities) -> None:\n    \"\"\"Test basic edit of state.\"\"\"\n    with requests_mock.Mocker() as mock:\n        get = mock.get(\n            \"http://localhost:8123/api/states\",\n            text=basic_entities_text,\n            status_code=200,\n        )\n        sensorone = next(x for x in basic_entities if x[\"entity_id\"] == \"sensor.one\")\n        get = mock.get(\n            \"http://localhost:8123/api/states/sensor.one\",\n            json=sensorone,\n            status_code=200,\n        )\n        post = mock.post(\n            \"http://localhost:8123/api/states/sensor.one\",\n            text=EDITED_ENTITY,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"state\", \"edit\", \"sensor.one\", \"myspecialstate\"],\n            catch_exceptions=False,\n        )\n\n        assert result.exit_code == 0\n\n        assert get.call_count == 1\n        assert post.call_count == 1\n        assert post.request_history[0].json()[\"state\"] == \"myspecialstate\"\n\n\ndef test_state_toggle(basic_entities_text, basic_entities) -> None:\n    \"\"\"Test basic edit of state.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/states\",\n            text=basic_entities_text,\n            status_code=200,\n        )\n        post = mock.post(\n            \"http://localhost:8123/api/services/homeassistant/toggle\",\n            text=LIST_EDITED_ENTITY,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"state\", \"toggle\", \"sensor.one\"],\n            catch_exceptions=False,\n        )\n\n        assert result.exit_code == 0\n        assert post.call_count == 1\n\n        data = json.loads(result.output)\n        assert isinstance(data, list)\n        assert len(data) == 1\n        assert isinstance(data[0], dict)\n\n\ndef test_state_filter(default_entities) -> None:\n    \"\"\"Test entities can be listed with filter.\"\"\"\n    with requests_mock.Mocker() as mock:\n        mock.get(\n            \"http://localhost:8123/api/states\",\n            json=default_entities,\n            status_code=200,\n        )\n\n        runner = CliRunner()\n        result = runner.invoke(\n            cli.cli,\n            [\"--output=json\", \"state\", \"list\", \"bathroom\"],\n            catch_exceptions=False,\n        )\n        assert result.exit_code == 0\n        data = json.loads(result.output)\n        assert len(data) == 3\n\n        ids = [d[\"entity_id\"] for d in data]\n\n        assert len(ids) == 3\n        assert \"timer.timer_small_bathroom\" in ids\n        assert \"group.small_bathroom_motionview\" in ids\n        assert \"light.small_bathroom_light\" in ids\n\n\n# TODO: FAils with regex._regex_core.error: bad escape \\d at position 7\n# def test_state_history(default_entities) -> None:\n#     \"\"\"Test entities can list history.\"\"\"\n#     with requests_mock.Mocker() as mock:\n#         mock.get(\n#             \"http://localhost:8123/api/states\",\n#             json=default_entities,\n#             status_code=200,\n#         )\n\n#         mock.get(\n#             re.compile(\"http://localhost:8123/api/history/period\"),\n#             json={},\n#             status_code=200,\n#             complete_qs=False,\n#         )\n\n#         runner = CliRunner()\n#         result = runner.invoke(\n#             cli.cli,\n#             [\"--output=json\", \"state\", \"history\", \"bathroom\"],\n#             catch_exceptions=False,\n#         )\n#         assert result.exit_code == 0\n#         # TODO: actually have history result testing\n"
  },
  {
    "path": "tests/test_template.py",
    "content": "\"\"\"Tests for template plugin.\"\"\"\n\nimport os\nimport tempfile\n\nimport pytest\nfrom jinja2.exceptions import UndefinedError\n\nfrom homeassistant_cli.exceptions import UnsafeTemplateError\nfrom homeassistant_cli.plugins.template import SAFE_ENV_VARS, render\n\n\ndef _render_template(content, data=None, strict=False):\n    \"\"\"Helper to render a template string via a temp file.\"\"\"\n    with tempfile.NamedTemporaryFile(\n        mode=\"w\", suffix=\".j2\", delete=False, dir=tempfile.gettempdir()\n    ) as temp_file:\n        temp_file.write(content)\n        temp_file.flush()\n        path = temp_file.name\n    try:\n        return render(path, data or {}, strict=strict)\n    finally:\n        os.unlink(path)\n\n\n# Safe template tests\ndef test_render_plain_text():\n    \"\"\"Plain text renders unchanged.\"\"\"\n    assert _render_template(\"hello world\") == \"hello world\"\n\n\ndef test_render_variable():\n    \"\"\"Template variables are substituted.\"\"\"\n    output = _render_template(\"Hello {{ name }}!\", {\"name\": \"Alice\"})\n    assert output == \"Hello Alice!\"\n\n\ndef test_render_environ_allowed():\n    \"\"\"The environ global can read allowlisted environment variables.\"\"\"\n    os.environ[\"HASS_SERVER\"] = \"http://localhost:8123\"\n    try:\n        output = _render_template(\"{{ environ('HASS_SERVER') }}\")\n        assert output == \"http://localhost:8123\"\n    finally:\n        del os.environ[\"HASS_SERVER\"]\n\n\ndef test_render_environ_blocked_secret():\n    \"\"\"Secret env vars not in the allowlist are not accessible.\"\"\"\n    os.environ[\"HASS_TOKEN\"] = \"super_secret_token\"\n    try:\n        output = _render_template(\n            \"{{ environ('HASS_TOKEN') or 'hidden' }}\"\n        )\n        assert output == \"hidden\"\n        assert \"super_secret_token\" not in output\n    finally:\n        del os.environ[\"HASS_TOKEN\"]\n\n\ndef test_render_environ_blocked_supervisor_token():\n    \"\"\"HASS_SUPERVISOR_TOKEN is not accessible from templates.\"\"\"\n    os.environ[\"HASS_SUPERVISOR_TOKEN\"] = \"supervisor_secret\"\n    try:\n        output = _render_template(\n            \"{{ environ('HASS_SUPERVISOR_TOKEN') or 'hidden' }}\"\n        )\n        assert output == \"hidden\"\n        assert \"supervisor_secret\" not in output\n    finally:\n        del os.environ[\"HASS_SUPERVISOR_TOKEN\"]\n\n\ndef test_render_environ_missing():\n    \"\"\"Missing env var returns None (rendered as empty).\"\"\"\n    output = _render_template(\n        \"{{ environ('HASS_NONEXISTENT_VAR_12345') or 'default' }}\"\n    )\n    assert output == \"default\"\n\n\ndef test_safe_env_vars_no_secrets():\n    \"\"\"The allowlist does not contain any secret variables.\"\"\"\n    secret_vars = {\"HASS_TOKEN\", \"HASS_SUPERVISOR_TOKEN\", \"HASS_PASSWORD\"}\n    assert SAFE_ENV_VARS.isdisjoint(secret_vars)\n\n\ndef test_render_strict_undefined():\n    \"\"\"Strict mode raises on undefined variables.\"\"\"\n    with pytest.raises(UndefinedError):\n        _render_template(\"{{ undefined_var }}\", strict=True)\n\n\n# Sandbox security tests\ndef test_sandbox_blocks_globals_access():\n    \"\"\"Accessing __globals__ on environ is blocked.\"\"\"\n    output = _render_template(\"{{ environ.__globals__ }}\")\n    assert \"builtins\" not in output\n    assert \"__import__\" not in output\n\n\ndef test_sandbox_blocks_builtins_via_globals():\n    \"\"\"Accessing __builtins__ via __globals__ is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\"{% set b = environ.__globals__['__builtins__'] %}{{ b }}\")\n\n\ndef test_sandbox_blocks_import():\n    \"\"\"Importing modules via __builtins__.__import__ is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\n            \"{%- set b = environ.__globals__['__builtins__'] -%}\"\n            \"{%- set os = b['__import__']('os') -%}\"\n            \"{{ os.listdir('/') }}\"\n        )\n\n\ndef test_sandbox_blocks_os_system():\n    \"\"\"Executing os.system via template injection is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\n            \"{%- set b = environ.__globals__['__builtins__'] -%}\"\n            \"{%- set os = b['__import__']('os') -%}\"\n            \"{%- set _ = os.system('echo pwned') -%}\"\n        )\n\n\ndef test_sandbox_blocks_subclass_traversal():\n    \"\"\"Traversing __subclasses__ is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\"{{ ''.__class__.__mro__[4].__subclasses__() }}\")\n\n\ndef test_sandbox_blocks_mro_traversal():\n    \"\"\"Traversing __class__.__mro__ to reach object base is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\n            \"{{ ''.__class__.__mro__[4].__subclasses__()[4].__init__.__globals__ }}\"\n        )\n\n\ndef test_sandbox_blocks_file_open():\n    \"\"\"Opening files via builtins is blocked.\"\"\"\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(\n            \"{%- set b = environ.__globals__['__builtins__'] -%}\"\n            \"{%- set bio = b['__import__']('builtins') -%}\"\n            \"{%- set f = bio.open('/etc/passwd') -%}\"\n            \"{{ f.read() }}\"\n        )\n\n\ndef test_sandbox_blocks_reverse_shell():\n    \"\"\"Test reverse shell payload is blocked.\"\"\"\n    template = (\n        \"{%- set b   = environ.__globals__['__builtins__'] -%}\"\n        \"{%- set os  = b['__import__']('os') -%}\"\n        \"{%- set bio = b['__import__']('builtins') -%}\"\n        \"{%- set _f  = bio.open('/tmp/test_shell.py', 'w') -%}\"\n        \"{%- set _   = _f.write('print(\\\"pwned\\\")') -%}\"\n        \"{%- set _   = _f.close() -%}\"\n        \"{%- set _   = os.system('python /tmp/test_shell.py') -%}\"\n    )\n    with pytest.raises(UnsafeTemplateError, match=\"unsafe operations\"):\n        _render_template(template)\n"
  }
]